import type { GitRunStackedActionResult, GitStackedAction, GitStatusResult,} from "@t3tools/contracts"; 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 = "commit_push" | "commit_push_pr"; const SHORT_SHA_LENGTH = 7;const TOAST_DESCRIPTION_MAX = 72;const AUTO_FEATURE_BRANCH_PREFIX = "feature/stacked"; function shortenSha(sha: string | undefined): string | null { if (!sha) return null; return sha.slice(0, SHORT_SHA_LENGTH);} function truncateText( value: string | undefined, maxLength = TOAST_DESCRIPTION_MAX,): string | undefined { if (!value) return undefined; if (value.length <= maxLength) return value; if (maxLength <= 3) return "...".slice(0, maxLength); return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;} export function buildGitActionProgressStages(input: { action: GitStackedAction; hasCustomCommitMessage: boolean; hasWorkingTreeChanges: boolean; forcePushOnly?: boolean; pushTarget?: string;}): string[] { const shouldIncludeCommitStages = !input.forcePushOnly && (input.action === "commit" || input.hasWorkingTreeChanges); const commitStages = !shouldIncludeCommitStages ? [] : input.hasCustomCommitMessage ? ["Committing..."] : ["Generating commit message...", "Committing..."]; const pushStage = input.pushTarget ? `Pushing to ${input.pushTarget}...` : "Pushing..."; if (input.action === "commit") { return commitStages; } if (input.action === "commit_push") { return [...commitStages, pushStage]; } return [...commitStages, pushStage, "Creating PR..."];} const withDescription = (title: string, description: string | undefined) => description ? { title, description } : { title }; export function summarizeGitResult(result: GitRunStackedActionResult): { title: string; description?: string;} { if (result.pr.status === "created" || result.pr.status === "opened_existing") { const prNumber = result.pr.number ? ` #${result.pr.number}` : ""; const title = `${result.pr.status === "created" ? "Created PR" : "Opened PR"}${prNumber}`; return withDescription(title, truncateText(result.pr.title)); } if (result.push.status === "pushed") { const shortSha = shortenSha(result.commit.commitSha); const branch = result.push.upstreamBranch ?? result.push.branch; const pushedCommitPart = shortSha ? ` ${shortSha}` : ""; const branchPart = branch ? ` to ${branch}` : ""; return withDescription( `Pushed${pushedCommitPart}${branchPart}`, truncateText(result.commit.subject), ); } if (result.commit.status === "created") { const shortSha = shortenSha(result.commit.commitSha); const title = shortSha ? `Committed ${shortSha}` : "Committed changes"; return withDescription(title, truncateText(result.commit.subject)); } return { title: "Done" };} export function buildMenuItems( gitStatus: GitStatusResult | null, isBusy: boolean,): GitActionMenuItem[] { if (!gitStatus) return []; const hasBranch = gitStatus.branch !== null; const hasChanges = gitStatus.hasWorkingTreeChanges; const hasOpenPr = gitStatus.pr?.state === "open"; const isBehind = gitStatus.behindCount > 0; const canCommit = !isBusy && hasChanges; const canPush = !isBusy && hasBranch && !hasChanges && !isBehind && gitStatus.aheadCount > 0; const canCreatePr = !isBusy && hasBranch && !hasChanges && !hasOpenPr && gitStatus.aheadCount > 0 && !isBehind; 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: "Open 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: GitStatusResult | null, isBusy: boolean, isDefaultBranch = false,): 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.branch !== 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 (hasOpenPr || isDefaultBranch) { return { label: "Commit & push", disabled: false, kind: "run_action", action: "commit_push" }; } return { label: "Commit, push & create PR", disabled: false, kind: "run_action", action: "commit_push_pr", }; } if (!gitStatus.hasUpstream) { if (!isAhead) { if (hasOpenPr) { return { label: "Open 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: "commit_push" }; } return { label: "Push & create PR", disabled: false, kind: "run_action", action: "commit_push_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: "commit_push" }; } return { label: "Push & create PR", disabled: false, kind: "run_action", action: "commit_push_pr", }; } if (hasOpenPr && gitStatus.hasUpstream) { return { label: "Open 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 requiresDefaultBranchConfirmation( action: GitStackedAction, isDefaultBranch: boolean,): boolean { if (!isDefaultBranch) return false; return 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 === "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", };} function formatDateForBranch(now: Date): string { const year = now.getUTCFullYear(); const month = String(now.getUTCMonth() + 1).padStart(2, "0"); const day = String(now.getUTCDate()).padStart(2, "0"); return `${year}${month}${day}`;} export function resolveAutoFeatureBranchName( existingBranchNames: readonly string[], now = new Date(),): string { const daySlug = formatDateForBranch(now); const base = `${AUTO_FEATURE_BRANCH_PREFIX}-${daySlug}`; const existingNames = new Set(existingBranchNames); if (!existingNames.has(base)) { return base; } let suffix = 2; while (existingNames.has(`${base}-${suffix}`)) { suffix += 1; } return `${base}-${suffix}`;}
import type {
GitRunStackedActionResult,
GitStackedAction,
GitStatusResult,
} from "@t3tools/contracts";
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 = "commit_push" | "commit_push_pr";
const SHORT_SHA_LENGTH = 7;
const TOAST_DESCRIPTION_MAX = 72;
const AUTO_FEATURE_BRANCH_PREFIX = "feature/stacked";
function shortenSha(sha: string | undefined): string | null {
if (!sha) return null;
return sha.slice(0, SHORT_SHA_LENGTH);
}
function truncateText(
value: string | undefined,
maxLength = TOAST_DESCRIPTION_MAX,
): string | undefined {
if (!value) return undefined;
if (value.length <= maxLength) return value;
if (maxLength <= 3) return "...".slice(0, maxLength);
return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
}
export function buildGitActionProgressStages(input: {
action: GitStackedAction;
hasCustomCommitMessage: boolean;
hasWorkingTreeChanges: boolean;
forcePushOnly?: boolean;
pushTarget?: string;
}): string[] {
const shouldIncludeCommitStages =
!input.forcePushOnly && (input.action === "commit" || input.hasWorkingTreeChanges);
const commitStages = !shouldIncludeCommitStages
? []
: input.hasCustomCommitMessage
? ["Committing..."]
: ["Generating commit message...", "Committing..."];
const pushStage = input.pushTarget ? `Pushing to ${input.pushTarget}...` : "Pushing...";
if (input.action === "commit") {
return commitStages;
}
if (input.action === "commit_push") {
return [...commitStages, pushStage];
}
return [...commitStages, pushStage, "Creating PR..."];
}
const withDescription = (title: string, description: string | undefined) =>
description ? { title, description } : { title };
export function summarizeGitResult(result: GitRunStackedActionResult): {
title: string;
description?: string;
} {
if (result.pr.status === "created" || result.pr.status === "opened_existing") {
const prNumber = result.pr.number ? ` #${result.pr.number}` : "";
const title = `${result.pr.status === "created" ? "Created PR" : "Opened PR"}${prNumber}`;
return withDescription(title, truncateText(result.pr.title));
}
if (result.push.status === "pushed") {
const shortSha = shortenSha(result.commit.commitSha);
const branch = result.push.upstreamBranch ?? result.push.branch;
const pushedCommitPart = shortSha ? ` ${shortSha}` : "";
const branchPart = branch ? ` to ${branch}` : "";
return withDescription(
`Pushed${pushedCommitPart}${branchPart}`,
truncateText(result.commit.subject),
);
}
if (result.commit.status === "created") {
const shortSha = shortenSha(result.commit.commitSha);
const title = shortSha ? `Committed ${shortSha}` : "Committed changes";
return withDescription(title, truncateText(result.commit.subject));
}
return { title: "Done" };
}
export function buildMenuItems(
gitStatus: GitStatusResult | null,
isBusy: boolean,
): GitActionMenuItem[] {
if (!gitStatus) return [];
const hasBranch = gitStatus.branch !== null;
const hasChanges = gitStatus.hasWorkingTreeChanges;
const hasOpenPr = gitStatus.pr?.state === "open";
const isBehind = gitStatus.behindCount > 0;
const canCommit = !isBusy && hasChanges;
const canPush = !isBusy && hasBranch && !hasChanges && !isBehind && gitStatus.aheadCount > 0;
const canCreatePr =
!isBusy &&
hasBranch &&
!hasChanges &&
!hasOpenPr &&
gitStatus.aheadCount > 0 &&
!isBehind;
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: "Open 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: GitStatusResult | null,
isBusy: boolean,
isDefaultBranch = false,
): 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.branch !== 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 (hasOpenPr || isDefaultBranch) {
return { label: "Commit & push", disabled: false, kind: "run_action", action: "commit_push" };
}
return {
label: "Commit, push & create PR",
disabled: false,
kind: "run_action",
action: "commit_push_pr",
};
}
if (!gitStatus.hasUpstream) {
if (!isAhead) {
if (hasOpenPr) {
return { label: "Open 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: "commit_push" };
}
return {
label: "Push & create PR",
disabled: false,
kind: "run_action",
action: "commit_push_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: "commit_push" };
}
return {
label: "Push & create PR",
disabled: false,
kind: "run_action",
action: "commit_push_pr",
};
}
if (hasOpenPr && gitStatus.hasUpstream) {
return { label: "Open 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 requiresDefaultBranchConfirmation(
action: GitStackedAction,
isDefaultBranch: boolean,
): boolean {
if (!isDefaultBranch) return false;
return 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 === "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",
};
}
function formatDateForBranch(now: Date): string {
const year = now.getUTCFullYear();
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
const day = String(now.getUTCDate()).padStart(2, "0");
return `${year}${month}${day}`;
}
export function resolveAutoFeatureBranchName(
existingBranchNames: readonly string[],
now = new Date(),
): string {
const daySlug = formatDateForBranch(now);
const base = `${AUTO_FEATURE_BRANCH_PREFIX}-${daySlug}`;
const existingNames = new Set(existingBranchNames);
if (!existingNames.has(base)) {
return base;
}
let suffix = 2;
while (existingNames.has(`${base}-${suffix}`)) {
suffix += 1;
}
return `${base}-${suffix}`;
}