import { execSync } from "node:child_process"; import { GitContext, BranchInfo } from "./git-context.js"; export interface BranchCreateResult { name: string; created: boolean; alreadyExisted: boolean; } export interface BranchMergeResult { success: boolean; squash: boolean; message: string; } export interface PhaseBranchInfo { phaseNumber: number; slug: string; branchName: string; status: "active" | "complete" | "unknown"; } export interface MilestoneBranchInfo { version: string; slug: string; branchName: string; status: "active" | "complete" | "unknown"; } export class GitBranch { private projectPath: string; private gitContext: GitContext; private projectSlug?: string; constructor(projectPath: string, projectSlug?: string) { this.projectPath = projectPath; this.projectSlug = projectSlug; this.gitContext = new GitContext(projectPath, projectSlug); } setProjectSlug(slug: string | undefined): void { this.projectSlug = slug; this.gitContext.setProjectSlug(slug); } private prefix(name: string): string { return this.projectSlug ? `${this.projectSlug}/${name}` : name; } private git(args: string): string { try { return execSync(`git ${args}`, { cwd: this.projectPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }).trim(); } catch { return ""; } } private slugify(name: string): string { return name .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } createPhaseBranch(phaseNumber: number, phaseName: string): BranchCreateResult { const padded = String(phaseNumber).padStart(2, "0"); const slug = this.slugify(phaseName); const baseName = `phase/${padded}-${slug}`; const branchName = this.prefix(baseName); const existing = this.gitContext.getBranches().find((b) => b.name === branchName); if (existing) { return { name: branchName, created: false, alreadyExisted: true }; } const result = this.git(`checkout -b ${branchName}`); if (!result && this.git(`checkout ${branchName}`)) { return { name: branchName, created: false, alreadyExisted: true }; } return { name: branchName, created: true, alreadyExisted: false }; } createMilestoneBranch(version: string, milestoneName: string): BranchCreateResult { const slug = this.slugify(milestoneName); const baseName = `milestone/${version}-${slug}`; const branchName = this.prefix(baseName); const existing = this.gitContext.getBranches().find((b) => b.name === branchName); if (existing) { return { name: branchName, created: false, alreadyExisted: true }; } const result = this.git(`checkout -b ${branchName}`); if (!result && this.git(`checkout ${branchName}`)) { return { name: branchName, created: false, alreadyExisted: true }; } return { name: branchName, created: true, alreadyExisted: false }; } mergePhaseBranch( phaseBranchName: string, targetBranch: string, squash: boolean = true ): BranchMergeResult { const branches = this.gitContext.getBranches(); const phaseBranch = branches.find((b) => b.name === phaseBranchName); if (!phaseBranch) { return { success: false, squash, message: `Branch ${phaseBranchName} not found` }; } this.git(`checkout ${targetBranch}`); const mergeCmd = squash ? `merge --squash ${phaseBranchName}` : `merge --no-ff ${phaseBranchName}`; const result = this.git(mergeCmd); if (result === "" && !squash) { return { success: false, squash, message: `Merge conflict on ${phaseBranchName}` }; } if (squash) { this.git(`commit -m "docs: merge phase branch ${phaseBranchName}"`); } return { success: true, squash, message: `Merged ${phaseBranchName} into ${targetBranch} (squash: ${squash})`, }; } getPhaseStatus(phaseNumber: number): PhaseBranchInfo | null { const branches = this.gitContext.getBranches(); const phaseBranch = branches.find( (b) => b.type === "phase" && b.phaseNumber === phaseNumber ); if (!phaseBranch) return null; const slug = phaseBranch.name.replace(/^phase\/\d+-/, ""); return { phaseNumber, slug, branchName: phaseBranch.name, status: phaseBranch.merged ? "complete" : "active", }; } getMilestoneStatus(version: string): MilestoneBranchInfo | null { const branches = this.gitContext.getBranches(); const milestoneBranch = branches.find( (b) => b.type === "milestone" && b.milestone?.startsWith(version) ); if (!milestoneBranch) return null; const slug = milestoneBranch.name.replace(/^milestone\//, ""); return { version, slug, branchName: milestoneBranch.name, status: milestoneBranch.merged ? "complete" : "active", }; } listPhases(): PhaseBranchInfo[] { const branches = this.gitContext.getPhaseBranches(); return branches.map((b) => ({ phaseNumber: b.phaseNumber || 0, slug: b.name.replace(/^phase\/\d+-/, ""), branchName: b.name, status: b.merged ? "complete" : "active" as const, })); } listMilestones(): MilestoneBranchInfo[] { const branches = this.gitContext.getMilestoneBranches(); return branches.map((b) => ({ version: b.milestone || "", slug: b.name.replace(/^milestone\//, ""), branchName: b.name, status: b.merged ? "complete" : "active" as const, })); } switchToBranch(branchName: string): boolean { const result = this.git(`checkout ${branchName}`); return result !== ""; } deleteBranch(branchName: string, force: boolean = false): boolean { const flag = force ? "-D" : "-d"; const result = this.git(`branch ${flag} ${branchName}`); return result !== ""; } }