aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/git
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/git')
-rw-r--r--src/main/git/worktree.ts179
1 files changed, 179 insertions, 0 deletions
diff --git a/src/main/git/worktree.ts b/src/main/git/worktree.ts
new file mode 100644
index 0000000..3264e5e
--- /dev/null
+++ b/src/main/git/worktree.ts
@@ -0,0 +1,179 @@
1import { execSync } from "node:child_process";
2import fs from "node:fs";
3import path from "node:path";
4
5export interface GitWorktreeInfo {
6 path: string;
7 branch: string;
8 commit?: string;
9}
10
11/**
12 * Check if a directory is a git repository
13 */
14export function isGitRepo(dir: string): boolean {
15 try {
16 execSync("git rev-parse --git-dir", { cwd: dir, stdio: "pipe" });
17 return true;
18 } catch {
19 return false;
20 }
21}
22
23/**
24 * Get the default branch name (main or master)
25 */
26export function getDefaultBranch(dir: string): string {
27 try {
28 const branches = execSync("git branch -rl '*/HEAD'", { cwd: dir, stdio: "pipe", encoding: "utf-8" });
29 const match = branches.match(/origin\/(main|master)/);
30 return match?.[1] || "main";
31 } catch {
32 return "main";
33 }
34}
35
36/**
37 * Create a new worktree for a session
38 * Returns the worktree path
39 */
40export function createWorktree(projectPath: string, sessionId: string, baseBranch?: string): string {
41 if (!isGitRepo(projectPath)) {
42 throw new Error("Not a git repository");
43 }
44
45 const branchName = `claude-flow/${sessionId}`;
46 const worktreePath = path.join(projectPath, ".claude-flow", "worktrees", sessionId);
47
48 // Ensure parent directory exists
49 fs.mkdirSync(path.dirname(worktreePath), { recursive: true });
50
51 // Create branch from base (or current HEAD)
52 const base = baseBranch || getDefaultBranch(projectPath);
53
54 try {
55 // Create worktree with new branch
56 execSync(
57 `git worktree add -b "${branchName}" "${worktreePath}" ${base}`,
58 { cwd: projectPath, stdio: "pipe" }
59 );
60 } catch (error) {
61 // If branch already exists, just add worktree pointing to it
62 try {
63 execSync(
64 `git worktree add "${worktreePath}" "${branchName}"`,
65 { cwd: projectPath, stdio: "pipe" }
66 );
67 } catch {
68 throw new Error(`Failed to create worktree: ${error}`);
69 }
70 }
71
72 return worktreePath;
73}
74
75/**
76 * Remove a worktree
77 */
78export function removeWorktree(projectPath: string, sessionId: string): void {
79 const worktreePath = path.join(projectPath, ".claude-flow", "worktrees", sessionId);
80 const branchName = `claude-flow/${sessionId}`;
81
82 try {
83 // Remove worktree
84 execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectPath, stdio: "pipe" });
85 } catch {
86 // Worktree might not exist, that's ok
87 }
88
89 try {
90 // Delete branch
91 execSync(`git branch -D "${branchName}"`, { cwd: projectPath, stdio: "pipe" });
92 } catch {
93 // Branch might not exist, that's ok
94 }
95}
96
97/**
98 * Get worktree info for a session
99 */
100export function getWorktreeInfo(projectPath: string, sessionId: string): GitWorktreeInfo | null {
101 const worktreePath = path.join(projectPath, ".claude-flow", "worktrees", sessionId);
102
103 if (!fs.existsSync(worktreePath)) {
104 return null;
105 }
106
107 try {
108 const branch = execSync("git rev-parse --abbrev-ref HEAD", {
109 cwd: worktreePath,
110 encoding: "utf-8"
111 }).trim();
112
113 const commit = execSync("git rev-parse --short HEAD", {
114 cwd: worktreePath,
115 encoding: "utf-8"
116 }).trim();
117
118 return { path: worktreePath, branch, commit };
119 } catch {
120 return null;
121 }
122}
123
124/**
125 * Commit changes in a worktree
126 */
127export function commitChanges(
128 worktreePath: string,
129 message: string,
130 files: string[] = ["."]
131): void {
132 try {
133 // Stage files
134 execSync(`git add ${files.join(" ")}`, { cwd: worktreePath, stdio: "pipe" });
135
136 // Commit
137 execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: worktreePath, stdio: "pipe" });
138 } catch (error) {
139 throw new Error(`Failed to commit: ${error}`);
140 }
141}
142
143/**
144 * Check if there are uncommitted changes
145 */
146export function hasUncommittedChanges(worktreePath: string): boolean {
147 try {
148 const status = execSync("git status --porcelain", { cwd: worktreePath, encoding: "utf-8" });
149 return status.trim().length > 0;
150 } catch {
151 return false;
152 }
153}
154
155/**
156 * Get diff summary of uncommitted changes
157 */
158export function getDiffSummary(worktreePath: string): string {
159 try {
160 return execSync("git diff --stat", { cwd: worktreePath, encoding: "utf-8" });
161 } catch {
162 return "";
163 }
164}
165
166/**
167 * Get the main project path from a worktree path
168 */
169export function getMainRepoPath(worktreePath: string): string {
170 try {
171 const gitDir = execSync("git rev-parse --git-common-dir", {
172 cwd: worktreePath,
173 encoding: "utf-8"
174 }).trim();
175 return path.dirname(gitDir);
176 } catch {
177 return worktreePath;
178 }
179}