import type { GitRunStackedActionInput, GitRunStackedActionResult, GitStackedAction, VcsStatusResult,} from "@t3tools/contracts";import { isTemporaryWorktreeBranch } from "@t3tools/shared/git"; export type GitActionIconName = "commit" | "push" | "pr"; export type GitDialogAction = "commit" | "push" | "create_pr"; export interface GitActionMenuItem { id: "commit" | "push" | "pr"; label: string; disabled: boolean; icon: GitActionIconName; kind: "open_dialog" | "open_pr"; dialogAction?: GitDialogAction;} export interface GitQuickAction { label: string; disabled: boolean; kind: "run_action" | "run_pull" | "open_pr" | "show_hint"; action?: GitStackedAction; hint?: string;} export interface DefaultBranchActionDialogCopy { title: string; description: string; continueLabel: string;} export type DefaultBranchConfirmableAction = | "push" | "create_pr" | "commit_push" | "commit_push_pr"; export type GitActionRequestInput = Pick< GitRunStackedActionInput, "action" | "commitMessage" | "featureBranch" | "filePaths">; export function buildGitActionProgressStages(input: { action: GitStackedAction; hasCustomCommitMessage: boolean; hasWorkingTreeChanges: boolean; pushTarget?: string; featureBranch?: boolean; shouldPushBeforePr?: boolean;}): string[] { const branchStages = input.featureBranch ? ["Preparing feature branch..."] : []; const pushStage = input.pushTarget ? `Pushing to ${input.pushTarget}...` : "Pushing..."; const prStages = [ "Preparing PR...", "Generating PR content...", "Creating GitHub pull request...", ]; if (input.action === "push") { return [pushStage]; } if (input.action === "create_pr") { return input.shouldPushBeforePr ? [pushStage, ...prStages] : prStages; } const shouldIncludeCommitStages = input.action === "commit" || input.hasWorkingTreeChanges; const commitStages = !shouldIncludeCommitStages ? [] : input.hasCustomCommitMessage ? ["Committing..."] : ["Generating commit message...", "Committing..."]; if (input.action === "commit") { return [...branchStages, ...commitStages]; } if (input.action === "commit_push") { return [...branchStages, ...commitStages, pushStage]; } return [...branchStages, ...commitStages, pushStage, ...prStages];} export function buildMenuItems( gitStatus: VcsStatusResult | null, isBusy: boolean, hasOriginRemote = true,): GitActionMenuItem[] { if (!gitStatus) return []; const hasBranch = gitStatus.refName !== null; const hasChanges = gitStatus.hasWorkingTreeChanges; const hasOpenPr = gitStatus.pr?.state === "open"; const isBehind = gitStatus.behindCount > 0; const canPushWithoutUpstream = hasOriginRemote && !gitStatus.hasUpstream; const canCommit = !isBusy && hasChanges; const canPush = !isBusy && hasBranch && !hasChanges && !isBehind && gitStatus.aheadCount > 0 && (gitStatus.hasUpstream || canPushWithoutUpstream); const canCreatePr = !isBusy && hasBranch && !hasChanges && !hasOpenPr && gitStatus.aheadCount > 0 && !isBehind && (gitStatus.hasUpstream || canPushWithoutUpstream); const canOpenPr = !isBusy && hasOpenPr; return [ { id: "commit", label: "Commit", disabled: !canCommit, icon: "commit", kind: "open_dialog", dialogAction: "commit", }, { id: "push", label: "Push", disabled: !canPush, icon: "push", kind: "open_dialog", dialogAction: "push", }, hasOpenPr ? { id: "pr", label: "View PR", disabled: !canOpenPr, icon: "pr", kind: "open_pr", } : { id: "pr", label: "Create PR", disabled: !canCreatePr, icon: "pr", kind: "open_dialog", dialogAction: "create_pr", }, ];} export function resolveQuickAction( gitStatus: VcsStatusResult | null, isBusy: boolean, isDefaultBranch = false, hasOriginRemote = true,): GitQuickAction { if (isBusy) { return { label: "Commit", disabled: true, kind: "show_hint", hint: "Git action in progress." }; } if (!gitStatus) { return { label: "Commit", disabled: true, kind: "show_hint", hint: "Git status is unavailable.", }; } const hasBranch = gitStatus.refName !== null; const hasChanges = gitStatus.hasWorkingTreeChanges; const hasOpenPr = gitStatus.pr?.state === "open"; const isAhead = gitStatus.aheadCount > 0; const isBehind = gitStatus.behindCount > 0; const isDiverged = isAhead && isBehind; if (!hasBranch) { return { label: "Commit", disabled: true, kind: "show_hint", hint: "Create and checkout a branch before pushing or opening a PR.", }; } if (hasChanges) { if (!gitStatus.hasUpstream && !hasOriginRemote) { return { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; } if (hasOpenPr || isDefaultBranch) { return { label: "Commit & push", disabled: false, kind: "run_action", action: "commit_push" }; } return { label: "Commit, push & PR", disabled: false, kind: "run_action", action: "commit_push_pr", }; } if (!gitStatus.hasUpstream) { if (!hasOriginRemote) { if (hasOpenPr && !isAhead) { return { label: "View PR", disabled: false, kind: "open_pr" }; } return { label: "Push", disabled: true, kind: "show_hint", hint: 'Add an "origin" remote before pushing or creating a PR.', }; } if (!isAhead) { if (hasOpenPr) { return { label: "View PR", disabled: false, kind: "open_pr" }; } return { label: "Push", disabled: true, kind: "show_hint", hint: "No local commits to push.", }; } if (hasOpenPr || isDefaultBranch) { return { label: "Push", disabled: false, kind: "run_action", action: isDefaultBranch ? "commit_push" : "push", }; } return { label: "Push & create PR", disabled: false, kind: "run_action", action: "create_pr", }; } if (isDiverged) { return { label: "Sync branch", disabled: true, kind: "show_hint", hint: "Branch has diverged from upstream. Rebase/merge first.", }; } if (isBehind) { return { label: "Pull", disabled: false, kind: "run_pull", }; } if (isAhead) { if (hasOpenPr || isDefaultBranch) { return { label: "Push", disabled: false, kind: "run_action", action: isDefaultBranch ? "commit_push" : "push", }; } return { label: "Push & create PR", disabled: false, kind: "run_action", action: "create_pr", }; } if (hasOpenPr && gitStatus.hasUpstream) { return { label: "View PR", disabled: false, kind: "open_pr" }; } return { label: "Commit", disabled: true, kind: "show_hint", hint: "Branch is up to date. No action needed.", };} export function getGitActionDisabledReason(input: { item: GitActionMenuItem; gitStatus: VcsStatusResult | null; isBusy: boolean; hasOriginRemote: boolean;}): string | null { const { item, gitStatus, isBusy, hasOriginRemote } = input; 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 hasOpenPr = gitStatus.pr?.state === "open"; const isAhead = gitStatus.aheadCount > 0; const isBehind = gitStatus.behindCount > 0; 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: checkout 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 && !hasOriginRemote) { return 'Add an "origin" remote before pushing.'; } if (!isAhead) { return "No local commits to push."; } return "Push is currently unavailable."; } if (hasOpenPr) { return "View PR is currently unavailable."; } if (!hasBranch) { return "Detached HEAD: checkout a branch before creating a PR."; } if (hasChanges) { return "Commit local changes before creating a PR."; } if (!gitStatus.hasUpstream && !hasOriginRemote) { return 'Add an "origin" remote before creating a PR.'; } if (!isAhead) { return "No local commits to include in a PR."; } if (isBehind) { return "Branch is behind upstream. Pull/rebase before creating a PR."; } return "Create PR is currently unavailable.";} export function requiresDefaultBranchConfirmation( action: GitStackedAction, isDefaultBranch: boolean,): boolean { if (!isDefaultBranch) return false; return ( action === "push" || action === "create_pr" || action === "commit_push" || action === "commit_push_pr" );} export function resolveDefaultBranchActionDialogCopy(input: { action: DefaultBranchConfirmableAction; branchName: string; includesCommit: boolean;}): DefaultBranchActionDialogCopy { const branchLabel = input.branchName; const suffix = ` on "${branchLabel}". You can continue on this branch or create a feature branch and run the same action there.`; if (input.action === "push" || input.action === "commit_push") { if (input.includesCommit) { return { title: "Commit & push to default branch?", description: `This action will commit and push changes${suffix}`, continueLabel: `Commit & push to ${branchLabel}`, }; } return { title: "Push to default branch?", description: `This action will push local commits${suffix}`, continueLabel: `Push to ${branchLabel}`, }; } if (input.includesCommit) { return { title: "Commit, push & create PR from default branch?", description: `This action will commit, push, and create a PR${suffix}`, continueLabel: "Commit, push & create PR", }; } return { title: "Push & create PR from default branch?", description: `This action will push local commits and create a PR${suffix}`, continueLabel: "Push & create PR", };} export function resolveThreadBranchUpdate( result: GitRunStackedActionResult,): { branch: string } | null { if (result.branch.status !== "created" || !result.branch.name) { return null; } return { branch: result.branch.name, };} export function resolveLiveThreadBranchUpdate(input: { threadBranch: string | null; gitStatus: VcsStatusResult | null;}): { branch: string | null } | null { if (!input.gitStatus) { return null; } if (input.gitStatus.refName === null && input.threadBranch !== null) { return null; } if (input.threadBranch === input.gitStatus.refName) { return null; } if ( input.threadBranch !== null && input.gitStatus.refName !== null && !isTemporaryWorktreeBranch(input.threadBranch) && isTemporaryWorktreeBranch(input.gitStatus.refName) ) { return null; } return { branch: input.gitStatus.refName, };}
import type {
GitRunStackedActionInput,
GitRunStackedActionResult,
GitStackedAction,
VcsStatusResult,
} from "@t3tools/contracts";
import { isTemporaryWorktreeBranch } from "@t3tools/shared/git";
export type GitActionIconName = "commit" | "push" | "pr";
export type GitDialogAction = "commit" | "push" | "create_pr";
export interface GitActionMenuItem {
id: "commit" | "push" | "pr";
label: string;
disabled: boolean;
icon: GitActionIconName;
kind: "open_dialog" | "open_pr";
dialogAction?: GitDialogAction;
}
export interface GitQuickAction {
label: string;
disabled: boolean;
kind: "run_action" | "run_pull" | "open_pr" | "show_hint";
action?: GitStackedAction;
hint?: string;
}
export interface DefaultBranchActionDialogCopy {
title: string;
description: string;
continueLabel: string;
}
export type DefaultBranchConfirmableAction =
| "push"
| "create_pr"
| "commit_push"
| "commit_push_pr";
export type GitActionRequestInput = Pick<
GitRunStackedActionInput,
"action" | "commitMessage" | "featureBranch" | "filePaths"
>;
export function buildGitActionProgressStages(input: {
action: GitStackedAction;
hasCustomCommitMessage: boolean;
hasWorkingTreeChanges: boolean;
pushTarget?: string;
featureBranch?: boolean;
shouldPushBeforePr?: boolean;
}): string[] {
const branchStages = input.featureBranch ? ["Preparing feature branch..."] : [];
const pushStage = input.pushTarget ? `Pushing to ${input.pushTarget}...` : "Pushing...";
const prStages = [
"Preparing PR...",
"Generating PR content...",
"Creating GitHub pull request...",
];
if (input.action === "push") {
return [pushStage];
}
if (input.action === "create_pr") {
return input.shouldPushBeforePr ? [pushStage, ...prStages] : prStages;
}
const shouldIncludeCommitStages = input.action === "commit" || input.hasWorkingTreeChanges;
const commitStages = !shouldIncludeCommitStages
? []
: input.hasCustomCommitMessage
? ["Committing..."]
: ["Generating commit message...", "Committing..."];
if (input.action === "commit") {
return [...branchStages, ...commitStages];
}
if (input.action === "commit_push") {
return [...branchStages, ...commitStages, pushStage];
}
return [...branchStages, ...commitStages, pushStage, ...prStages];
}
export function buildMenuItems(
gitStatus: VcsStatusResult | null,
isBusy: boolean,
hasOriginRemote = true,
): GitActionMenuItem[] {
if (!gitStatus) return [];
const hasBranch = gitStatus.refName !== null;
const hasChanges = gitStatus.hasWorkingTreeChanges;
const hasOpenPr = gitStatus.pr?.state === "open";
const isBehind = gitStatus.behindCount > 0;
const canPushWithoutUpstream = hasOriginRemote && !gitStatus.hasUpstream;
const canCommit = !isBusy && hasChanges;
const canPush =
!isBusy &&
hasBranch &&
!hasChanges &&
!isBehind &&
gitStatus.aheadCount > 0 &&
(gitStatus.hasUpstream || canPushWithoutUpstream);
const canCreatePr =
!isBusy &&
hasBranch &&
!hasChanges &&
!hasOpenPr &&
gitStatus.aheadCount > 0 &&
!isBehind &&
(gitStatus.hasUpstream || canPushWithoutUpstream);
const canOpenPr = !isBusy && hasOpenPr;
return [
{
id: "commit",
label: "Commit",
disabled: !canCommit,
icon: "commit",
kind: "open_dialog",
dialogAction: "commit",
},
{
id: "push",
label: "Push",
disabled: !canPush,
icon: "push",
kind: "open_dialog",
dialogAction: "push",
},
hasOpenPr
? {
id: "pr",
label: "View PR",
disabled: !canOpenPr,
icon: "pr",
kind: "open_pr",
}
: {
id: "pr",
label: "Create PR",
disabled: !canCreatePr,
icon: "pr",
kind: "open_dialog",
dialogAction: "create_pr",
},
];
}
export function resolveQuickAction(
gitStatus: VcsStatusResult | null,
isBusy: boolean,
isDefaultBranch = false,
hasOriginRemote = true,
): GitQuickAction {
if (isBusy) {
return { label: "Commit", disabled: true, kind: "show_hint", hint: "Git action in progress." };
}
if (!gitStatus) {
return {
label: "Commit",
disabled: true,
kind: "show_hint",
hint: "Git status is unavailable.",
};
}
const hasBranch = gitStatus.refName !== null;
const hasChanges = gitStatus.hasWorkingTreeChanges;
const hasOpenPr = gitStatus.pr?.state === "open";
const isAhead = gitStatus.aheadCount > 0;
const isBehind = gitStatus.behindCount > 0;
const isDiverged = isAhead && isBehind;
if (!hasBranch) {
return {
label: "Commit",
disabled: true,
kind: "show_hint",
hint: "Create and checkout a branch before pushing or opening a PR.",
};
}
if (hasChanges) {
if (!gitStatus.hasUpstream && !hasOriginRemote) {
return { label: "Commit", disabled: false, kind: "run_action", action: "commit" };
}
if (hasOpenPr || isDefaultBranch) {
return { label: "Commit & push", disabled: false, kind: "run_action", action: "commit_push" };
}
return {
label: "Commit, push & PR",
disabled: false,
kind: "run_action",
action: "commit_push_pr",
};
}
if (!gitStatus.hasUpstream) {
if (!hasOriginRemote) {
if (hasOpenPr && !isAhead) {
return { label: "View PR", disabled: false, kind: "open_pr" };
}
return {
label: "Push",
disabled: true,
kind: "show_hint",
hint: 'Add an "origin" remote before pushing or creating a PR.',
};
}
if (!isAhead) {
if (hasOpenPr) {
return { label: "View PR", disabled: false, kind: "open_pr" };
}
return {
label: "Push",
disabled: true,
kind: "show_hint",
hint: "No local commits to push.",
};
}
if (hasOpenPr || isDefaultBranch) {
return {
label: "Push",
disabled: false,
kind: "run_action",
action: isDefaultBranch ? "commit_push" : "push",
};
}
return {
label: "Push & create PR",
disabled: false,
kind: "run_action",
action: "create_pr",
};
}
if (isDiverged) {
return {
label: "Sync branch",
disabled: true,
kind: "show_hint",
hint: "Branch has diverged from upstream. Rebase/merge first.",
};
}
if (isBehind) {
return {
label: "Pull",
disabled: false,
kind: "run_pull",
};
}
if (isAhead) {
if (hasOpenPr || isDefaultBranch) {
return {
label: "Push",
disabled: false,
kind: "run_action",
action: isDefaultBranch ? "commit_push" : "push",
};
}
return {
label: "Push & create PR",
disabled: false,
kind: "run_action",
action: "create_pr",
};
}
if (hasOpenPr && gitStatus.hasUpstream) {
return { label: "View PR", disabled: false, kind: "open_pr" };
}
return {
label: "Commit",
disabled: true,
kind: "show_hint",
hint: "Branch is up to date. No action needed.",
};
}
export function getGitActionDisabledReason(input: {
item: GitActionMenuItem;
gitStatus: VcsStatusResult | null;
isBusy: boolean;
hasOriginRemote: boolean;
}): string | null {
const { item, gitStatus, isBusy, hasOriginRemote } = input;
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 hasOpenPr = gitStatus.pr?.state === "open";
const isAhead = gitStatus.aheadCount > 0;
const isBehind = gitStatus.behindCount > 0;
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: checkout 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 && !hasOriginRemote) {
return 'Add an "origin" remote before pushing.';
}
if (!isAhead) {
return "No local commits to push.";
}
return "Push is currently unavailable.";
}
if (hasOpenPr) {
return "View PR is currently unavailable.";
}
if (!hasBranch) {
return "Detached HEAD: checkout a branch before creating a PR.";
}
if (hasChanges) {
return "Commit local changes before creating a PR.";
}
if (!gitStatus.hasUpstream && !hasOriginRemote) {
return 'Add an "origin" remote before creating a PR.';
}
if (!isAhead) {
return "No local commits to include in a PR.";
}
if (isBehind) {
return "Branch is behind upstream. Pull/rebase before creating a PR.";
}
return "Create PR is currently unavailable.";
}
export function requiresDefaultBranchConfirmation(
action: GitStackedAction,
isDefaultBranch: boolean,
): boolean {
if (!isDefaultBranch) return false;
return (
action === "push" ||
action === "create_pr" ||
action === "commit_push" ||
action === "commit_push_pr"
);
}
export function resolveDefaultBranchActionDialogCopy(input: {
action: DefaultBranchConfirmableAction;
branchName: string;
includesCommit: boolean;
}): DefaultBranchActionDialogCopy {
const branchLabel = input.branchName;
const suffix = ` on "${branchLabel}". You can continue on this branch or create a feature branch and run the same action there.`;
if (input.action === "push" || input.action === "commit_push") {
if (input.includesCommit) {
return {
title: "Commit & push to default branch?",
description: `This action will commit and push changes${suffix}`,
continueLabel: `Commit & push to ${branchLabel}`,
};
}
return {
title: "Push to default branch?",
description: `This action will push local commits${suffix}`,
continueLabel: `Push to ${branchLabel}`,
};
}
if (input.includesCommit) {
return {
title: "Commit, push & create PR from default branch?",
description: `This action will commit, push, and create a PR${suffix}`,
continueLabel: "Commit, push & create PR",
};
}
return {
title: "Push & create PR from default branch?",
description: `This action will push local commits and create a PR${suffix}`,
continueLabel: "Push & create PR",
};
}
export function resolveThreadBranchUpdate(
result: GitRunStackedActionResult,
): { branch: string } | null {
if (result.branch.status !== "created" || !result.branch.name) {
return null;
}
return {
branch: result.branch.name,
};
}
export function resolveLiveThreadBranchUpdate(input: {
threadBranch: string | null;
gitStatus: VcsStatusResult | null;
}): { branch: string | null } | null {
if (!input.gitStatus) {
return null;
}
if (input.gitStatus.refName === null && input.threadBranch !== null) {
return null;
}
if (input.threadBranch === input.gitStatus.refName) {
return null;
}
if (
input.threadBranch !== null &&
input.gitStatus.refName !== null &&
!isTemporaryWorktreeBranch(input.threadBranch) &&
isTemporaryWorktreeBranch(input.gitStatus.refName)
) {
return null;
}
return {
branch: input.gitStatus.refName,
};
}