From c04dcc5c8c212e4e1d11fed06d9f946422c6faec Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:57:01 -0700 Subject: [PATCH] Block mobile PR creation on unresolved conflicts and refresh status Prevent creating a hosted review on mobile when there are unresolved merge conflicts. Also, return the latest git status on failures and reload it in the UI to keep the source control screen in sync. --- mobile/pnpm-lock.yaml | 26 +++++++++++++++ ...mobile-hosted-review-create-intent.test.ts | 32 ++++++++++++++++++- .../mobile-hosted-review-create-intent.ts | 32 ++++++++++++++++--- .../use-mobile-create-pr-runner.ts | 12 +++++++ .../use-mobile-source-control-runners.ts | 1 + 5 files changed, 98 insertions(+), 5 deletions(-) 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,