Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/web/src/components/GitActionsControl.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
resolveDefaultBranchActionDialogCopy,
resolveLiveThreadBranchUpdate,
resolveQuickAction,
getMenuActionDisabledReason,
} from "./GitActionsControl.logic";

function status(overrides: Partial<VcsStatusResult> = {}): VcsStatusResult {
Expand All @@ -32,6 +33,12 @@ function status(overrides: Partial<VcsStatusResult> = {}): 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(
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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(
{
Expand Down
59 changes: 59 additions & 0 deletions apps/web/src/components/GitActionsControl.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,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;
Expand Down
63 changes: 1 addition & 62 deletions apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
resolveGitActionProgressPresentation,
resolveGitActionResultToastTiming,
resolveLiveThreadBranchUpdate,
getMenuActionDisabledReason,
resolveThreadBranchMetadataPatch,
resolveQuickAction,
resolveThreadBranchUpdate,
Expand Down Expand Up @@ -212,68 +213,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.";
Expand Down
Loading