import type { GitRunStackedActionResult, GitStackedAction, VcsStatusResult, } from "@t3tools/contracts"; import { isTemporaryWorktreeBranch } from "@t3tools/shared/git"; import { DEFAULT_CHANGE_REQUEST_TERMINOLOGY, getChangeRequestTerminology, type ChangeRequestTerminology, } from "../sourceControlPresentation"; 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" | "open_publish" | "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"; function resolveChangeRequestTerminology( gitStatus: VcsStatusResult | null, ): ChangeRequestTerminology { return gitStatus?.sourceControlProvider ? getChangeRequestTerminology(gitStatus.sourceControlProvider) : DEFAULT_CHANGE_REQUEST_TERMINOLOGY; } export function buildGitActionProgressStages(input: { action: GitStackedAction; hasCustomCommitMessage: boolean; hasWorkingTreeChanges: boolean; pushTarget?: string; featureBranch?: boolean; shouldPushBeforePr?: boolean; terminology?: ChangeRequestTerminology; }): string[] { const terminology = input.terminology ?? DEFAULT_CHANGE_REQUEST_TERMINOLOGY; const branchStages = input.featureBranch ? ["Preparing feature ref..."] : []; const pushStage = input.pushTarget ? `Pushing to ${input.pushTarget}...` : "Pushing..."; const prStages = [ `Preparing ${terminology.shortLabel}...`, `Generating ${terminology.shortLabel} content...`, `Creating ${terminology.singular}...`, ]; 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, hasPrimaryRemote = true, ): GitActionMenuItem[] { if (!gitStatus) return []; const terminology = resolveChangeRequestTerminology(gitStatus); const hasBranch = gitStatus.refName !== null; const hasChanges = gitStatus.hasWorkingTreeChanges; const hasOpenPr = gitStatus.pr?.state === "open"; const isBehind = gitStatus.behindCount > 0; const hasDefaultBranchDelta = (gitStatus.aheadOfDefaultCount ?? gitStatus.aheadCount) > 0; const canPushWithoutUpstream = hasPrimaryRemote && !gitStatus.hasUpstream; const canCommit = !isBusy && hasChanges; const canPush = !isBusy && hasBranch && !isBehind && gitStatus.aheadCount > 0 && (gitStatus.hasUpstream || canPushWithoutUpstream); const canCreatePr = !isBusy && hasBranch && !hasChanges && !hasOpenPr && hasDefaultBranchDelta && !isBehind && (gitStatus.hasUpstream || canPushWithoutUpstream); const canOpenPr = !isBusy && hasOpenPr; const commitItem: GitActionMenuItem = { id: "commit", label: "Commit", disabled: !canCommit, icon: "commit", kind: "open_dialog", dialogAction: "commit", }; if (!hasPrimaryRemote) { return [commitItem]; } return [ commitItem, { id: "push", label: "Push", disabled: !canPush, icon: "push", kind: "open_dialog", dialogAction: "push", }, hasOpenPr ? { id: "pr", label: `View ${terminology.shortLabel}`, disabled: !canOpenPr, icon: "pr", kind: "open_pr", } : { id: "pr", label: `Create ${terminology.shortLabel}`, disabled: !canCreatePr, icon: "pr", kind: "open_dialog", dialogAction: "create_pr", }, ]; } export function resolveQuickAction( gitStatus: VcsStatusResult | null, isBusy: boolean, isDefaultRef = false, hasPrimaryRemote = 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 hasDefaultBranchDelta = (gitStatus.aheadOfDefaultCount ?? gitStatus.aheadCount) > 0; const isBehind = gitStatus.behindCount > 0; const isDiverged = isAhead && isBehind; const terminology = resolveChangeRequestTerminology(gitStatus); if (!hasBranch) { return { label: "Commit", disabled: true, kind: "show_hint", hint: `Create and checkout a ref before pushing or opening a ${terminology.singular}.`, }; } if (hasChanges) { if (!gitStatus.hasUpstream && !hasPrimaryRemote) { return { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; } if (hasOpenPr || isDefaultRef) { return { label: "Commit & push", disabled: false, kind: "run_action", action: "commit_push" }; } return { label: `Commit, push & ${terminology.shortLabel}`, disabled: false, kind: "run_action", action: "commit_push_pr", }; } if (!gitStatus.hasUpstream) { if (!hasPrimaryRemote) { if (hasOpenPr && !isAhead) { return { label: `View ${terminology.shortLabel}`, disabled: false, kind: "open_pr" }; } return { label: "Publish repository", disabled: false, kind: "open_publish", }; } if (!isAhead) { if (hasOpenPr) { return { label: `View ${terminology.shortLabel}`, disabled: false, kind: "open_pr" }; } return { label: "Push", disabled: true, kind: "show_hint", hint: "No local commits to push.", }; } if (hasOpenPr || isDefaultRef) { return { label: "Push", disabled: false, kind: "run_action", action: isDefaultRef ? "commit_push" : "push", }; } return { label: `Push & create ${terminology.shortLabel}`, disabled: false, kind: "run_action", action: "create_pr", }; } if (isDiverged) { return { label: "Sync ref", 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 || isDefaultRef) { return { label: "Push", disabled: false, kind: "run_action", action: isDefaultRef ? "commit_push" : "push", }; } return { label: `Push & create ${terminology.shortLabel}`, disabled: false, kind: "run_action", action: "create_pr", }; } if (hasOpenPr && gitStatus.hasUpstream) { return { label: `View ${terminology.shortLabel}`, disabled: false, kind: "open_pr" }; } if (hasDefaultBranchDelta && !isDefaultRef) { return { label: `Create ${terminology.shortLabel}`, disabled: false, kind: "run_action", action: "create_pr", }; } return { label: "Commit", disabled: true, kind: "show_hint", hint: "Branch is up to date. No action needed.", }; } export function requiresDefaultBranchConfirmation( action: GitStackedAction, isDefaultRef: boolean, ): boolean { if (!isDefaultRef) 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; terminology?: ChangeRequestTerminology; }): DefaultBranchActionDialogCopy { const branchLabel = input.branchName; const suffix = ` on "${branchLabel}". You can continue on this ref or create a feature ref and run the same action there.`; const terminology = input.terminology ?? DEFAULT_CHANGE_REQUEST_TERMINOLOGY; if (input.action === "push" || input.action === "commit_push") { if (input.includesCommit) { return { title: "Commit & push to default ref?", description: `This action will commit and push changes${suffix}`, continueLabel: `Commit & push to ${branchLabel}`, }; } return { title: "Push to default ref?", description: `This action will push local commits${suffix}`, continueLabel: `Push to ${branchLabel}`, }; } if (input.includesCommit) { return { title: `Commit, push & create ${terminology.shortLabel} from default ref?`, description: `This action will commit, push, and create a ${terminology.singular}${suffix}`, continueLabel: `Commit, push & create ${terminology.shortLabel}`, }; } return { title: `Push & create ${terminology.shortLabel} from default ref?`, description: `This action will push local commits and create a ${terminology.singular}${suffix}`, continueLabel: `Push & create ${terminology.shortLabel}`, }; } 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 resolveThreadBranchMetadataPatch( branch: string | null, expectedBranch: string | null, ): { branch: string | null; expectedBranch: string | null; } { return { branch, expectedBranch }; } 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, }; } // Re-export from shared for backwards compatibility in this module's exports export { resolveAutoFeatureBranchName } from "@t3tools/shared/git";