diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml index a7abf175872..700bcc09d05 100644 --- a/mobile/pnpm-lock.yaml +++ b/mobile/pnpm-lock.yaml @@ -1721,48 +1721,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.52.0': resolution: {integrity: sha512-wZg6bLjDvh2KibyI3QFUYo8GTXneIFsd0JvehtvJiUmQ8WRPERgxd/VM4ctWb86U5FT1FkqgS8/wZKVB+AZScg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.52.0': resolution: {integrity: sha512-IngE8uxhNvxcMrLjZNDo9xNLY7rEK33AKnaMd2B46he1e/mz2CfcW6If/U1wUjdRZddm1QzQaciqZkuMkdh1FA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.52.0': resolution: {integrity: sha512-H3+DdFMv/efN3Efmhsv18jDrpiWWqKG7wsfAlQBqAt6z/E2Bx+TwEj2Nowe51CPOWB8/mFBC2dAMSgVFLvvowA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.52.0': resolution: {integrity: sha512-zji+1kb7lJKohSDjzC1IsS+K/cKRs1hdVf0ZH0VbdbiakmtLvN9twBoXo/k8VdjFax7kfo+DyPxS7vv52br1aw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.52.0': resolution: {integrity: sha512-hcLBYedpCy7ToUvvBidWk7+11Yhg1oAZ4+6hKPic/mQI6NaqXJSXMps5nFlwUuX2ewhtLZZDPg63TI042qGKBg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.52.0': resolution: {integrity: sha512-IDO2loXK2OtTOhSPchU9MW25mWL2QCDGdJbjN8MXKZVS80qXe5gMTwQWu/gMJ3juoBHbkuUZNB2N1LHzNT7DoA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.52.0': resolution: {integrity: sha512-mAV2Hjn0SatJ+KoAzKUC3eJhdJ8wv+3m1KyuS0dTsbF0c5weq+QrCt/DRZZM+uj/XiKzCDEUKYsBF30e2qkcyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxfmt/binding-openharmony-arm64@0.52.0': resolution: {integrity: sha512-vd4npaUIwChxp7XzkqmepBWTT9YMcSe/NBApVGPC30/lLyOVaV3dvma1SKo03t8O73BPRAG7EyJzGlN5cJM5hQ==} @@ -1835,48 +1843,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.67.0': resolution: {integrity: sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.67.0': resolution: {integrity: sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.67.0': resolution: {integrity: sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.67.0': resolution: {integrity: sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.67.0': resolution: {integrity: sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.67.0': resolution: {integrity: sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-musl@1.67.0': resolution: {integrity: sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/binding-openharmony-arm64@1.67.0': resolution: {integrity: sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g==} @@ -2338,36 +2354,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0': resolution: {integrity: sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0': resolution: {integrity: sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0': resolution: {integrity: sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0': resolution: {integrity: sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0': resolution: {integrity: sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0': resolution: {integrity: sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==} @@ -4629,24 +4651,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} diff --git a/mobile/src/source-control/mobile-hosted-review-create-intent.test.ts b/mobile/src/source-control/mobile-hosted-review-create-intent.test.ts index 5f004f9c239..4f756d67b3b 100644 --- a/mobile/src/source-control/mobile-hosted-review-create-intent.test.ts +++ b/mobile/src/source-control/mobile-hosted-review-create-intent.test.ts @@ -25,6 +25,10 @@ function entry(area: 'unstaged' | 'untracked' | 'staged') { return { path: 'a.ts', status: 'modified', area } } +function unresolvedEntry(area: 'unstaged' | 'staged') { + return { path: 'conflicted.ts', status: 'modified', area, conflictStatus: 'unresolved' } +} + function eligibility(overrides: Record) { return { provider: 'github', @@ -139,7 +143,11 @@ describe('prepareMobileHostedReviewCreateIntent', () => { }) ).resolves.toEqual({ ok: false, - error: 'Could not generate a commit message. Add one in Source Control, then retry.' + error: 'Could not generate a commit message. Add one in Source Control, then retry.', + committed: false, + status: expect.objectContaining({ + entries: [expect.objectContaining({ area: 'staged' })] + }) }) expect(client.calls.map((call) => call.method)).toEqual([ @@ -147,4 +155,26 @@ describe('prepareMobileHostedReviewCreateIntent', () => { 'git.generateCommitMessage' ]) }) + + it('blocks unresolved conflicts before attempting a commit', async () => { + const client = clientWith([ok(status([entry('staged'), unresolvedEntry('unstaged')]))]) + + await expect( + prepareMobileHostedReviewCreateIntent(client, 'repo-1::/tmp/wt', { + branch: 'feature/x', + title: 'feature/x', + status: null, + commitMessage: 'Use my message' + }) + ).resolves.toEqual({ + ok: false, + error: 'Resolve conflicts before creating a pull request.', + committed: false, + status: expect.objectContaining({ + entries: expect.arrayContaining([expect.objectContaining({ path: 'conflicted.ts' })]) + }) + }) + + expect(client.calls.map((call) => call.method)).toEqual(['git.status']) + }) }) diff --git a/mobile/src/source-control/mobile-hosted-review-create-intent.ts b/mobile/src/source-control/mobile-hosted-review-create-intent.ts index 404adf0f234..b4c5d6bb3dd 100644 --- a/mobile/src/source-control/mobile-hosted-review-create-intent.ts +++ b/mobile/src/source-control/mobile-hosted-review-create-intent.ts @@ -72,6 +72,10 @@ function branchStillMatches(inputBranch: string, status: MobileGitStatusResult | ) } +function hasUnresolvedConflicts(status: MobileGitStatusResult | null): boolean { + return status?.entries.some((entry) => entry.conflictStatus === 'unresolved') === true +} + async function sendGitMutation( client: Pick, method: string, @@ -138,6 +142,14 @@ async function ensureLocalChangesCommitted( if ((currentStatus?.entries.length ?? 0) === 0) { return { ok: true, status: currentStatus, committed: false } } + if (hasUnresolvedConflicts(currentStatus)) { + return { + ok: false, + error: 'Resolve conflicts before creating a pull request.', + committed: false, + status: currentStatus + } + } const stagePaths = getStageablePaths(currentStatus?.entries ?? []) if (stagePaths.length > 0) { @@ -153,13 +165,23 @@ async function ensureLocalChangesCommitted( } currentStatus = await readStatus(client, worktreeId) if (!branchStillMatches(input.branch, currentStatus)) { - return { ok: false, error: 'Branch changed while preparing the pull request.' } + return { + ok: false, + error: 'Branch changed while preparing the pull request.', + committed: false, + status: currentStatus + } } } const hasStagedChanges = currentStatus?.entries.some((entry) => entry.area === 'staged') === true if (!hasStagedChanges) { - return { ok: false, error: 'Resolve or stage changes before creating a pull request.' } + return { + ok: false, + error: 'Resolve or stage changes before creating a pull request.', + committed: false, + status: currentStatus + } } let message = input.commitMessage?.trim() ?? '' @@ -169,7 +191,9 @@ async function ensureLocalChangesCommitted( if (!generated.success) { return { ok: false, - error: 'Could not generate a commit message. Add one in Source Control, then retry.' + error: 'Could not generate a commit message. Add one in Source Control, then retry.', + committed: false, + status: currentStatus } } message = generated.message @@ -178,7 +202,7 @@ async function ensureLocalChangesCommitted( input.onProgress?.('committing') const committed = await commitStagedChanges(client, worktreeId, message) if (!committed.ok) { - return committed + return { ...committed, committed: false, status: currentStatus } } currentStatus = await readStatus(client, worktreeId) if (!branchStillMatches(input.branch, currentStatus)) { diff --git a/mobile/src/source-control/use-mobile-create-pr-runner.ts b/mobile/src/source-control/use-mobile-create-pr-runner.ts index 9cd7d978834..9952faaaa3d 100644 --- a/mobile/src/source-control/use-mobile-create-pr-runner.ts +++ b/mobile/src/source-control/use-mobile-create-pr-runner.ts @@ -2,6 +2,7 @@ import { useCallback, type MutableRefObject } from 'react' import type { RpcClient } from '../transport/rpc-client' import { triggerError } from '../platform/haptics' import type { MobileGitStatusResult } from './mobile-git-status' +import type { LoadStatusOptions } from './mobile-source-control-screen-state' import { mobileHostedReviewCreateIntentProgressMessage, type MobileHostedReviewCreateIntentProgress @@ -12,6 +13,7 @@ import { } from './mobile-hosted-review-create-intent-runner' type RunGitWorkflow = (actionId: string, runner: () => Promise) => Promise +type LoadStatus = (options?: LoadStatusOptions) => Promise type Params = { client: RpcClient | null @@ -21,6 +23,7 @@ type Params = { commitMessage: string mountedRef: MutableRefObject runGitWorkflow: RunGitWorkflow + loadStatus: LoadStatus setActionError: (next: string | null) => void setCommitMessage: (next: string) => void setShowActionSheet: (next: boolean) => void @@ -36,6 +39,7 @@ export function useMobileCreatePrRunner({ commitMessage, mountedRef, runGitWorkflow, + loadStatus, setActionError, setCommitMessage, setShowActionSheet, @@ -71,6 +75,13 @@ export function useMobileCreatePrRunner({ if (outcome?.committed && mountedRef.current) { setCommitMessage('') } + if (!ran && outcome?.status !== undefined && mountedRef.current) { + await loadStatus({ + preserveReadyOnFailure: true, + clearActionErrorOnSuccess: false, + force: true + }) + } if (!ran || !mountedRef.current || !outcome || !outcome.ok) { return } @@ -82,6 +93,7 @@ export function useMobileCreatePrRunner({ branchLabel, client, commitMessage, + loadStatus, mountedRef, runGitWorkflow, setActionError, diff --git a/mobile/src/source-control/use-mobile-source-control-runners.ts b/mobile/src/source-control/use-mobile-source-control-runners.ts index 6bddf9bdd1f..a04ead69690 100644 --- a/mobile/src/source-control/use-mobile-source-control-runners.ts +++ b/mobile/src/source-control/use-mobile-source-control-runners.ts @@ -191,6 +191,7 @@ export function useMobileSourceControlRunners(params: Params) { commitMessage, mountedRef, runGitWorkflow, + loadStatus, setActionError, setCommitMessage, setShowActionSheet,