Files
orca/src/shared/source-control-create-review-intent.ts
T
Kelvin Amoaba 75743353de fix(mobile): prepare Create PR and the commit button with desktop's rules (#34011)
* fix(mobile): prepare Create PR and the commit action like desktop

Create PR now fast-forwards a behind-only branch before committing, picks its push step
from desktop's shared rule, and the commit-area button reads the branch's review state.

* fix(mobile): keep Create PR commit failures intact and skip fork push-target gating

Restaging after a failed commit is best effort and checks the branch first, and reviews
with their own push target are not gated until mobile reads targeted upstream status.

* fix(mobile): enter Create PR on desktop's intent rule and leave fork push targets ungated

* refactor(mobile): share the linked-review push decision and enter Create PR on desktop's intent rule

Desktop and mobile compose the review push target through one shared function, the
Create button admits branches Create PR can prepare, and older hosts without a lookup
outcome still fail closed on mobile.
2026-10-27 03:19:49 +00:00

138 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { isBehindOnlyUpstream, shouldForcePushWithLeaseForUpstream } from './git-upstream-status'
import { supportsHostedReviewCreation } from './hosted-review-creation-providers'
import type { GitUpstreamStatus } from './git-status-types'
export type CreateReviewIntentKind =
| 'dirty'
| 'message_required'
| 'no_upstream'
| 'needs_push'
| 'needs_sync'
| 'force_push'
export type CreateReviewIntentEligibility = {
eligible: boolean
kind: CreateReviewIntentKind | null
}
export function resolveCreateReviewIntentEligibility({
stagedCount,
hasStageableChanges,
hasMessage,
hasUnresolvedConflicts,
upstreamStatus,
hostedReviewCreation,
branchCommitsAhead,
hasCurrentBranch = true
}: {
stagedCount: number
hasStageableChanges: boolean
hasMessage: boolean
hasUnresolvedConflicts: boolean
upstreamStatus: GitUpstreamStatus | undefined
// Strings, not the closed unions: mobile round-trips tokens newer hosts add.
hostedReviewCreation?: {
provider: string
canCreate?: boolean
blockedReason?: string | null
reviewLookupOutcome?: string
defaultBaseRef?: string | null
} | null
branchCommitsAhead?: number
hasCurrentBranch?: boolean
}): CreateReviewIntentEligibility {
if (
hasUnresolvedConflicts ||
!hasCurrentBranch ||
!hostedReviewCreation ||
hostedReviewCreation.canCreate ||
!supportsHostedReviewCreation(hostedReviewCreation.provider)
) {
return { eligible: false, kind: null }
}
if (
hostedReviewCreation.reviewLookupOutcome === 'unavailable' &&
!hostedReviewCreation.defaultBaseRef?.trim()
) {
return { eligible: false, kind: null }
}
// Branch preparation needs no lookup authority; create preflight still fails closed against duplicates.
if (hostedReviewCreation.blockedReason === 'dirty') {
if (stagedCount > 0 && !hasMessage) {
return { eligible: true, kind: 'message_required' }
}
return { eligible: stagedCount > 0 || hasStageableChanges, kind: 'dirty' }
}
if (hostedReviewCreation.blockedReason === 'no_upstream') {
const hasPublishableCommits = branchCommitsAhead === undefined ? false : branchCommitsAhead > 0
return {
eligible: hasPublishableCommits || stagedCount > 0 || hasStageableChanges,
kind: 'no_upstream'
}
}
if (hostedReviewCreation.blockedReason === 'needs_push') {
return { eligible: true, kind: 'needs_push' }
}
if (
hostedReviewCreation.blockedReason === 'needs_sync' &&
shouldForcePushWithLeaseForUpstream(upstreamStatus)
) {
return { eligible: true, kind: 'force_push' }
}
// Behind-only allows ff-only preparation; divergence requires the user’s explicit sync step.
if (hostedReviewCreation.blockedReason === 'needs_sync' && isBehindOnlyUpstream(upstreamStatus)) {
return { eligible: true, kind: 'needs_sync' }
}
return { eligible: false, kind: null }
}
export type CreatePrIntentRemoteStep =
| 'publish'
| 'push'
| 'force_push'
| 'fast_forward'
| 'blocked'
| 'none'
export function resolveCreatePrIntentRemoteStep({
upstreamStatus,
hostedReviewCreation,
branchCommitsAhead,
hasCurrentBranch
}: {
upstreamStatus: GitUpstreamStatus | undefined
// Strings, not the closed union: mobile round-trips reasons newer hosts add.
hostedReviewCreation?: { canCreate?: boolean; blockedReason?: string | null } | null
branchCommitsAhead?: number
hasCurrentBranch: boolean
}): CreatePrIntentRemoteStep {
if (!hasCurrentBranch || !hostedReviewCreation || hostedReviewCreation.canCreate) {
return 'none'
}
if (hostedReviewCreation.blockedReason === 'no_upstream') {
return branchCommitsAhead && branchCommitsAhead > 0 ? 'publish' : 'blocked'
}
if (hostedReviewCreation.blockedReason === 'needs_push') {
return 'push'
}
if (hostedReviewCreation.blockedReason === 'needs_sync') {
if (shouldForcePushWithLeaseForUpstream(upstreamStatus)) {
return 'force_push'
}
// Enforce ff-only at execution time to prevent mid-flight divergence or pull.ff=no from silently creating merge commits.
return isBehindOnlyUpstream(upstreamStatus) ? 'fast_forward' : 'blocked'
}
return 'none'
}