flake-update-20260505
  1// =============================================================================
  2// Git Utilities
  3// =============================================================================
  4
  5import { execSync } from "child_process";
  6import * as path from "path";
  7import * as fs from "fs";
  8
  9export function findRepoRoot(cwd: string = process.cwd()): string | null {
 10	try {
 11		return execSync("git rev-parse --show-toplevel", {
 12			cwd,
 13			encoding: "utf-8",
 14		}).trim();
 15	} catch {
 16		return null;
 17	}
 18}
 19
 20export function getCurrentBranch(cwd: string = process.cwd()): string | null {
 21	try {
 22		return execSync("git rev-parse --abbrev-ref HEAD", {
 23			cwd,
 24			encoding: "utf-8",
 25		}).trim();
 26	} catch {
 27		return null;
 28	}
 29}
 30
 31export function getCommitHash(cwd: string = process.cwd()): string | null {
 32	try {
 33		return execSync("git rev-parse HEAD", {
 34			cwd,
 35			encoding: "utf-8",
 36		}).trim();
 37	} catch {
 38		return null;
 39	}
 40}
 41
 42export function isWorktree(cwd: string = process.cwd()): boolean {
 43	try {
 44		const gitDir = execSync("git rev-parse --git-dir", {
 45			cwd,
 46			encoding: "utf-8",
 47		}).trim();
 48		return gitDir.includes("/worktrees/");
 49	} catch {
 50		return false;
 51	}
 52}
 53
 54export function hasUncommittedChanges(cwd: string = process.cwd()): boolean {
 55	try {
 56		const status = execSync("git status --porcelain", {
 57			cwd,
 58			encoding: "utf-8",
 59		}).trim();
 60		return status.length > 0;
 61	} catch {
 62		return false;
 63	}
 64}
 65
 66export function sanitizeBranchName(branch: string): string {
 67	// Remove remote prefix if present (origin/feature -> feature)
 68	const withoutRemote = branch.replace(/^[^/]+\//, "");
 69	// Replace invalid characters with hyphens
 70	return withoutRemote.replace(/[^a-zA-Z0-9-_]/g, "-");
 71}
 72
 73export function branchExists(branch: string, cwd: string = process.cwd()): boolean {
 74	try {
 75		execSync(`git show-ref --verify refs/heads/${branch}`, {
 76			cwd,
 77			encoding: "utf-8",
 78		});
 79		return true;
 80	} catch {
 81		return false;
 82	}
 83}
 84
 85export function remoteBranchExists(remote: string, branch: string, cwd: string = process.cwd()): boolean {
 86	try {
 87		execSync(`git show-ref --verify refs/remotes/${remote}/${branch}`, {
 88			cwd,
 89			encoding: "utf-8",
 90		});
 91		return true;
 92	} catch {
 93		return false;
 94	}
 95}
 96
 97export function getRemoteOrgAndRepo(cwd: string = process.cwd()): { org: string; repo: string } | null {
 98	try {
 99		const remoteUrl = execSync("git remote get-url origin", {
100			cwd,
101			encoding: "utf-8",
102		}).trim();
103
104		// Parse org/repo from various URL formats
105		// SSH: git@github.com:tektoncd/pipeline.git
106		// HTTPS: https://github.com/tektoncd/pipeline.git
107		// Local: /path/to/repo.git or kerkouane.vpn:git/public/home.git
108
109		let org: string | undefined;
110		let repo: string | undefined;
111
112		// SSH format: git@host:org/repo.git
113		const sshMatch = remoteUrl.match(/git@[^:]+:([^/]+)\/([^/.]+)/);
114		if (sshMatch) {
115			org = sshMatch[1];
116			repo = sshMatch[2];
117		}
118
119		// HTTPS format: https://host/org/repo.git
120		const httpsMatch = remoteUrl.match(/https:\/\/[^/]+\/([^/]+)\/([^/.]+)/);
121		if (httpsMatch) {
122			org = httpsMatch[1];
123			repo = httpsMatch[2];
124		}
125
126		// Local SSH without git@: host:path/org/repo.git
127		const localSshMatch = remoteUrl.match(/[^@]+:(?:.*\/)?([^/]+)\/([^/.]+)/);
128		if (localSshMatch && !org) {
129			org = localSshMatch[1];
130			repo = localSshMatch[2];
131		}
132
133		if (!org || !repo) {
134			return null;
135		}
136
137		// Remove .git suffix if present
138		repo = repo.replace(/\.git$/, "");
139
140		return { org, repo };
141	} catch {
142		return null;
143	}
144}
145
146export function getWorktreeBaseDir(): string {
147	// XDG data directory: ~/.local/share/worktrees/
148	const home = process.env.HOME || process.env.USERPROFILE || "";
149	return path.join(home, ".local", "share", "worktrees");
150}
151
152export function getWorktreeDir(cwd: string = process.cwd()): string | null {
153	const orgRepo = getRemoteOrgAndRepo(cwd);
154	if (!orgRepo) {
155		return null;
156	}
157
158	return path.join(getWorktreeBaseDir(), orgRepo.org, orgRepo.repo);
159}
160
161export function ensureWorktreeDir(cwd: string = process.cwd()): void {
162	const worktreeDir = getWorktreeDir(cwd);
163	if (worktreeDir && !fs.existsSync(worktreeDir)) {
164		fs.mkdirSync(worktreeDir, { recursive: true });
165	}
166}