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.
This commit is contained in:
Jinjing
2026-06-27 15:57:01 -07:00
parent 0ce4046962
commit c04dcc5c8c
5 changed files with 98 additions and 5 deletions
+26
View File
@@ -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==}
@@ -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<string, unknown>) {
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'])
})
})
@@ -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<RpcClient, 'sendRequest'>,
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)) {
@@ -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<void>) => Promise<boolean>
type LoadStatus = (options?: LoadStatusOptions) => Promise<boolean>
type Params = {
client: RpcClient | null
@@ -21,6 +23,7 @@ type Params = {
commitMessage: string
mountedRef: MutableRefObject<boolean>
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,
@@ -191,6 +191,7 @@ export function useMobileSourceControlRunners(params: Params) {
commitMessage,
mountedRef,
runGitWorkflow,
loadStatus,
setActionError,
setCommitMessage,
setShowActionSheet,