v0.2.0: Git-native architecture (#1)
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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;
|
||||
|
||||
constructor(projectPath: string) {
|
||||
this.projectPath = projectPath;
|
||||
this.gitContext = new GitContext(projectPath);
|
||||
}
|
||||
|
||||
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 branchName = `phase/${padded}-${slug}`;
|
||||
|
||||
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 branchName = `milestone/${version}-${slug}`;
|
||||
|
||||
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 !== "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user