Stabilize git action menu
This commit is contained in:
@@ -1,62 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import { shouldShowMergeFromBaseAction } from "./git-action-visibility";
|
|
||||||
|
|
||||||
describe("git-action-visibility", () => {
|
|
||||||
describe("shouldShowMergeFromBaseAction", () => {
|
|
||||||
it("shows on non-base branches", () => {
|
|
||||||
expect(
|
|
||||||
shouldShowMergeFromBaseAction({
|
|
||||||
isOnBaseBranch: false,
|
|
||||||
hasRemote: false,
|
|
||||||
aheadOfOrigin: 0,
|
|
||||||
behindOfOrigin: 0,
|
|
||||||
})
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hides on base branch when no remote exists", () => {
|
|
||||||
expect(
|
|
||||||
shouldShowMergeFromBaseAction({
|
|
||||||
isOnBaseBranch: true,
|
|
||||||
hasRemote: false,
|
|
||||||
aheadOfOrigin: 0,
|
|
||||||
behindOfOrigin: 0,
|
|
||||||
})
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hides on base branch when local is in sync with origin", () => {
|
|
||||||
expect(
|
|
||||||
shouldShowMergeFromBaseAction({
|
|
||||||
isOnBaseBranch: true,
|
|
||||||
hasRemote: true,
|
|
||||||
aheadOfOrigin: 0,
|
|
||||||
behindOfOrigin: 0,
|
|
||||||
})
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows on base branch when ahead of origin", () => {
|
|
||||||
expect(
|
|
||||||
shouldShowMergeFromBaseAction({
|
|
||||||
isOnBaseBranch: true,
|
|
||||||
hasRemote: true,
|
|
||||||
aheadOfOrigin: 1,
|
|
||||||
behindOfOrigin: 0,
|
|
||||||
})
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows on base branch when behind origin", () => {
|
|
||||||
expect(
|
|
||||||
shouldShowMergeFromBaseAction({
|
|
||||||
isOnBaseBranch: true,
|
|
||||||
hasRemote: true,
|
|
||||||
aheadOfOrigin: 0,
|
|
||||||
behindOfOrigin: 2,
|
|
||||||
})
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export function shouldShowMergeFromBaseAction(input: {
|
|
||||||
isOnBaseBranch: boolean;
|
|
||||||
hasRemote: boolean;
|
|
||||||
aheadOfOrigin: number;
|
|
||||||
behindOfOrigin: number;
|
|
||||||
}): boolean {
|
|
||||||
if (!input.isOnBaseBranch) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!input.hasRemote) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return input.aheadOfOrigin > 0 || input.behindOfOrigin > 0;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { buildGitActions, type BuildGitActionsInput } from "./git-actions-policy";
|
||||||
|
|
||||||
|
function createInput(
|
||||||
|
overrides: Partial<BuildGitActionsInput> = {}
|
||||||
|
): BuildGitActionsInput {
|
||||||
|
return {
|
||||||
|
isGit: true,
|
||||||
|
githubFeaturesEnabled: true,
|
||||||
|
hasPullRequest: false,
|
||||||
|
pullRequestUrl: null,
|
||||||
|
hasRemote: false,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
isOnBaseBranch: true,
|
||||||
|
hasUncommittedChanges: false,
|
||||||
|
baseRefAvailable: true,
|
||||||
|
baseRefLabel: "main",
|
||||||
|
aheadCount: 0,
|
||||||
|
aheadOfOrigin: 0,
|
||||||
|
behindOfOrigin: 0,
|
||||||
|
shouldPromoteArchive: false,
|
||||||
|
shipDefault: "merge",
|
||||||
|
runtime: {
|
||||||
|
commit: {
|
||||||
|
disabled: false,
|
||||||
|
status: "idle",
|
||||||
|
handler: () => undefined,
|
||||||
|
},
|
||||||
|
push: {
|
||||||
|
disabled: false,
|
||||||
|
status: "idle",
|
||||||
|
handler: () => undefined,
|
||||||
|
},
|
||||||
|
pr: {
|
||||||
|
disabled: false,
|
||||||
|
status: "idle",
|
||||||
|
handler: () => undefined,
|
||||||
|
},
|
||||||
|
"merge-branch": {
|
||||||
|
disabled: false,
|
||||||
|
status: "idle",
|
||||||
|
handler: () => undefined,
|
||||||
|
},
|
||||||
|
"merge-from-base": {
|
||||||
|
disabled: false,
|
||||||
|
status: "idle",
|
||||||
|
handler: () => undefined,
|
||||||
|
},
|
||||||
|
"archive-worktree": {
|
||||||
|
disabled: false,
|
||||||
|
status: "idle",
|
||||||
|
handler: () => undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("git-actions-policy", () => {
|
||||||
|
it("keeps the secondary menu order stable while the primary action changes", () => {
|
||||||
|
const noPrActions = buildGitActions(createInput());
|
||||||
|
const withPrActions = buildGitActions(
|
||||||
|
createInput({
|
||||||
|
hasRemote: true,
|
||||||
|
hasPullRequest: true,
|
||||||
|
pullRequestUrl: "https://example.com/pr/123",
|
||||||
|
aheadCount: 3,
|
||||||
|
aheadOfOrigin: 2,
|
||||||
|
shipDefault: "pr",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(noPrActions.primary).toBeNull();
|
||||||
|
expect(withPrActions.primary?.id).toBe("push");
|
||||||
|
expect(noPrActions.secondary.map((action) => action.id)).toEqual([
|
||||||
|
"merge-branch",
|
||||||
|
"pr",
|
||||||
|
"merge-from-base",
|
||||||
|
"push",
|
||||||
|
]);
|
||||||
|
expect(withPrActions.secondary.map((action) => action.id)).toEqual([
|
||||||
|
"merge-branch",
|
||||||
|
"pr",
|
||||||
|
"merge-from-base",
|
||||||
|
"push",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables hidden-before actions with explanations instead", () => {
|
||||||
|
const actions = buildGitActions(createInput());
|
||||||
|
const actionById = new Map(actions.secondary.map((action) => [action.id, action]));
|
||||||
|
|
||||||
|
expect(actionById.get("push")).toMatchObject({
|
||||||
|
disabled: true,
|
||||||
|
description: "No remote configured",
|
||||||
|
});
|
||||||
|
expect(actionById.get("pr")).toMatchObject({
|
||||||
|
label: "Create PR",
|
||||||
|
disabled: true,
|
||||||
|
description: "Branch has no commits ahead of main",
|
||||||
|
});
|
||||||
|
expect(actionById.get("merge-branch")).toMatchObject({
|
||||||
|
disabled: true,
|
||||||
|
description: "No commits to merge into main",
|
||||||
|
});
|
||||||
|
expect(actionById.get("merge-from-base")).toMatchObject({
|
||||||
|
disabled: true,
|
||||||
|
description: "No remote configured",
|
||||||
|
});
|
||||||
|
expect(actionById.has("archive-worktree")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the current primary action visible in the menu", () => {
|
||||||
|
const actions = buildGitActions(
|
||||||
|
createInput({
|
||||||
|
hasRemote: true,
|
||||||
|
hasPullRequest: true,
|
||||||
|
pullRequestUrl: "https://example.com/pr/456",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(actions.primary?.id).toBe("pr");
|
||||||
|
expect(actions.secondary.some((action) => action.id === "pr" && action.label === "View PR")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables sync on the base branch when already up to date", () => {
|
||||||
|
const actions = buildGitActions(
|
||||||
|
createInput({
|
||||||
|
hasRemote: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const syncAction = actions.secondary.find((action) => action.id === "merge-from-base");
|
||||||
|
|
||||||
|
expect(syncAction).toMatchObject({
|
||||||
|
label: "Sync",
|
||||||
|
disabled: true,
|
||||||
|
description: "Already up to date",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only shows archive worktree for paseo worktrees", () => {
|
||||||
|
const hidden = buildGitActions(createInput());
|
||||||
|
const shown = buildGitActions(createInput({ isPaseoOwnedWorktree: true }));
|
||||||
|
|
||||||
|
expect(hidden.secondary.some((action) => action.id === "archive-worktree")).toBe(false);
|
||||||
|
expect(shown.secondary.some((action) => action.id === "archive-worktree")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import type { ReactElement } from "react";
|
||||||
|
|
||||||
|
import type { ActionStatus } from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
export type GitActionId =
|
||||||
|
| "commit"
|
||||||
|
| "push"
|
||||||
|
| "pr"
|
||||||
|
| "merge-branch"
|
||||||
|
| "merge-from-base"
|
||||||
|
| "archive-worktree";
|
||||||
|
|
||||||
|
export interface GitAction {
|
||||||
|
id: GitActionId;
|
||||||
|
label: string;
|
||||||
|
pendingLabel: string;
|
||||||
|
successLabel: string;
|
||||||
|
disabled: boolean;
|
||||||
|
status: ActionStatus;
|
||||||
|
description?: string;
|
||||||
|
icon?: ReactElement;
|
||||||
|
handler: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GitActions {
|
||||||
|
primary: GitAction | null;
|
||||||
|
secondary: GitAction[];
|
||||||
|
menu: GitAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GitActionRuntimeState {
|
||||||
|
disabled: boolean;
|
||||||
|
status: ActionStatus;
|
||||||
|
icon?: ReactElement;
|
||||||
|
handler: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildGitActionsInput {
|
||||||
|
isGit: boolean;
|
||||||
|
githubFeaturesEnabled: boolean;
|
||||||
|
hasPullRequest: boolean;
|
||||||
|
pullRequestUrl: string | null;
|
||||||
|
hasRemote: boolean;
|
||||||
|
isPaseoOwnedWorktree: boolean;
|
||||||
|
isOnBaseBranch: boolean;
|
||||||
|
hasUncommittedChanges: boolean;
|
||||||
|
baseRefAvailable: boolean;
|
||||||
|
baseRefLabel: string;
|
||||||
|
aheadCount: number;
|
||||||
|
aheadOfOrigin: number;
|
||||||
|
behindOfOrigin: number;
|
||||||
|
shouldPromoteArchive: boolean;
|
||||||
|
shipDefault: "merge" | "pr";
|
||||||
|
runtime: Record<GitActionId, GitActionRuntimeState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECONDARY_ACTION_IDS: GitActionId[] = [
|
||||||
|
"merge-branch",
|
||||||
|
"pr",
|
||||||
|
"merge-from-base",
|
||||||
|
"push",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function buildGitActions(input: BuildGitActionsInput): GitActions {
|
||||||
|
if (!input.isGit) {
|
||||||
|
return { primary: null, secondary: [], menu: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const allActions = new Map<GitActionId, GitAction>();
|
||||||
|
|
||||||
|
allActions.set("commit", {
|
||||||
|
id: "commit",
|
||||||
|
label: "Commit",
|
||||||
|
pendingLabel: "Committing...",
|
||||||
|
successLabel: "Committed",
|
||||||
|
disabled: input.runtime.commit.disabled,
|
||||||
|
status: input.runtime.commit.status,
|
||||||
|
icon: input.runtime.commit.icon,
|
||||||
|
handler: input.runtime.commit.handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
allActions.set("push", {
|
||||||
|
id: "push",
|
||||||
|
label: "Push",
|
||||||
|
pendingLabel: "Pushing...",
|
||||||
|
successLabel: "Pushed",
|
||||||
|
disabled: input.runtime.push.disabled || !input.hasRemote,
|
||||||
|
status: input.runtime.push.status,
|
||||||
|
description: input.hasRemote ? undefined : "No remote configured",
|
||||||
|
icon: input.runtime.push.icon,
|
||||||
|
handler: input.runtime.push.handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
allActions.set("pr", buildPrAction(input));
|
||||||
|
|
||||||
|
allActions.set("merge-branch", {
|
||||||
|
id: "merge-branch",
|
||||||
|
label: `Merge into ${input.baseRefLabel}`,
|
||||||
|
pendingLabel: "Merging...",
|
||||||
|
successLabel: "Merged",
|
||||||
|
disabled:
|
||||||
|
input.runtime["merge-branch"].disabled ||
|
||||||
|
!input.baseRefAvailable ||
|
||||||
|
input.hasUncommittedChanges ||
|
||||||
|
input.aheadCount === 0,
|
||||||
|
status: input.runtime["merge-branch"].status,
|
||||||
|
description: getMergeBranchDescription(input),
|
||||||
|
icon: input.runtime["merge-branch"].icon,
|
||||||
|
handler: input.runtime["merge-branch"].handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
allActions.set("merge-from-base", {
|
||||||
|
id: "merge-from-base",
|
||||||
|
label: input.isOnBaseBranch ? "Sync" : `Update from ${input.baseRefLabel}`,
|
||||||
|
pendingLabel: "Updating...",
|
||||||
|
successLabel: "Updated",
|
||||||
|
disabled: input.runtime["merge-from-base"].disabled || !canMergeFromBase(input),
|
||||||
|
status: input.runtime["merge-from-base"].status,
|
||||||
|
description: getMergeFromBaseDescription(input),
|
||||||
|
icon: input.runtime["merge-from-base"].icon,
|
||||||
|
handler: input.runtime["merge-from-base"].handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
allActions.set("archive-worktree", {
|
||||||
|
id: "archive-worktree",
|
||||||
|
label: "Archive worktree",
|
||||||
|
pendingLabel: "Archiving...",
|
||||||
|
successLabel: "Archived",
|
||||||
|
disabled: input.runtime["archive-worktree"].disabled || !input.isPaseoOwnedWorktree,
|
||||||
|
status: input.runtime["archive-worktree"].status,
|
||||||
|
description: input.isPaseoOwnedWorktree ? undefined : "Only available for Paseo worktrees",
|
||||||
|
icon: input.runtime["archive-worktree"].icon,
|
||||||
|
handler: input.runtime["archive-worktree"].handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
const primaryActionId = getPrimaryActionId(input);
|
||||||
|
const primary = primaryActionId ? allActions.get(primaryActionId) ?? null : null;
|
||||||
|
const secondary = SECONDARY_ACTION_IDS.map((id) => allActions.get(id)!);
|
||||||
|
if (input.isPaseoOwnedWorktree) {
|
||||||
|
secondary.push(allActions.get("archive-worktree")!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { primary, secondary, menu: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrimaryActionId(input: BuildGitActionsInput): GitActionId | null {
|
||||||
|
if (input.shouldPromoteArchive && input.isPaseoOwnedWorktree) {
|
||||||
|
return "archive-worktree";
|
||||||
|
}
|
||||||
|
if (input.hasUncommittedChanges) {
|
||||||
|
return "commit";
|
||||||
|
}
|
||||||
|
if (input.aheadOfOrigin > 0 && input.hasRemote) {
|
||||||
|
return "push";
|
||||||
|
}
|
||||||
|
if (input.githubFeaturesEnabled && input.hasPullRequest && input.pullRequestUrl) {
|
||||||
|
return "pr";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
input.isOnBaseBranch &&
|
||||||
|
input.hasRemote &&
|
||||||
|
(input.aheadOfOrigin > 0 || input.behindOfOrigin > 0)
|
||||||
|
) {
|
||||||
|
return "merge-from-base";
|
||||||
|
}
|
||||||
|
if (input.aheadCount > 0) {
|
||||||
|
return input.shipDefault === "merge" ? "merge-branch" : "pr";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPrAction(input: BuildGitActionsInput): GitAction {
|
||||||
|
if (input.hasPullRequest && input.pullRequestUrl) {
|
||||||
|
return {
|
||||||
|
id: "pr",
|
||||||
|
label: "View PR",
|
||||||
|
pendingLabel: "View PR",
|
||||||
|
successLabel: "View PR",
|
||||||
|
disabled: input.runtime.pr.disabled || !input.githubFeaturesEnabled,
|
||||||
|
status: input.runtime.pr.status,
|
||||||
|
description: input.githubFeaturesEnabled ? undefined : "GitHub features unavailable",
|
||||||
|
icon: input.runtime.pr.icon,
|
||||||
|
handler: input.runtime.pr.handler,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: "pr",
|
||||||
|
label: "Create PR",
|
||||||
|
pendingLabel: "Creating PR...",
|
||||||
|
successLabel: "PR Created",
|
||||||
|
disabled:
|
||||||
|
input.runtime.pr.disabled ||
|
||||||
|
!input.githubFeaturesEnabled ||
|
||||||
|
input.aheadCount === 0,
|
||||||
|
status: input.runtime.pr.status,
|
||||||
|
description: getCreatePrDescription(input),
|
||||||
|
icon: input.runtime.pr.icon,
|
||||||
|
handler: input.runtime.pr.handler,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function canMergeFromBase(input: BuildGitActionsInput): boolean {
|
||||||
|
if (!input.baseRefAvailable || input.hasUncommittedChanges) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!input.isOnBaseBranch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!input.hasRemote) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return input.aheadOfOrigin > 0 || input.behindOfOrigin > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCreatePrDescription(input: BuildGitActionsInput): string | undefined {
|
||||||
|
if (!input.githubFeaturesEnabled) {
|
||||||
|
return "GitHub features unavailable";
|
||||||
|
}
|
||||||
|
if (input.aheadCount === 0) {
|
||||||
|
return `Branch has no commits ahead of ${input.baseRefLabel}`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMergeBranchDescription(input: BuildGitActionsInput): string | undefined {
|
||||||
|
if (!input.baseRefAvailable) {
|
||||||
|
return "Base ref unavailable";
|
||||||
|
}
|
||||||
|
if (input.hasUncommittedChanges) {
|
||||||
|
return "Requires clean working tree";
|
||||||
|
}
|
||||||
|
if (input.aheadCount === 0) {
|
||||||
|
return `No commits to merge into ${input.baseRefLabel}`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMergeFromBaseDescription(input: BuildGitActionsInput): string | undefined {
|
||||||
|
if (!input.baseRefAvailable) {
|
||||||
|
return "Base ref unavailable";
|
||||||
|
}
|
||||||
|
if (input.hasUncommittedChanges) {
|
||||||
|
return "Requires clean working tree";
|
||||||
|
}
|
||||||
|
if (!input.isOnBaseBranch) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!input.hasRemote) {
|
||||||
|
return "No remote configured";
|
||||||
|
}
|
||||||
|
if (input.aheadOfOrigin === 0 && input.behindOfOrigin === 0) {
|
||||||
|
return "Already up to date";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import type { GitAction, GitActions } from "@/hooks/use-git-actions";
|
import type { GitAction, GitActions } from "@/components/git-actions-policy";
|
||||||
|
|
||||||
interface GitActionsSplitButtonProps {
|
interface GitActionsSplitButtonProps {
|
||||||
gitActions: GitActions;
|
gitActions: GitActions;
|
||||||
@@ -79,7 +79,11 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps
|
|||||||
status={action.status}
|
status={action.status}
|
||||||
pendingLabel={action.pendingLabel}
|
pendingLabel={action.pendingLabel}
|
||||||
successLabel={action.successLabel}
|
successLabel={action.successLabel}
|
||||||
closeOnSelect={action.status === "idle" && action.id === "view-pr"}
|
closeOnSelect={
|
||||||
|
action.status === "idle" &&
|
||||||
|
action.id === "pr" &&
|
||||||
|
action.label === "View PR"
|
||||||
|
}
|
||||||
description={action.description}
|
description={action.description}
|
||||||
onSelect={action.handler}
|
onSelect={action.handler}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -47,19 +47,19 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { GitHubIcon } from "@/components/icons/github-icon";
|
import { GitHubIcon } from "@/components/icons/github-icon";
|
||||||
|
import {
|
||||||
|
buildGitActions,
|
||||||
|
type GitActions,
|
||||||
|
} from "@/components/git-actions-policy";
|
||||||
import {
|
import {
|
||||||
WebDesktopScrollbarOverlay,
|
WebDesktopScrollbarOverlay,
|
||||||
useWebDesktopScrollbarMetrics,
|
useWebDesktopScrollbarMetrics,
|
||||||
} from "@/components/web-desktop-scrollbar";
|
} from "@/components/web-desktop-scrollbar";
|
||||||
import { buildNewAgentRoute, resolveNewAgentWorkingDir } from "@/utils/new-agent-routing";
|
import { buildNewAgentRoute, resolveNewAgentWorkingDir } from "@/utils/new-agent-routing";
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
import { shouldShowMergeFromBaseAction } from "./git-action-visibility";
|
|
||||||
|
|
||||||
import { type GitActionId, type GitAction, type GitActions } from "@/hooks/use-git-actions";
|
|
||||||
import { GitActionsSplitButton } from "@/components/git-actions-split-button";
|
import { GitActionsSplitButton } from "@/components/git-actions-split-button";
|
||||||
|
|
||||||
// Re-export types from shared hook
|
export type { GitActionId, GitAction, GitActions } from "@/components/git-actions-policy";
|
||||||
export type { GitActionId, GitAction, GitActions } from "@/hooks/use-git-actions";
|
|
||||||
|
|
||||||
function openURLInNewTab(url: string): void {
|
function openURLInNewTab(url: string): void {
|
||||||
void openExternalUrl(url);
|
void openExternalUrl(url);
|
||||||
@@ -790,19 +790,13 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
|||||||
const commitDisabled = actionsDisabled || commitStatus === "pending";
|
const commitDisabled = actionsDisabled || commitStatus === "pending";
|
||||||
const prDisabled = actionsDisabled || prCreateStatus === "pending";
|
const prDisabled = actionsDisabled || prCreateStatus === "pending";
|
||||||
const mergeDisabled =
|
const mergeDisabled =
|
||||||
actionsDisabled || mergeStatus === "pending" || hasUncommittedChanges || !baseRef;
|
actionsDisabled || mergeStatus === "pending";
|
||||||
const mergeFromBaseDisabled =
|
const mergeFromBaseDisabled =
|
||||||
actionsDisabled ||
|
actionsDisabled || mergeFromBaseStatus === "pending";
|
||||||
mergeFromBaseStatus === "pending" ||
|
|
||||||
hasUncommittedChanges ||
|
|
||||||
!baseRef ||
|
|
||||||
(isOnBaseBranch && !hasRemote);
|
|
||||||
const pushDisabled =
|
const pushDisabled =
|
||||||
actionsDisabled || pushStatus === "pending" || !(gitStatus?.hasRemote ?? false);
|
actionsDisabled || pushStatus === "pending";
|
||||||
const archiveDisabled =
|
const archiveDisabled =
|
||||||
actionsDisabled ||
|
actionsDisabled || archiveStatus === "pending";
|
||||||
archiveStatus === "pending" ||
|
|
||||||
!gitStatus?.isPaseoOwnedWorktree;
|
|
||||||
|
|
||||||
let bodyContent: ReactElement;
|
let bodyContent: ReactElement;
|
||||||
|
|
||||||
@@ -892,184 +886,67 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
const gitActions: GitActions = useMemo(() => {
|
const gitActions: GitActions = useMemo(() => {
|
||||||
if (!isGit) {
|
return buildGitActions({
|
||||||
return { primary: null, secondary: [], menu: [] };
|
isGit,
|
||||||
}
|
githubFeaturesEnabled,
|
||||||
|
hasPullRequest,
|
||||||
// Build all possible actions
|
pullRequestUrl: prStatus?.url ?? null,
|
||||||
const allActions = new Map<GitActionId, GitAction>();
|
hasRemote,
|
||||||
|
isPaseoOwnedWorktree,
|
||||||
// Commit - always available
|
isOnBaseBranch,
|
||||||
allActions.set("commit", {
|
hasUncommittedChanges,
|
||||||
id: "commit",
|
baseRefAvailable: Boolean(baseRef),
|
||||||
label: "Commit",
|
baseRefLabel,
|
||||||
pendingLabel: "Committing...",
|
aheadCount,
|
||||||
successLabel: "Committed",
|
aheadOfOrigin,
|
||||||
disabled: commitDisabled,
|
behindOfOrigin,
|
||||||
status: commitStatus,
|
shouldPromoteArchive,
|
||||||
icon: <GitCommitHorizontal size={16} color={theme.colors.foregroundMuted} />,
|
shipDefault,
|
||||||
handler: handleCommit,
|
runtime: {
|
||||||
|
commit: {
|
||||||
|
disabled: commitDisabled,
|
||||||
|
status: commitStatus,
|
||||||
|
icon: <GitCommitHorizontal size={16} color={theme.colors.foregroundMuted} />,
|
||||||
|
handler: handleCommit,
|
||||||
|
},
|
||||||
|
push: {
|
||||||
|
disabled: pushDisabled,
|
||||||
|
status: pushStatus,
|
||||||
|
icon: <Upload size={16} color={theme.colors.foregroundMuted} />,
|
||||||
|
handler: handlePush,
|
||||||
|
},
|
||||||
|
pr: {
|
||||||
|
disabled: prDisabled,
|
||||||
|
status: hasPullRequest ? "idle" : prCreateStatus,
|
||||||
|
icon: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
|
||||||
|
handler: () => {
|
||||||
|
if (prStatus?.url) {
|
||||||
|
openURLInNewTab(prStatus.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleCreatePr();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"merge-branch": {
|
||||||
|
disabled: mergeDisabled,
|
||||||
|
status: mergeStatus,
|
||||||
|
icon: <GitMerge size={16} color={theme.colors.foregroundMuted} />,
|
||||||
|
handler: handleMergeBranch,
|
||||||
|
},
|
||||||
|
"merge-from-base": {
|
||||||
|
disabled: mergeFromBaseDisabled,
|
||||||
|
status: mergeFromBaseStatus,
|
||||||
|
icon: <RefreshCcw size={16} color={theme.colors.foregroundMuted} />,
|
||||||
|
handler: handleMergeFromBase,
|
||||||
|
},
|
||||||
|
"archive-worktree": {
|
||||||
|
disabled: archiveDisabled,
|
||||||
|
status: archiveStatus,
|
||||||
|
icon: <Archive size={16} color={theme.colors.foregroundMuted} />,
|
||||||
|
handler: handleArchiveWorktree,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Push - when has remote
|
|
||||||
if (hasRemote) {
|
|
||||||
allActions.set("push", {
|
|
||||||
id: "push",
|
|
||||||
label: "Push",
|
|
||||||
pendingLabel: "Pushing...",
|
|
||||||
successLabel: "Pushed",
|
|
||||||
disabled: pushDisabled,
|
|
||||||
status: pushStatus,
|
|
||||||
description: !hasRemote ? "No remote configured" : undefined,
|
|
||||||
icon: <Upload size={16} color={theme.colors.foregroundMuted} />,
|
|
||||||
handler: handlePush,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// View PR - when PR exists
|
|
||||||
if (githubFeaturesEnabled && hasPullRequest && prStatus?.url) {
|
|
||||||
const prUrl = prStatus.url;
|
|
||||||
allActions.set("view-pr", {
|
|
||||||
id: "view-pr",
|
|
||||||
label: "View PR",
|
|
||||||
pendingLabel: "View PR",
|
|
||||||
successLabel: "View PR",
|
|
||||||
disabled: false,
|
|
||||||
status: "idle",
|
|
||||||
icon: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
|
|
||||||
handler: () => openURLInNewTab(prUrl),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create PR - when ahead of base and no PR
|
|
||||||
if (githubFeaturesEnabled && aheadCount > 0 && !hasPullRequest) {
|
|
||||||
allActions.set("create-pr", {
|
|
||||||
id: "create-pr",
|
|
||||||
label: "Create PR",
|
|
||||||
pendingLabel: "Creating PR...",
|
|
||||||
successLabel: "PR Created",
|
|
||||||
disabled: prDisabled,
|
|
||||||
status: prCreateStatus,
|
|
||||||
icon: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
|
|
||||||
handler: handleCreatePr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge branch - when ahead of base
|
|
||||||
if (aheadCount > 0) {
|
|
||||||
allActions.set("merge-branch", {
|
|
||||||
id: "merge-branch",
|
|
||||||
label: `Merge into ${baseRefLabel}`,
|
|
||||||
pendingLabel: "Merging...",
|
|
||||||
successLabel: "Merged",
|
|
||||||
disabled: mergeDisabled,
|
|
||||||
status: mergeStatus,
|
|
||||||
description: hasUncommittedChanges ? "Requires clean working tree" : undefined,
|
|
||||||
icon: <GitMerge size={16} color={theme.colors.foregroundMuted} />,
|
|
||||||
handler: handleMergeBranch,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update/sync from base
|
|
||||||
if (
|
|
||||||
shouldShowMergeFromBaseAction({
|
|
||||||
isOnBaseBranch,
|
|
||||||
hasRemote,
|
|
||||||
aheadOfOrigin,
|
|
||||||
behindOfOrigin,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
allActions.set("merge-from-base", {
|
|
||||||
id: "merge-from-base",
|
|
||||||
label: isOnBaseBranch ? "Sync" : `Update from ${baseRefLabel}`,
|
|
||||||
pendingLabel: "Updating...",
|
|
||||||
successLabel: "Updated",
|
|
||||||
disabled: mergeFromBaseDisabled,
|
|
||||||
status: mergeFromBaseStatus,
|
|
||||||
description:
|
|
||||||
hasUncommittedChanges
|
|
||||||
? "Requires clean working tree"
|
|
||||||
: isOnBaseBranch && !hasRemote
|
|
||||||
? "No remote configured"
|
|
||||||
: undefined,
|
|
||||||
icon: <RefreshCcw size={16} color={theme.colors.foregroundMuted} />,
|
|
||||||
handler: handleMergeFromBase,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Archive worktree - only for Paseo worktrees
|
|
||||||
if (isPaseoOwnedWorktree) {
|
|
||||||
allActions.set("archive-worktree", {
|
|
||||||
id: "archive-worktree",
|
|
||||||
label: "Archive worktree",
|
|
||||||
pendingLabel: "Archiving...",
|
|
||||||
successLabel: "Archived",
|
|
||||||
disabled: archiveDisabled,
|
|
||||||
status: archiveStatus,
|
|
||||||
icon: <Archive size={16} color={theme.colors.foregroundMuted} />,
|
|
||||||
handler: handleArchiveWorktree,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select primary action (priority rules)
|
|
||||||
let primaryActionId: GitActionId | null = null;
|
|
||||||
|
|
||||||
// Rule 0: Post-ship in worktree -> Archive
|
|
||||||
if (shouldPromoteArchive && allActions.has("archive-worktree")) {
|
|
||||||
primaryActionId = "archive-worktree";
|
|
||||||
}
|
|
||||||
// Rule 1: Uncommitted changes → Commit
|
|
||||||
else if (hasUncommittedChanges) {
|
|
||||||
primaryActionId = "commit";
|
|
||||||
}
|
|
||||||
// Rule 2: Ahead of origin → Push
|
|
||||||
else if (aheadOfOrigin > 0 && allActions.has("push")) {
|
|
||||||
primaryActionId = "push";
|
|
||||||
}
|
|
||||||
// Rule 3: Has PR → View PR
|
|
||||||
else if (hasPullRequest) {
|
|
||||||
primaryActionId = "view-pr";
|
|
||||||
}
|
|
||||||
// Rule 4: On base branch -> surface sync explicitly
|
|
||||||
else if (isOnBaseBranch && allActions.has("merge-from-base")) {
|
|
||||||
primaryActionId = "merge-from-base";
|
|
||||||
}
|
|
||||||
// Rule 5: Ahead of base → Ship action based on preference
|
|
||||||
else if (aheadCount > 0) {
|
|
||||||
const preferred: GitActionId = shipDefault === "merge" ? "merge-branch" : "create-pr";
|
|
||||||
const fallback: GitActionId = shipDefault === "merge" ? "create-pr" : "merge-branch";
|
|
||||||
|
|
||||||
const preferredAction = allActions.get(preferred);
|
|
||||||
const fallbackAction = allActions.get(fallback);
|
|
||||||
|
|
||||||
if (preferredAction && !preferredAction.disabled) {
|
|
||||||
primaryActionId = preferred;
|
|
||||||
} else if (fallbackAction && !fallbackAction.disabled) {
|
|
||||||
primaryActionId = fallback;
|
|
||||||
} else if (preferredAction) {
|
|
||||||
primaryActionId = preferred;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const primary = primaryActionId ? allActions.get(primaryActionId) ?? null : null;
|
|
||||||
|
|
||||||
// Secondary actions: ship-related + merge from base + push (excluding primary)
|
|
||||||
const secondaryIds: GitActionId[] = [
|
|
||||||
"merge-branch",
|
|
||||||
"create-pr",
|
|
||||||
"view-pr",
|
|
||||||
"merge-from-base",
|
|
||||||
"push",
|
|
||||||
"archive-worktree",
|
|
||||||
];
|
|
||||||
const secondary = secondaryIds
|
|
||||||
.filter(id => id !== primaryActionId && allActions.has(id))
|
|
||||||
.map(id => allActions.get(id)!);
|
|
||||||
|
|
||||||
// Menu actions: none for now (all actionable items are in primary/secondary)
|
|
||||||
const menu: GitAction[] = [];
|
|
||||||
|
|
||||||
return { primary, secondary, menu };
|
|
||||||
}, [
|
}, [
|
||||||
isGit, hasRemote, hasPullRequest, prStatus?.url, aheadCount, isPaseoOwnedWorktree, isOnBaseBranch, githubFeaturesEnabled,
|
isGit, hasRemote, hasPullRequest, prStatus?.url, aheadCount, isPaseoOwnedWorktree, isOnBaseBranch, githubFeaturesEnabled,
|
||||||
hasUncommittedChanges, aheadOfOrigin, behindOfOrigin, shipDefault, baseRefLabel, shouldPromoteArchive,
|
hasUncommittedChanges, aheadOfOrigin, behindOfOrigin, shipDefault, baseRefLabel, shouldPromoteArchive,
|
||||||
|
|||||||
@@ -4,37 +4,14 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
|
|||||||
import { useCheckoutGitActionsStore } from "@/stores/checkout-git-actions-store";
|
import { useCheckoutGitActionsStore } from "@/stores/checkout-git-actions-store";
|
||||||
import { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query";
|
import { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query";
|
||||||
import { useCheckoutPrStatusQuery } from "@/hooks/use-checkout-pr-status-query";
|
import { useCheckoutPrStatusQuery } from "@/hooks/use-checkout-pr-status-query";
|
||||||
import { shouldShowMergeFromBaseAction } from "@/components/git-action-visibility";
|
import {
|
||||||
|
buildGitActions,
|
||||||
|
type GitActions,
|
||||||
|
} from "@/components/git-actions-policy";
|
||||||
import { buildNewAgentRoute, resolveNewAgentWorkingDir } from "@/utils/new-agent-routing";
|
import { buildNewAgentRoute, resolveNewAgentWorkingDir } from "@/utils/new-agent-routing";
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
import type { ActionStatus } from "@/components/ui/dropdown-menu";
|
|
||||||
|
|
||||||
export type GitActionId =
|
export type { GitActionId, GitAction, GitActions } from "@/components/git-actions-policy";
|
||||||
| "commit"
|
|
||||||
| "push"
|
|
||||||
| "view-pr"
|
|
||||||
| "create-pr"
|
|
||||||
| "merge-branch"
|
|
||||||
| "merge-from-base"
|
|
||||||
| "archive-worktree";
|
|
||||||
|
|
||||||
export interface GitAction {
|
|
||||||
id: GitActionId;
|
|
||||||
label: string;
|
|
||||||
pendingLabel: string;
|
|
||||||
successLabel: string;
|
|
||||||
disabled: boolean;
|
|
||||||
status: ActionStatus;
|
|
||||||
description?: string;
|
|
||||||
icon?: ReactElement;
|
|
||||||
handler: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GitActions {
|
|
||||||
primary: GitAction | null;
|
|
||||||
secondary: GitAction[];
|
|
||||||
menu: GitAction[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function openURLInNewTab(url: string): void {
|
function openURLInNewTab(url: string): void {
|
||||||
void openExternalUrl(url);
|
void openExternalUrl(url);
|
||||||
@@ -252,19 +229,13 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
const commitDisabled = actionsDisabled || commitStatus === "pending";
|
const commitDisabled = actionsDisabled || commitStatus === "pending";
|
||||||
const prDisabled = actionsDisabled || prCreateStatus === "pending";
|
const prDisabled = actionsDisabled || prCreateStatus === "pending";
|
||||||
const mergeDisabled =
|
const mergeDisabled =
|
||||||
actionsDisabled || mergeStatus === "pending" || hasUncommittedChanges || !baseRef;
|
actionsDisabled || mergeStatus === "pending";
|
||||||
const mergeFromBaseDisabled =
|
const mergeFromBaseDisabled =
|
||||||
actionsDisabled ||
|
actionsDisabled || mergeFromBaseStatus === "pending";
|
||||||
mergeFromBaseStatus === "pending" ||
|
|
||||||
hasUncommittedChanges ||
|
|
||||||
!baseRef ||
|
|
||||||
(isOnBaseBranch && !hasRemote);
|
|
||||||
const pushDisabled =
|
const pushDisabled =
|
||||||
actionsDisabled || pushStatus === "pending" || !(gitStatus?.hasRemote ?? false);
|
actionsDisabled || pushStatus === "pending";
|
||||||
const archiveDisabled =
|
const archiveDisabled =
|
||||||
actionsDisabled ||
|
actionsDisabled || archiveStatus === "pending";
|
||||||
archiveStatus === "pending" ||
|
|
||||||
!gitStatus?.isPaseoOwnedWorktree;
|
|
||||||
|
|
||||||
const branchLabel =
|
const branchLabel =
|
||||||
gitStatus?.currentBranch && gitStatus.currentBranch !== "HEAD"
|
gitStatus?.currentBranch && gitStatus.currentBranch !== "HEAD"
|
||||||
@@ -275,163 +246,67 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
|
|
||||||
// Build actions
|
// Build actions
|
||||||
const gitActions: GitActions = useMemo(() => {
|
const gitActions: GitActions = useMemo(() => {
|
||||||
if (!isGit) {
|
return buildGitActions({
|
||||||
return { primary: null, secondary: [], menu: [] };
|
isGit,
|
||||||
}
|
githubFeaturesEnabled,
|
||||||
|
hasPullRequest,
|
||||||
const allActions = new Map<GitActionId, GitAction>();
|
pullRequestUrl: prStatus?.url ?? null,
|
||||||
|
hasRemote,
|
||||||
allActions.set("commit", {
|
isPaseoOwnedWorktree,
|
||||||
id: "commit",
|
isOnBaseBranch,
|
||||||
label: "Commit",
|
hasUncommittedChanges,
|
||||||
pendingLabel: "Committing...",
|
baseRefAvailable: Boolean(baseRef),
|
||||||
successLabel: "Committed",
|
baseRefLabel,
|
||||||
disabled: commitDisabled,
|
aheadCount,
|
||||||
status: commitStatus,
|
aheadOfOrigin,
|
||||||
icon: icons.commit,
|
behindOfOrigin,
|
||||||
handler: handleCommit,
|
shouldPromoteArchive,
|
||||||
|
shipDefault,
|
||||||
|
runtime: {
|
||||||
|
commit: {
|
||||||
|
disabled: commitDisabled,
|
||||||
|
status: commitStatus,
|
||||||
|
icon: icons.commit,
|
||||||
|
handler: handleCommit,
|
||||||
|
},
|
||||||
|
push: {
|
||||||
|
disabled: pushDisabled,
|
||||||
|
status: pushStatus,
|
||||||
|
icon: icons.push,
|
||||||
|
handler: handlePush,
|
||||||
|
},
|
||||||
|
pr: {
|
||||||
|
disabled: prDisabled,
|
||||||
|
status: hasPullRequest ? "idle" : prCreateStatus,
|
||||||
|
icon: hasPullRequest ? icons.viewPr : icons.createPr,
|
||||||
|
handler: () => {
|
||||||
|
if (prStatus?.url) {
|
||||||
|
openURLInNewTab(prStatus.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleCreatePr();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"merge-branch": {
|
||||||
|
disabled: mergeDisabled,
|
||||||
|
status: mergeStatus,
|
||||||
|
icon: icons.merge,
|
||||||
|
handler: handleMergeBranch,
|
||||||
|
},
|
||||||
|
"merge-from-base": {
|
||||||
|
disabled: mergeFromBaseDisabled,
|
||||||
|
status: mergeFromBaseStatus,
|
||||||
|
icon: icons.mergeFromBase,
|
||||||
|
handler: handleMergeFromBase,
|
||||||
|
},
|
||||||
|
"archive-worktree": {
|
||||||
|
disabled: archiveDisabled,
|
||||||
|
status: archiveStatus,
|
||||||
|
icon: icons.archive,
|
||||||
|
handler: handleArchiveWorktree,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasRemote) {
|
|
||||||
allActions.set("push", {
|
|
||||||
id: "push",
|
|
||||||
label: "Push",
|
|
||||||
pendingLabel: "Pushing...",
|
|
||||||
successLabel: "Pushed",
|
|
||||||
disabled: pushDisabled,
|
|
||||||
status: pushStatus,
|
|
||||||
description: !hasRemote ? "No remote configured" : undefined,
|
|
||||||
icon: icons.push,
|
|
||||||
handler: handlePush,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (githubFeaturesEnabled && hasPullRequest && prStatus?.url) {
|
|
||||||
const prUrl = prStatus.url;
|
|
||||||
allActions.set("view-pr", {
|
|
||||||
id: "view-pr",
|
|
||||||
label: "View PR",
|
|
||||||
pendingLabel: "View PR",
|
|
||||||
successLabel: "View PR",
|
|
||||||
disabled: false,
|
|
||||||
status: "idle",
|
|
||||||
icon: icons.viewPr,
|
|
||||||
handler: () => openURLInNewTab(prUrl),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (githubFeaturesEnabled && aheadCount > 0 && !hasPullRequest) {
|
|
||||||
allActions.set("create-pr", {
|
|
||||||
id: "create-pr",
|
|
||||||
label: "Create PR",
|
|
||||||
pendingLabel: "Creating PR...",
|
|
||||||
successLabel: "PR Created",
|
|
||||||
disabled: prDisabled,
|
|
||||||
status: prCreateStatus,
|
|
||||||
icon: icons.createPr,
|
|
||||||
handler: handleCreatePr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aheadCount > 0) {
|
|
||||||
allActions.set("merge-branch", {
|
|
||||||
id: "merge-branch",
|
|
||||||
label: `Merge into ${baseRefLabel}`,
|
|
||||||
pendingLabel: "Merging...",
|
|
||||||
successLabel: "Merged",
|
|
||||||
disabled: mergeDisabled,
|
|
||||||
status: mergeStatus,
|
|
||||||
description: hasUncommittedChanges ? "Requires clean working tree" : undefined,
|
|
||||||
icon: icons.merge,
|
|
||||||
handler: handleMergeBranch,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
shouldShowMergeFromBaseAction({
|
|
||||||
isOnBaseBranch,
|
|
||||||
hasRemote,
|
|
||||||
aheadOfOrigin,
|
|
||||||
behindOfOrigin,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
allActions.set("merge-from-base", {
|
|
||||||
id: "merge-from-base",
|
|
||||||
label: isOnBaseBranch ? "Sync" : `Update from ${baseRefLabel}`,
|
|
||||||
pendingLabel: "Updating...",
|
|
||||||
successLabel: "Updated",
|
|
||||||
disabled: mergeFromBaseDisabled,
|
|
||||||
status: mergeFromBaseStatus,
|
|
||||||
description:
|
|
||||||
hasUncommittedChanges
|
|
||||||
? "Requires clean working tree"
|
|
||||||
: isOnBaseBranch && !hasRemote
|
|
||||||
? "No remote configured"
|
|
||||||
: undefined,
|
|
||||||
icon: icons.mergeFromBase,
|
|
||||||
handler: handleMergeFromBase,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPaseoOwnedWorktree) {
|
|
||||||
allActions.set("archive-worktree", {
|
|
||||||
id: "archive-worktree",
|
|
||||||
label: "Archive worktree",
|
|
||||||
pendingLabel: "Archiving...",
|
|
||||||
successLabel: "Archived",
|
|
||||||
disabled: archiveDisabled,
|
|
||||||
status: archiveStatus,
|
|
||||||
icon: icons.archive,
|
|
||||||
handler: handleArchiveWorktree,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select primary action (priority rules)
|
|
||||||
let primaryActionId: GitActionId | null = null;
|
|
||||||
|
|
||||||
if (shouldPromoteArchive && allActions.has("archive-worktree")) {
|
|
||||||
primaryActionId = "archive-worktree";
|
|
||||||
} else if (hasUncommittedChanges) {
|
|
||||||
primaryActionId = "commit";
|
|
||||||
} else if (aheadOfOrigin > 0 && allActions.has("push")) {
|
|
||||||
primaryActionId = "push";
|
|
||||||
} else if (hasPullRequest) {
|
|
||||||
primaryActionId = "view-pr";
|
|
||||||
} else if (isOnBaseBranch && allActions.has("merge-from-base")) {
|
|
||||||
primaryActionId = "merge-from-base";
|
|
||||||
} else if (aheadCount > 0) {
|
|
||||||
const preferred: GitActionId = shipDefault === "merge" ? "merge-branch" : "create-pr";
|
|
||||||
const fallback: GitActionId = shipDefault === "merge" ? "create-pr" : "merge-branch";
|
|
||||||
|
|
||||||
const preferredAction = allActions.get(preferred);
|
|
||||||
const fallbackAction = allActions.get(fallback);
|
|
||||||
|
|
||||||
if (preferredAction && !preferredAction.disabled) {
|
|
||||||
primaryActionId = preferred;
|
|
||||||
} else if (fallbackAction && !fallbackAction.disabled) {
|
|
||||||
primaryActionId = fallback;
|
|
||||||
} else if (preferredAction) {
|
|
||||||
primaryActionId = preferred;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const primary = primaryActionId ? allActions.get(primaryActionId) ?? null : null;
|
|
||||||
|
|
||||||
const secondaryIds: GitActionId[] = [
|
|
||||||
"merge-branch",
|
|
||||||
"create-pr",
|
|
||||||
"view-pr",
|
|
||||||
"merge-from-base",
|
|
||||||
"push",
|
|
||||||
"archive-worktree",
|
|
||||||
];
|
|
||||||
const secondary = secondaryIds
|
|
||||||
.filter(id => id !== primaryActionId && allActions.has(id))
|
|
||||||
.map(id => allActions.get(id)!);
|
|
||||||
|
|
||||||
const menu: GitAction[] = [];
|
|
||||||
|
|
||||||
return { primary, secondary, menu };
|
|
||||||
}, [
|
}, [
|
||||||
isGit, hasRemote, hasPullRequest, prStatus?.url, aheadCount, isPaseoOwnedWorktree, isOnBaseBranch, githubFeaturesEnabled,
|
isGit, hasRemote, hasPullRequest, prStatus?.url, aheadCount, isPaseoOwnedWorktree, isOnBaseBranch, githubFeaturesEnabled,
|
||||||
hasUncommittedChanges, aheadOfOrigin, behindOfOrigin, shipDefault, baseRefLabel, shouldPromoteArchive,
|
hasUncommittedChanges, aheadOfOrigin, behindOfOrigin, shipDefault, baseRefLabel, shouldPromoteArchive,
|
||||||
|
|||||||
Reference in New Issue
Block a user