import type { GitRunStackedActionInput, GitStackedAction, VcsStatusResult, } 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 = | "push" | "create_pr" | "commit_push" | "commit_push_pr"; export type GitActionRequestInput = Pick< GitRunStackedActionInput, "action" | "commitMessage" | "featureBranch" | "filePaths" >; 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", }; }