mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
Align mobile review quick action with desktop
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -1,78 +1,14 @@
|
||||
import { shouldForcePushWithLeaseForUpstream } from '../../../../shared/git-upstream-status'
|
||||
import type { HostedReviewCreationEligibility } from '../../../../shared/hosted-review'
|
||||
import { supportsHostedReviewCreation } from '../../../../shared/hosted-review-creation-providers'
|
||||
import type { GitUpstreamStatus } from '../../../../shared/types'
|
||||
import type { PrimaryAction } from './source-control-primary-action-types'
|
||||
import {
|
||||
resolveCreateReviewIntentEligibility,
|
||||
type CreateReviewIntentEligibility,
|
||||
type CreateReviewIntentKind
|
||||
} from '../../../../shared/source-control-create-review-intent'
|
||||
|
||||
export type CreatePrIntentKind =
|
||||
| 'dirty'
|
||||
| 'message_required'
|
||||
| 'no_upstream'
|
||||
| 'needs_push'
|
||||
| 'force_push'
|
||||
export type CreatePrIntentKind = CreateReviewIntentKind
|
||||
export type CreatePrIntentEligibility = CreateReviewIntentEligibility
|
||||
|
||||
export type CreatePrIntentEligibility = {
|
||||
eligible: boolean
|
||||
kind: CreatePrIntentKind | null
|
||||
}
|
||||
|
||||
export function resolveCreatePrIntentEligibility({
|
||||
stagedCount,
|
||||
hasStageableChanges,
|
||||
hasMessage,
|
||||
hasUnresolvedConflicts,
|
||||
upstreamStatus,
|
||||
hostedReviewCreation,
|
||||
branchCommitsAhead,
|
||||
hasCurrentBranch = true
|
||||
}: {
|
||||
stagedCount: number
|
||||
hasStageableChanges: boolean
|
||||
hasMessage: boolean
|
||||
hasUnresolvedConflicts: boolean
|
||||
upstreamStatus: GitUpstreamStatus | undefined
|
||||
hostedReviewCreation?: HostedReviewCreationEligibility | null
|
||||
branchCommitsAhead?: number
|
||||
hasCurrentBranch?: boolean
|
||||
}): CreatePrIntentEligibility {
|
||||
if (
|
||||
hasUnresolvedConflicts ||
|
||||
!hasCurrentBranch ||
|
||||
!hostedReviewCreation ||
|
||||
hostedReviewCreation.canCreate ||
|
||||
!supportsHostedReviewCreation(hostedReviewCreation.provider)
|
||||
) {
|
||||
return { eligible: false, kind: null }
|
||||
}
|
||||
|
||||
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' }
|
||||
}
|
||||
|
||||
return { eligible: false, kind: null }
|
||||
}
|
||||
export const resolveCreatePrIntentEligibility = resolveCreateReviewIntentEligibility
|
||||
|
||||
export function resolveVisibleCreatePrHeaderAction({
|
||||
createPrHeaderAction
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { HostedReviewCreationEligibility } from '../../../../shared/hosted-review'
|
||||
import type { GitUpstreamStatus, PRState } from '../../../../shared/types'
|
||||
import type {
|
||||
SourceControlPrimaryActionKind,
|
||||
SourceControlRemoteOpKind
|
||||
} from '../../../../shared/source-control-primary-action-decision-types'
|
||||
|
||||
// Why: the primary button collapses to one-label-per-action. Compound
|
||||
// kinds ('commit_push', 'commit_sync', 'commit_publish') live in
|
||||
@@ -8,15 +12,7 @@ import type { GitUpstreamStatus, PRState } from '../../../../shared/types'
|
||||
// `handlePrimaryClick` switch exhaustively over only the kinds the
|
||||
// primary can actually emit, and it kills the compound-commit branch in
|
||||
// the isRemoteOperationActive tooltip below at compile time.
|
||||
export type PrimaryActionKind =
|
||||
| 'commit'
|
||||
| 'stage'
|
||||
| 'push'
|
||||
| 'pull'
|
||||
| 'sync'
|
||||
| 'publish'
|
||||
| 'create_pr_intent'
|
||||
| 'create_pr'
|
||||
export type PrimaryActionKind = SourceControlPrimaryActionKind
|
||||
|
||||
// Why: the in-flight remote op tracker stores which action the user actually
|
||||
// triggered, so the primary button can mirror that label/spinner instead of
|
||||
@@ -24,15 +20,7 @@ export type PrimaryActionKind =
|
||||
// kinds are included because they participate in the busy flag, but they are
|
||||
// intentionally NOT in PrimaryActionKind — when Fetch is in flight the primary
|
||||
// keeps its natural label, while Force Push maps back to the push icon/slot.
|
||||
export type RemoteOpKind =
|
||||
| 'push'
|
||||
| 'force_push'
|
||||
| 'pull'
|
||||
| 'sync'
|
||||
| 'fetch'
|
||||
| 'fast_forward'
|
||||
| 'publish'
|
||||
| 'rebase'
|
||||
export type RemoteOpKind = SourceControlRemoteOpKind
|
||||
|
||||
export type PrimaryAction = {
|
||||
kind: PrimaryActionKind
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
// Why: split from the combined primary+dropdown module because the primary and dropdown are independent derivations with different priority ladders; together they exceed the max-lines budget and tangle unrelated concerns.
|
||||
|
||||
import { shouldForcePushWithLeaseForUpstream } from '../../../../shared/git-upstream-status'
|
||||
import {
|
||||
resolveSourceControlCommitAreaPrimaryActionDecision,
|
||||
resolveSourceControlPrimaryActionDecision
|
||||
} from '../../../../shared/source-control-primary-action-decision'
|
||||
import type { SourceControlPrimaryActionDecision } from '../../../../shared/source-control-primary-action-decision-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
localizedHostedReviewCopy,
|
||||
resolveSupportedHostedReviewCopyProvider
|
||||
} from '@/i18n/hosted-review-localized-copy'
|
||||
import { type PrimaryAction, type PrimaryActionInputs } from './source-control-primary-action-types'
|
||||
import { resolvePrimaryActionDuringRemoteOp } from './source-control-primary-action-in-flight'
|
||||
import {
|
||||
describeForcePushWithLease,
|
||||
describePullCount,
|
||||
describePushCount,
|
||||
describeSyncCounts
|
||||
} from './source-control-primary-action-titles'
|
||||
import { resolveLinkedReviewPrimaryAction } from './source-control-linked-review-primary-action'
|
||||
import {
|
||||
resolveCreatePrIntentInFlightPrimaryAction,
|
||||
resolveCreatePrIntentPrimaryAction
|
||||
} from './source-control-primary-create-pr-intent-action'
|
||||
import { resolveUnpublishedPrimaryAction } from './source-control-primary-unpublished-action'
|
||||
|
||||
export type {
|
||||
PrimaryActionKind,
|
||||
@@ -28,10 +25,8 @@ export type {
|
||||
PrimaryActionInputs
|
||||
} from './source-control-primary-action-types'
|
||||
|
||||
// Why: this module owns the pure state-machine logic for the Source Control
|
||||
// primary action (split button). Keeping the logic outside the React component
|
||||
// makes it straightforward to unit-test each row of the priority table without
|
||||
// spinning up a renderer.
|
||||
// Why: the shared module owns the pure state-machine logic; this renderer
|
||||
// adapter keeps localized copy and the historical exported shape in place.
|
||||
|
||||
/**
|
||||
* Resolve the primary split-button action.
|
||||
@@ -54,250 +49,200 @@ export type {
|
||||
* through "Publish Branch" on every worktree switch.
|
||||
*/
|
||||
export function resolvePrimaryAction(inputs: PrimaryActionInputs): PrimaryAction {
|
||||
const {
|
||||
stagedCount,
|
||||
hasUnstagedChanges,
|
||||
hasStageableChanges,
|
||||
hasMessage,
|
||||
hasUnresolvedConflicts,
|
||||
isCommitting,
|
||||
isRemoteOperationActive,
|
||||
upstreamStatus,
|
||||
prState,
|
||||
isPRStateLoading,
|
||||
hostedReviewCreation,
|
||||
branchCommitsAhead,
|
||||
hasCurrentBranch = true,
|
||||
canPushLinkedReviewWithoutUpstream = false,
|
||||
isPrIntentInFlight = false
|
||||
} = inputs
|
||||
|
||||
if (isPrIntentInFlight) {
|
||||
return resolveCreatePrIntentInFlightPrimaryAction(inputs)
|
||||
}
|
||||
|
||||
// 1. Commit in flight — lock the primary no matter what else is true.
|
||||
if (isCommitting) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.ed93b4f14f',
|
||||
'Commit'
|
||||
),
|
||||
title: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.16aee3a5c1',
|
||||
'Commit in progress…'
|
||||
),
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
if (isRemoteOperationActive) {
|
||||
return resolvePrimaryActionDuringRemoteOp(inputs, resolvePrimaryAction)
|
||||
}
|
||||
|
||||
// 3. Unresolved conflicts block any commit path.
|
||||
if (hasUnresolvedConflicts) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.ed93b4f14f',
|
||||
'Commit'
|
||||
),
|
||||
title: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.a6457b46a7',
|
||||
'Resolve conflicts before committing'
|
||||
),
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
const createPrIntent = resolveCreatePrIntentPrimaryAction(inputs)
|
||||
if (createPrIntent) {
|
||||
return createPrIntent
|
||||
}
|
||||
|
||||
const hasStaged = stagedCount > 0
|
||||
const hasOpenHostedReview = prState === 'open' || prState === 'draft'
|
||||
|
||||
// 5. Has staged files + message → plain Commit. The primary button never
|
||||
// compounds ("Commit & Push" etc.) — after the commit lands, the primary
|
||||
// naturally rotates to the appropriate remote action (Push / Sync /
|
||||
// Publish Branch) via step 7 below. Users who want the one-click
|
||||
// compound flow can still reach it from the dropdown.
|
||||
if (hasStaged && hasMessage) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.ed93b4f14f',
|
||||
'Commit'
|
||||
),
|
||||
title: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.ab41fb926b',
|
||||
'Commit staged changes'
|
||||
),
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Has staged files but no message — user just needs to type something.
|
||||
if (hasStaged && !hasMessage) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.ed93b4f14f',
|
||||
'Commit'
|
||||
),
|
||||
title: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.f01f16d77f',
|
||||
'Enter a commit message to commit'
|
||||
),
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
// 6b. Nothing staged but local changes exist — surface staging as the
|
||||
// primary so dirty trees don't invite a remote op (pull/sync would fail
|
||||
// with uncommitted changes; push/publish skips the actual user need).
|
||||
// Sits before the upstream-status checks so it works regardless of
|
||||
// whether upstream has resolved yet.
|
||||
if (!hasStaged && hasStageableChanges) {
|
||||
return {
|
||||
kind: 'stage',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.18a0fca877',
|
||||
'Stage All'
|
||||
),
|
||||
title: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.5a477d80cb',
|
||||
'Stage all changes'
|
||||
),
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Clean tree + no staged files → adaptive remote action.
|
||||
if (!upstreamStatus) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.ed93b4f14f',
|
||||
'Commit'
|
||||
),
|
||||
title: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.fa3bd4f40c',
|
||||
'Stage at least one file to commit'
|
||||
),
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
if (!upstreamStatus.hasUpstream) {
|
||||
const unpublishedAction = resolveUnpublishedPrimaryAction({
|
||||
hasCurrentBranch,
|
||||
isPRStateLoading,
|
||||
prState
|
||||
})
|
||||
|
||||
if (unpublishedAction.kind === 'publish') {
|
||||
const linkedReviewAction = resolveLinkedReviewPrimaryAction({
|
||||
hasOpenHostedReview,
|
||||
canPushLinkedReviewWithoutUpstream
|
||||
})
|
||||
if (linkedReviewAction) {
|
||||
return linkedReviewAction
|
||||
}
|
||||
}
|
||||
|
||||
return unpublishedAction
|
||||
}
|
||||
|
||||
if (upstreamStatus.ahead > 0 && upstreamStatus.behind > 0) {
|
||||
if (shouldForcePushWithLeaseForUpstream(upstreamStatus)) {
|
||||
return {
|
||||
kind: 'push',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.390abeab93',
|
||||
'Force Push'
|
||||
),
|
||||
title: describeForcePushWithLease(branchCommitsAhead, upstreamStatus.upstreamName),
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
return {
|
||||
kind: 'sync',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.795f1509c5',
|
||||
'Sync'
|
||||
),
|
||||
title: describeSyncCounts(upstreamStatus.ahead, upstreamStatus.behind),
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
if (upstreamStatus.behind > 0) {
|
||||
return {
|
||||
kind: 'pull',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.d64292a938',
|
||||
'Pull'
|
||||
),
|
||||
title: describePullCount(upstreamStatus.behind),
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
if (upstreamStatus.ahead > 0) {
|
||||
return {
|
||||
kind: 'push',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.95550cff15',
|
||||
'Push'
|
||||
),
|
||||
title: describePushCount(upstreamStatus.ahead),
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
if (hostedReviewCreation?.canCreate) {
|
||||
const copy = localizedHostedReviewCopy(
|
||||
resolveSupportedHostedReviewCopyProvider(hostedReviewCreation.provider)
|
||||
)
|
||||
return {
|
||||
kind: 'create_pr',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.e7ffa46946',
|
||||
'Create {{value0}}',
|
||||
{ value0: copy.shortLabel }
|
||||
),
|
||||
title: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.946a8a05ea',
|
||||
'Create a {{value0}} for this branch',
|
||||
{ value0: copy.reviewLabel }
|
||||
),
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
// Clean + tracked + in sync — distinguish truly clean from work that still
|
||||
// needs staging before commit can proceed.
|
||||
return {
|
||||
kind: 'commit',
|
||||
label: translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.ed93b4f14f',
|
||||
'Commit'
|
||||
),
|
||||
title: hasUnstagedChanges
|
||||
? 'Stage at least one file to commit'
|
||||
: 'Nothing to commit. Branch is up to date.',
|
||||
disabled: true
|
||||
}
|
||||
return toRendererPrimaryAction(resolveSourceControlPrimaryActionDecision(inputs), inputs)
|
||||
}
|
||||
|
||||
export function resolveCommitAreaPrimaryAction(inputs: PrimaryActionInputs): PrimaryAction {
|
||||
// Why: review creation is additive chrome. The commit area should keep the
|
||||
// same local/remote primary action it would have without review eligibility.
|
||||
return resolvePrimaryAction({
|
||||
...inputs,
|
||||
hostedReviewCreation: null,
|
||||
isPrIntentInFlight: false
|
||||
})
|
||||
return toRendererPrimaryAction(
|
||||
resolveSourceControlCommitAreaPrimaryActionDecision(inputs),
|
||||
inputs
|
||||
)
|
||||
}
|
||||
|
||||
function toRendererPrimaryAction(
|
||||
decision: SourceControlPrimaryActionDecision,
|
||||
inputs: PrimaryActionInputs
|
||||
): PrimaryAction {
|
||||
return {
|
||||
kind: decision.kind,
|
||||
label: resolvePrimaryActionLabel(decision, inputs),
|
||||
title: resolvePrimaryActionTitle(decision, inputs),
|
||||
disabled: decision.disabled
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePrimaryActionLabel(
|
||||
decision: SourceControlPrimaryActionDecision,
|
||||
inputs: PrimaryActionInputs
|
||||
): string {
|
||||
if (decision.labelIntent === 'force_push') {
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.390abeab93',
|
||||
'Force Push'
|
||||
)
|
||||
}
|
||||
if (decision.labelIntent === 'create_pr') {
|
||||
const copy = localizedHostedReviewCopy(
|
||||
resolveSupportedHostedReviewCopyProvider(inputs.hostedReviewCreation?.provider)
|
||||
)
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.e7ffa46946',
|
||||
'Create {{value0}}',
|
||||
{ value0: copy.shortLabel }
|
||||
)
|
||||
}
|
||||
switch (decision.labelIntent) {
|
||||
case 'commit':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.ed93b4f14f',
|
||||
'Commit'
|
||||
)
|
||||
case 'stage':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.18a0fca877',
|
||||
'Stage All'
|
||||
)
|
||||
case 'push':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.95550cff15',
|
||||
'Push'
|
||||
)
|
||||
case 'pull':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.d64292a938',
|
||||
'Pull'
|
||||
)
|
||||
case 'sync':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.795f1509c5',
|
||||
'Sync'
|
||||
)
|
||||
case 'publish':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.7b4d02e6b8',
|
||||
'Publish Branch'
|
||||
)
|
||||
case 'create_pr_intent':
|
||||
return resolvePrimaryActionLabel({ ...decision, labelIntent: 'create_pr' }, inputs)
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePrimaryActionTitle(
|
||||
decision: SourceControlPrimaryActionDecision,
|
||||
inputs: PrimaryActionInputs
|
||||
): string {
|
||||
const copy = localizedHostedReviewCopy(
|
||||
resolveSupportedHostedReviewCopyProvider(inputs.hostedReviewCreation?.provider)
|
||||
)
|
||||
switch (decision.titleIntent) {
|
||||
case 'commit_in_progress':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.16aee3a5c1',
|
||||
'Commit in progress…'
|
||||
)
|
||||
case 'force_push_in_progress':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.74fc171e99',
|
||||
'Force Push in progress…'
|
||||
)
|
||||
case 'action_in_progress':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.484f45c439',
|
||||
'{{value0}} in progress…',
|
||||
{ value0: resolvePrimaryActionLabel(decision, inputs) }
|
||||
)
|
||||
case 'remote_operation_in_progress':
|
||||
return 'Remote operation in progress…'
|
||||
case 'remote_operation_blocks_commit':
|
||||
return 'Remote operation in progress — try again once it finishes'
|
||||
case 'resolve_conflicts_before_commit':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.a6457b46a7',
|
||||
'Resolve conflicts before committing'
|
||||
)
|
||||
case 'prepare_review':
|
||||
if (decision.disabled) {
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.d37e68f61d',
|
||||
'Preparing branch for review…'
|
||||
)
|
||||
}
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.c72e5e65d1',
|
||||
'Prepare this branch and create a {{value0}}',
|
||||
{ value0: copy.reviewLabel }
|
||||
)
|
||||
case 'commit_staged_changes':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.ab41fb926b',
|
||||
'Commit staged changes'
|
||||
)
|
||||
case 'enter_commit_message':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.f01f16d77f',
|
||||
'Enter a commit message to commit'
|
||||
)
|
||||
case 'stage_all_changes':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.5a477d80cb',
|
||||
'Stage all changes'
|
||||
)
|
||||
case 'stage_file_to_commit':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.fa3bd4f40c',
|
||||
'Stage at least one file to commit'
|
||||
)
|
||||
case 'checkout_branch_before_publish':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.e61b0d7a3c',
|
||||
'Check out a branch before publishing commits.'
|
||||
)
|
||||
case 'checking_review_status':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.41d4bcf157',
|
||||
'Checking PR status…'
|
||||
)
|
||||
case 'review_already_merged':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.3d5dccef0b',
|
||||
'Nothing to commit. PR is already merged.'
|
||||
)
|
||||
case 'publish_branch':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.1884cf34af',
|
||||
'Publish this branch to origin'
|
||||
)
|
||||
case 'push_linked_review':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.1d47e850cf',
|
||||
'Push updates to the linked review branch'
|
||||
)
|
||||
case 'linked_review_target_unavailable':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.c39d0c75c3',
|
||||
'Linked review branch target is unavailable.'
|
||||
)
|
||||
case 'force_push_with_lease':
|
||||
return describeForcePushWithLease(decision.count, decision.upstreamName)
|
||||
case 'sync_counts':
|
||||
return describeSyncCounts(decision.ahead ?? 0, decision.behind ?? 0)
|
||||
case 'pull_count':
|
||||
return describePullCount(decision.count ?? 0)
|
||||
case 'push_count':
|
||||
return describePushCount(decision.count ?? 0)
|
||||
case 'create_review':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.946a8a05ea',
|
||||
'Create a {{value0}} for this branch',
|
||||
{ value0: copy.reviewLabel }
|
||||
)
|
||||
case 'nothing_to_commit_up_to_date':
|
||||
return 'Nothing to commit. Branch is up to date.'
|
||||
case 'checking_review_creation':
|
||||
return translate(
|
||||
'auto.components.right.sidebar.source.control.primary.action.h3i4j5k607',
|
||||
'Checking whether this branch can create a {{value0}}…',
|
||||
{ value0: copy.reviewLabel }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { shouldForcePushWithLeaseForUpstream } from './git-upstream-status'
|
||||
import type { HostedReviewCreationEligibility } from './hosted-review'
|
||||
import { supportsHostedReviewCreation } from './hosted-review-creation-providers'
|
||||
import type { GitUpstreamStatus } from './git-status-types'
|
||||
import type { SourceControlPrimaryActionDecision } from './source-control-primary-action-decision-types'
|
||||
|
||||
export type CreateReviewIntentKind =
|
||||
| 'dirty'
|
||||
| 'message_required'
|
||||
| 'no_upstream'
|
||||
| 'needs_push'
|
||||
| '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
|
||||
hostedReviewCreation?: HostedReviewCreationEligibility | null
|
||||
branchCommitsAhead?: number
|
||||
hasCurrentBranch?: boolean
|
||||
}): CreateReviewIntentEligibility {
|
||||
if (
|
||||
hasUnresolvedConflicts ||
|
||||
!hasCurrentBranch ||
|
||||
!hostedReviewCreation ||
|
||||
hostedReviewCreation.canCreate ||
|
||||
!supportsHostedReviewCreation(hostedReviewCreation.provider)
|
||||
) {
|
||||
return { eligible: false, kind: null }
|
||||
}
|
||||
|
||||
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' }
|
||||
}
|
||||
|
||||
return { eligible: false, kind: null }
|
||||
}
|
||||
|
||||
export function resolveVisibleCreateReviewHeaderAction({
|
||||
createPrHeaderAction
|
||||
}: {
|
||||
createPrHeaderAction: SourceControlPrimaryActionDecision | null
|
||||
}): SourceControlPrimaryActionDecision | null {
|
||||
// Why: keep a stable header anchor; disable Create Review when the branch is
|
||||
// not ready instead of hiding it and shifting toolbar layout.
|
||||
return createPrHeaderAction
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { HostedReviewCreationEligibility } from './hosted-review'
|
||||
import type { GitUpstreamStatus } from './git-status-types'
|
||||
import type { PRState } from './types'
|
||||
|
||||
export type SourceControlPrimaryActionKind =
|
||||
| 'commit'
|
||||
| 'stage'
|
||||
| 'push'
|
||||
| 'pull'
|
||||
| 'sync'
|
||||
| 'publish'
|
||||
| 'create_pr_intent'
|
||||
| 'create_pr'
|
||||
|
||||
export type SourceControlRemoteOpKind =
|
||||
| 'push'
|
||||
| 'force_push'
|
||||
| 'pull'
|
||||
| 'sync'
|
||||
| 'fetch'
|
||||
| 'fast_forward'
|
||||
| 'publish'
|
||||
| 'rebase'
|
||||
|
||||
export type SourceControlPrimaryActionTitleIntent =
|
||||
| 'commit_in_progress'
|
||||
| 'force_push_in_progress'
|
||||
| 'action_in_progress'
|
||||
| 'remote_operation_in_progress'
|
||||
| 'remote_operation_blocks_commit'
|
||||
| 'resolve_conflicts_before_commit'
|
||||
| 'prepare_review'
|
||||
| 'commit_staged_changes'
|
||||
| 'enter_commit_message'
|
||||
| 'stage_all_changes'
|
||||
| 'stage_file_to_commit'
|
||||
| 'checkout_branch_before_publish'
|
||||
| 'checking_review_status'
|
||||
| 'review_already_merged'
|
||||
| 'publish_branch'
|
||||
| 'push_linked_review'
|
||||
| 'linked_review_target_unavailable'
|
||||
| 'force_push_with_lease'
|
||||
| 'sync_counts'
|
||||
| 'pull_count'
|
||||
| 'push_count'
|
||||
| 'create_review'
|
||||
| 'nothing_to_commit_up_to_date'
|
||||
| 'checking_review_creation'
|
||||
|
||||
export type SourceControlPrimaryActionDecision = {
|
||||
kind: SourceControlPrimaryActionKind
|
||||
disabled: boolean
|
||||
labelIntent: SourceControlPrimaryActionKind | 'force_push'
|
||||
titleIntent: SourceControlPrimaryActionTitleIntent
|
||||
count?: number
|
||||
ahead?: number
|
||||
behind?: number
|
||||
upstreamName?: string
|
||||
requiresForceWithLease?: boolean
|
||||
}
|
||||
|
||||
export type SourceControlPrimaryActionDecisionInputs = {
|
||||
stagedCount: number
|
||||
hasUnstagedChanges: boolean
|
||||
hasStageableChanges: boolean
|
||||
hasPartiallyStagedChanges: boolean
|
||||
hasMessage: boolean
|
||||
hasUnresolvedConflicts: boolean
|
||||
isCommitting: boolean
|
||||
isRemoteOperationActive: boolean
|
||||
upstreamStatus: GitUpstreamStatus | undefined
|
||||
prState?: PRState | null
|
||||
isPRStateLoading?: boolean
|
||||
inFlightRemoteOpKind?: SourceControlRemoteOpKind | null
|
||||
hostedReviewCreation?: HostedReviewCreationEligibility | null
|
||||
branchCommitsAhead?: number
|
||||
hasCurrentBranch?: boolean
|
||||
canPushLinkedReviewWithoutUpstream?: boolean
|
||||
isPrIntentInFlight?: boolean
|
||||
isHostedReviewCreationLoading?: boolean
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
resolveSourceControlCommitAreaPrimaryActionDecision,
|
||||
resolveSourceControlPrimaryActionDecision,
|
||||
type SourceControlPrimaryActionDecisionInputs
|
||||
} from './source-control-primary-action-decision'
|
||||
|
||||
function inputs(
|
||||
overrides: Partial<SourceControlPrimaryActionDecisionInputs> = {}
|
||||
): SourceControlPrimaryActionDecisionInputs {
|
||||
return {
|
||||
stagedCount: 0,
|
||||
hasUnstagedChanges: false,
|
||||
hasStageableChanges: false,
|
||||
hasPartiallyStagedChanges: false,
|
||||
hasMessage: false,
|
||||
hasUnresolvedConflicts: false,
|
||||
isCommitting: false,
|
||||
isRemoteOperationActive: false,
|
||||
upstreamStatus: undefined,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('source-control primary action decision', () => {
|
||||
it('returns Stage All for a dirty tree before remote actions', () => {
|
||||
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs({
|
||||
hasUnstagedChanges: true,
|
||||
hasStageableChanges: true,
|
||||
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 4 }
|
||||
})
|
||||
)
|
||||
expect(result.kind).toBe('stage')
|
||||
expect(result.titleIntent).toBe('stage_all_changes')
|
||||
expect(result.disabled).toBe(false)
|
||||
})
|
||||
|
||||
it('returns enabled Commit for staged changes with a message', () => {
|
||||
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs({ stagedCount: 2, hasMessage: true })
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
kind: 'commit',
|
||||
titleIntent: 'commit_staged_changes',
|
||||
disabled: false
|
||||
})
|
||||
})
|
||||
|
||||
it('blocks commits while unresolved conflicts exist', () => {
|
||||
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs({ stagedCount: 1, hasMessage: true, hasUnresolvedConflicts: true })
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
kind: 'commit',
|
||||
titleIntent: 'resolve_conflicts_before_commit',
|
||||
disabled: true
|
||||
})
|
||||
})
|
||||
|
||||
it('returns remote push, pull, and sync decisions for clean tracked branches', () => {
|
||||
expect(
|
||||
resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs({ upstreamStatus: { hasUpstream: true, ahead: 2, behind: 0 } })
|
||||
)
|
||||
).toMatchObject({ kind: 'push', titleIntent: 'push_count', count: 2 })
|
||||
expect(
|
||||
resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs({ upstreamStatus: { hasUpstream: true, ahead: 0, behind: 3 } })
|
||||
)
|
||||
).toMatchObject({ kind: 'pull', titleIntent: 'pull_count', count: 3 })
|
||||
expect(
|
||||
resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs({ upstreamStatus: { hasUpstream: true, ahead: 2, behind: 3 } })
|
||||
)
|
||||
).toMatchObject({ kind: 'sync', titleIntent: 'sync_counts', ahead: 2, behind: 3 })
|
||||
})
|
||||
|
||||
it('returns Publish Branch for clean unpublished branches with a current branch', () => {
|
||||
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs({
|
||||
upstreamStatus: { hasUpstream: false, ahead: 0, behind: 0 },
|
||||
hasCurrentBranch: true
|
||||
})
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
kind: 'publish',
|
||||
titleIntent: 'publish_branch',
|
||||
disabled: false
|
||||
})
|
||||
})
|
||||
|
||||
it('blocks unpublished branch publishing when HEAD is detached', () => {
|
||||
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs({
|
||||
upstreamStatus: { hasUpstream: false, ahead: 0, behind: 0 },
|
||||
hasCurrentBranch: false
|
||||
})
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
kind: 'commit',
|
||||
titleIntent: 'checkout_branch_before_publish',
|
||||
disabled: true
|
||||
})
|
||||
})
|
||||
|
||||
it('mirrors in-flight remote operation semantics', () => {
|
||||
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs({
|
||||
isRemoteOperationActive: true,
|
||||
inFlightRemoteOpKind: 'pull',
|
||||
upstreamStatus: { hasUpstream: true, ahead: 3, behind: 0 }
|
||||
})
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
kind: 'pull',
|
||||
labelIntent: 'pull',
|
||||
titleIntent: 'action_in_progress',
|
||||
disabled: true
|
||||
})
|
||||
})
|
||||
|
||||
it('marks patch-equivalent diverged branches as force-push-with-lease decisions', () => {
|
||||
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs({
|
||||
branchCommitsAhead: 4,
|
||||
upstreamStatus: {
|
||||
hasUpstream: true,
|
||||
upstreamName: 'origin/feature',
|
||||
ahead: 14,
|
||||
behind: 3,
|
||||
behindCommitsArePatchEquivalent: true
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
kind: 'push',
|
||||
labelIntent: 'force_push',
|
||||
titleIntent: 'force_push_with_lease',
|
||||
requiresForceWithLease: true,
|
||||
count: 4,
|
||||
upstreamName: 'origin/feature'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps review creation out of commit-area decisions', () => {
|
||||
const input = inputs({
|
||||
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
|
||||
hostedReviewCreation: {
|
||||
provider: 'gitlab',
|
||||
review: null,
|
||||
canCreate: true,
|
||||
blockedReason: null,
|
||||
nextAction: null
|
||||
}
|
||||
})
|
||||
expect(resolveSourceControlPrimaryActionDecision(input).kind).toBe('create_pr')
|
||||
expect(resolveSourceControlCommitAreaPrimaryActionDecision(input).kind).toBe('commit')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,252 @@
|
||||
import { shouldForcePushWithLeaseForUpstream } from './git-upstream-status'
|
||||
import { resolveCreateReviewIntentEligibility } from './source-control-create-review-intent'
|
||||
import { resolveSourceControlPrimaryActionDuringRemoteOp } from './source-control-primary-action-in-flight'
|
||||
import type {
|
||||
SourceControlPrimaryActionDecision,
|
||||
SourceControlPrimaryActionDecisionInputs
|
||||
} from './source-control-primary-action-decision-types'
|
||||
import { resolveUnpublishedSourceControlPrimaryAction } from './source-control-primary-unpublished-action'
|
||||
|
||||
export type {
|
||||
SourceControlPrimaryActionKind,
|
||||
SourceControlRemoteOpKind,
|
||||
SourceControlPrimaryActionDecision,
|
||||
SourceControlPrimaryActionDecisionInputs
|
||||
} from './source-control-primary-action-decision-types'
|
||||
|
||||
export function resolveSourceControlPrimaryActionDecision(
|
||||
inputs: SourceControlPrimaryActionDecisionInputs
|
||||
): SourceControlPrimaryActionDecision {
|
||||
const {
|
||||
stagedCount,
|
||||
hasUnstagedChanges,
|
||||
hasStageableChanges,
|
||||
hasMessage,
|
||||
hasUnresolvedConflicts,
|
||||
isCommitting,
|
||||
isRemoteOperationActive,
|
||||
upstreamStatus,
|
||||
prState,
|
||||
isPRStateLoading,
|
||||
hostedReviewCreation,
|
||||
branchCommitsAhead,
|
||||
hasCurrentBranch = true,
|
||||
canPushLinkedReviewWithoutUpstream = false,
|
||||
isPrIntentInFlight = false
|
||||
} = inputs
|
||||
|
||||
if (isPrIntentInFlight) {
|
||||
return {
|
||||
kind: 'create_pr_intent',
|
||||
labelIntent: 'create_pr',
|
||||
titleIntent: 'prepare_review',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
if (isCommitting) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
labelIntent: 'commit',
|
||||
titleIntent: 'commit_in_progress',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
if (isRemoteOperationActive) {
|
||||
return resolveSourceControlPrimaryActionDuringRemoteOp(
|
||||
inputs,
|
||||
resolveSourceControlPrimaryActionDecision
|
||||
)
|
||||
}
|
||||
|
||||
if (hasUnresolvedConflicts) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
labelIntent: 'commit',
|
||||
titleIntent: 'resolve_conflicts_before_commit',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
const createPrIntent = resolveCreatePrIntentDecision(inputs)
|
||||
if (createPrIntent) {
|
||||
return createPrIntent
|
||||
}
|
||||
|
||||
const hasStaged = stagedCount > 0
|
||||
const hasOpenHostedReview = prState === 'open' || prState === 'draft'
|
||||
|
||||
if (hasStaged && hasMessage) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
labelIntent: 'commit',
|
||||
titleIntent: 'commit_staged_changes',
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStaged && !hasMessage) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
labelIntent: 'commit',
|
||||
titleIntent: 'enter_commit_message',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasStaged && hasStageableChanges) {
|
||||
return {
|
||||
kind: 'stage',
|
||||
labelIntent: 'stage',
|
||||
titleIntent: 'stage_all_changes',
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
if (!upstreamStatus) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
labelIntent: 'commit',
|
||||
titleIntent: 'stage_file_to_commit',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
if (!upstreamStatus.hasUpstream) {
|
||||
const unpublishedAction = resolveUnpublishedSourceControlPrimaryAction({
|
||||
hasCurrentBranch,
|
||||
isPRStateLoading,
|
||||
prState
|
||||
})
|
||||
|
||||
if (unpublishedAction.kind === 'publish') {
|
||||
const linkedReviewAction = resolveLinkedReviewSourceControlPrimaryAction({
|
||||
hasOpenHostedReview,
|
||||
canPushLinkedReviewWithoutUpstream
|
||||
})
|
||||
if (linkedReviewAction) {
|
||||
return linkedReviewAction
|
||||
}
|
||||
}
|
||||
|
||||
return unpublishedAction
|
||||
}
|
||||
|
||||
if (upstreamStatus.ahead > 0 && upstreamStatus.behind > 0) {
|
||||
if (shouldForcePushWithLeaseForUpstream(upstreamStatus)) {
|
||||
return {
|
||||
kind: 'push',
|
||||
labelIntent: 'force_push',
|
||||
titleIntent: 'force_push_with_lease',
|
||||
disabled: false,
|
||||
count: branchCommitsAhead,
|
||||
upstreamName: upstreamStatus.upstreamName,
|
||||
requiresForceWithLease: true
|
||||
}
|
||||
}
|
||||
return {
|
||||
kind: 'sync',
|
||||
labelIntent: 'sync',
|
||||
titleIntent: 'sync_counts',
|
||||
disabled: false,
|
||||
ahead: upstreamStatus.ahead,
|
||||
behind: upstreamStatus.behind
|
||||
}
|
||||
}
|
||||
|
||||
if (upstreamStatus.behind > 0) {
|
||||
return {
|
||||
kind: 'pull',
|
||||
labelIntent: 'pull',
|
||||
titleIntent: 'pull_count',
|
||||
disabled: false,
|
||||
count: upstreamStatus.behind
|
||||
}
|
||||
}
|
||||
|
||||
if (upstreamStatus.ahead > 0) {
|
||||
return {
|
||||
kind: 'push',
|
||||
labelIntent: 'push',
|
||||
titleIntent: 'push_count',
|
||||
disabled: false,
|
||||
count: upstreamStatus.ahead
|
||||
}
|
||||
}
|
||||
|
||||
if (hostedReviewCreation?.canCreate) {
|
||||
return {
|
||||
kind: 'create_pr',
|
||||
labelIntent: 'create_pr',
|
||||
titleIntent: 'create_review',
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'commit',
|
||||
labelIntent: 'commit',
|
||||
titleIntent: hasUnstagedChanges ? 'stage_file_to_commit' : 'nothing_to_commit_up_to_date',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveSourceControlCommitAreaPrimaryActionDecision(
|
||||
inputs: SourceControlPrimaryActionDecisionInputs
|
||||
): SourceControlPrimaryActionDecision {
|
||||
// Why: review creation is additive chrome. Commit/mobile bottom areas keep
|
||||
// the local/remote action they would have without review eligibility.
|
||||
return resolveSourceControlPrimaryActionDecision({
|
||||
...inputs,
|
||||
hostedReviewCreation: null,
|
||||
isPrIntentInFlight: false
|
||||
})
|
||||
}
|
||||
|
||||
function resolveCreatePrIntentDecision(
|
||||
inputs: SourceControlPrimaryActionDecisionInputs
|
||||
): SourceControlPrimaryActionDecision | null {
|
||||
const createPrIntent = resolveCreateReviewIntentEligibility({
|
||||
stagedCount: inputs.stagedCount,
|
||||
hasStageableChanges: inputs.hasStageableChanges,
|
||||
hasMessage: inputs.hasMessage,
|
||||
hasUnresolvedConflicts: inputs.hasUnresolvedConflicts,
|
||||
upstreamStatus: inputs.upstreamStatus,
|
||||
hostedReviewCreation: inputs.hostedReviewCreation,
|
||||
branchCommitsAhead: inputs.branchCommitsAhead,
|
||||
hasCurrentBranch: inputs.hasCurrentBranch
|
||||
})
|
||||
if (!createPrIntent.eligible) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
kind: 'create_pr_intent',
|
||||
labelIntent: 'create_pr',
|
||||
titleIntent: 'prepare_review',
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
function resolveLinkedReviewSourceControlPrimaryAction(args: {
|
||||
hasOpenHostedReview: boolean
|
||||
canPushLinkedReviewWithoutUpstream: boolean
|
||||
}): SourceControlPrimaryActionDecision | null {
|
||||
if (!args.hasOpenHostedReview) {
|
||||
return null
|
||||
}
|
||||
if (args.canPushLinkedReviewWithoutUpstream) {
|
||||
return {
|
||||
kind: 'push',
|
||||
labelIntent: 'push',
|
||||
titleIntent: 'push_linked_review',
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
return {
|
||||
kind: 'commit',
|
||||
labelIntent: 'commit',
|
||||
titleIntent: 'linked_review_target_unavailable',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type {
|
||||
SourceControlPrimaryActionDecision,
|
||||
SourceControlPrimaryActionDecisionInputs
|
||||
} from './source-control-primary-action-decision-types'
|
||||
|
||||
export function resolveSourceControlPrimaryActionDuringRemoteOp(
|
||||
inputs: SourceControlPrimaryActionDecisionInputs,
|
||||
resolveWithoutRemoteOp: (
|
||||
inputs: SourceControlPrimaryActionDecisionInputs
|
||||
) => SourceControlPrimaryActionDecision
|
||||
): SourceControlPrimaryActionDecision {
|
||||
const { inFlightRemoteOpKind, hasUnresolvedConflicts } = inputs
|
||||
const candidate = resolveWithoutRemoteOp({ ...inputs, isRemoteOperationActive: false })
|
||||
const inFlightIsPrimaryKind =
|
||||
inFlightRemoteOpKind === 'push' ||
|
||||
inFlightRemoteOpKind === 'pull' ||
|
||||
inFlightRemoteOpKind === 'sync' ||
|
||||
inFlightRemoteOpKind === 'publish'
|
||||
|
||||
if (inFlightRemoteOpKind === 'force_push') {
|
||||
return {
|
||||
kind: 'push',
|
||||
labelIntent: 'force_push',
|
||||
titleIntent: 'force_push_in_progress',
|
||||
disabled: true,
|
||||
requiresForceWithLease: true
|
||||
}
|
||||
}
|
||||
|
||||
if (inFlightIsPrimaryKind && candidate.kind !== inFlightRemoteOpKind) {
|
||||
return {
|
||||
kind: inFlightRemoteOpKind,
|
||||
labelIntent: inFlightRemoteOpKind,
|
||||
titleIntent: 'action_in_progress',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
const titleIntent = hasUnresolvedConflicts
|
||||
? 'resolve_conflicts_before_commit'
|
||||
: candidate.kind === 'commit'
|
||||
? 'remote_operation_blocks_commit'
|
||||
: 'remote_operation_in_progress'
|
||||
|
||||
return {
|
||||
...candidate,
|
||||
titleIntent,
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type {
|
||||
SourceControlPrimaryActionDecision,
|
||||
SourceControlPrimaryActionDecisionInputs
|
||||
} from './source-control-primary-action-decision-types'
|
||||
|
||||
export function resolveUnpublishedSourceControlPrimaryAction({
|
||||
hasCurrentBranch,
|
||||
isPRStateLoading,
|
||||
prState
|
||||
}: Pick<
|
||||
SourceControlPrimaryActionDecisionInputs,
|
||||
'hasCurrentBranch' | 'isPRStateLoading' | 'prState'
|
||||
>): SourceControlPrimaryActionDecision {
|
||||
if (!hasCurrentBranch) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
labelIntent: 'commit',
|
||||
titleIntent: 'checkout_branch_before_publish',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
if (isPRStateLoading) {
|
||||
return {
|
||||
kind: 'commit',
|
||||
labelIntent: 'commit',
|
||||
titleIntent: 'checking_review_status',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
if (prState === 'merged') {
|
||||
return {
|
||||
kind: 'commit',
|
||||
labelIntent: 'commit',
|
||||
titleIntent: 'review_already_merged',
|
||||
disabled: true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'publish',
|
||||
labelIntent: 'publish',
|
||||
titleIntent: 'publish_branch',
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user