diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index d0e243cda25c4..f1b000676f307 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -10,6 +10,7 @@ import { resolveDefaultBranchActionDialogCopy, resolveLiveThreadBranchUpdate, resolveQuickAction, + getMenuActionDisabledReason, } from "./GitActionsControl.logic"; function status(overrides: Partial = {}): VcsStatusResult { @@ -32,6 +33,12 @@ function status(overrides: Partial = {}): VcsStatusResult { }; } +function pushDisabledReason(gitStatus: VcsStatusResult): string | null { + const item = buildMenuItems(gitStatus, false).find((menuItem) => menuItem.id === "push"); + assert.isDefined(item); + return getMenuActionDisabledReason({ item, gitStatus, isBusy: false, hasPrimaryRemote: true }); +} + describe("when: ref is clean, ahead, and has an open PR", () => { it("resolveQuickAction prefers push", () => { const quick = resolveQuickAction( @@ -77,6 +84,7 @@ describe("when: ref is clean, up to date, and has no open PR", () => { buildMenuItems(dirtyFeature, false).find((item) => item.id === "pr")?.disabled, false, ); + assert.equal(pushDisabledReason(dirtyFeature), "No local commits to push."); }); it("offers the actions menu only when something in it is usable", () => { @@ -129,6 +137,10 @@ describe("when: local changes are behind upstream", () => { action: "commit", } as const; assert.deepEqual(resolveQuickAction(behind, false), commit); + assert.equal( + pushDisabledReason({ ...behind, aheadCount: 1 }), + "Branch is behind upstream. Pull/rebase before pushing.", + ); assert.deepEqual( resolveQuickAction( { diff --git a/apps/web/src/components/GitActionsControl.logic.ts b/apps/web/src/components/GitActionsControl.logic.ts index 399e013068364..19994bed1863b 100644 --- a/apps/web/src/components/GitActionsControl.logic.ts +++ b/apps/web/src/components/GitActionsControl.logic.ts @@ -175,6 +175,65 @@ export function buildMenuItems( ]; } +export function getMenuActionDisabledReason({ + item, + gitStatus, + isBusy, + hasPrimaryRemote, +}: { + item: GitActionMenuItem; + gitStatus: VcsStatusResult | null; + isBusy: boolean; + hasPrimaryRemote: boolean; +}): string | null { + if (!item.disabled) return null; + if (isBusy) return "Git action in progress."; + if (!gitStatus) return "Git status is unavailable."; + + const hasBranch = gitStatus.refName !== null; + const hasChanges = gitStatus.hasWorkingTreeChanges; + const isAhead = gitStatus.aheadCount > 0; + const isBehind = gitStatus.behindCount > 0; + const terminology = changeRequestTerminologyFor(gitStatus); + + if (item.id === "commit") { + if (!hasChanges) { + return "Worktree is clean. Make changes before committing."; + } + return "Commit is currently unavailable."; + } + + if (item.id === "push") { + if (!hasBranch) { + return "Detached HEAD: check out a branch before pushing."; + } + if (isBehind) { + return "Branch is behind upstream. Pull/rebase before pushing."; + } + if (!gitStatus.hasUpstream && !hasPrimaryRemote) { + return 'Add an "origin" remote before pushing.'; + } + if (!isAhead) { + return "No local commits to push."; + } + return "Push is currently unavailable."; + } + + if (!hasBranch) { + return `Detached HEAD: check out a branch before creating a ${terminology.singular}.`; + } + if (!gitStatus.hasUpstream && !hasPrimaryRemote) { + return `Add an "origin" remote before creating a ${terminology.singular}.`; + } + if ((gitStatus.aheadOfDefaultCount ?? gitStatus.aheadCount) <= 0) { + return `No local commits to include in a ${terminology.singular}.`; + } + if (isBehind) { + return `Branch is behind upstream. Pull/rebase before creating a ${terminology.singular}.`; + } + return `Create ${terminology.singular} is currently unavailable.`; +} + export function hasUsableGitMenuContent(input: { readonly menuItems: readonly GitActionMenuItem[]; readonly canWriteSourceControl: boolean; diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 3fe8374d0e12f..6dc7647951a4c 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -59,6 +59,7 @@ import { resolveGitActionProgressPresentation, resolveGitActionResultToastTiming, resolveLiveThreadBranchUpdate, + getMenuActionDisabledReason, resolveThreadBranchMetadataPatch, resolveQuickAction, resolveThreadBranchUpdate, @@ -228,68 +229,6 @@ function getPublishProviderReadiness(input: { return { ready: true, hint: null }; } -function getMenuActionDisabledReason({ - item, - gitStatus, - isBusy, - hasPrimaryRemote, -}: { - item: GitActionMenuItem; - gitStatus: VcsStatusResult | null; - isBusy: boolean; - hasPrimaryRemote: boolean; -}): string | null { - if (!item.disabled) return null; - if (isBusy) return "Git action in progress."; - if (!gitStatus) return "Git status is unavailable."; - - const hasBranch = gitStatus.refName !== null; - const hasChanges = gitStatus.hasWorkingTreeChanges; - const isAhead = gitStatus.aheadCount > 0; - const isBehind = gitStatus.behindCount > 0; - const terminology = getSourceControlPresentation(gitStatus.sourceControlProvider).terminology; - - if (item.id === "commit") { - if (!hasChanges) { - return "Worktree is clean. Make changes before committing."; - } - return "Commit is currently unavailable."; - } - - if (item.id === "push") { - if (!hasBranch) { - return "Detached HEAD: check out a branch before pushing."; - } - if (hasChanges) { - return "Commit or stash local changes before pushing."; - } - if (isBehind) { - return "Branch is behind upstream. Pull/rebase before pushing."; - } - if (!gitStatus.hasUpstream && !hasPrimaryRemote) { - return 'Add an "origin" remote before pushing.'; - } - if (!isAhead) { - return "No local commits to push."; - } - return "Push is currently unavailable."; - } - - if (!hasBranch) { - return `Detached HEAD: check out a branch before creating a ${terminology.singular}.`; - } - if (!gitStatus.hasUpstream && !hasPrimaryRemote) { - return `Add an "origin" remote before creating a ${terminology.singular}.`; - } - if ((gitStatus.aheadOfDefaultCount ?? gitStatus.aheadCount) <= 0) { - return `No local commits to include in a ${terminology.singular}.`; - } - if (isBehind) { - return `Branch is behind upstream. Pull/rebase before creating a ${terminology.singular}.`; - } - return `Create ${terminology.singular} is currently unavailable.`; -} - const COMMIT_DIALOG_TITLE = "Commit changes"; const COMMIT_DIALOG_DESCRIPTION = "Review and confirm your commit. Leave the message blank to auto-generate one.";