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}