|
| 1 | +import { execFile } from "node:child_process"; |
1 | 2 | import * as fs from "node:fs"; |
2 | 3 | import * as fsPromises from "node:fs/promises"; |
3 | 4 | import path from "node:path"; |
| 5 | +import { promisify } from "node:util"; |
4 | 6 | import { createGitClient } from "@posthog/git/client"; |
5 | 7 | import { |
6 | 8 | getCurrentBranch, |
7 | 9 | getDefaultBranch, |
8 | 10 | hasTrackedFiles, |
| 11 | + listWorktrees, |
9 | 12 | } from "@posthog/git/queries"; |
10 | 13 | import { CreateOrSwitchBranchSaga } from "@posthog/git/sagas/branch"; |
11 | 14 | import { DetachHeadSaga } from "@posthog/git/sagas/head"; |
@@ -44,6 +47,8 @@ import type { |
44 | 47 | import { ScriptRunner } from "./scriptRunner"; |
45 | 48 | import { buildWorkspaceEnv } from "./workspaceEnv"; |
46 | 49 |
|
| 50 | +const execFileAsync = promisify(execFile); |
| 51 | + |
47 | 52 | type TaskAssociation = |
48 | 53 | | { taskId: string; folderId: string; mode: "local" } |
49 | 54 | | { taskId: string; folderId: string | null; mode: "cloud" } |
@@ -1197,6 +1202,70 @@ export class WorkspaceService extends TypedEventEmitter<WorkspaceServiceEvents> |
1197 | 1202 | return result; |
1198 | 1203 | } |
1199 | 1204 |
|
| 1205 | + async listGitWorktrees(mainRepoPath: string): Promise< |
| 1206 | + Array<{ |
| 1207 | + worktreePath: string; |
| 1208 | + head: string; |
| 1209 | + branch: string | null; |
| 1210 | + taskIds: string[]; |
| 1211 | + }> |
| 1212 | + > { |
| 1213 | + const worktreeBasePath = getWorktreeLocation(); |
| 1214 | + const rawWorktrees = await listWorktrees(mainRepoPath); |
| 1215 | + |
| 1216 | + const twigWorktrees = rawWorktrees.filter((wt) => { |
| 1217 | + const isMainRepo = path.resolve(wt.path) === path.resolve(mainRepoPath); |
| 1218 | + const isUnderTwig = path |
| 1219 | + .resolve(wt.path) |
| 1220 | + .startsWith(path.resolve(worktreeBasePath)); |
| 1221 | + return !isMainRepo && isUnderTwig; |
| 1222 | + }); |
| 1223 | + |
| 1224 | + return twigWorktrees.map((wt) => { |
| 1225 | + const taskIds = this.getWorktreeTasks(wt.path).map((t) => t.taskId); |
| 1226 | + return { |
| 1227 | + worktreePath: wt.path, |
| 1228 | + head: wt.head, |
| 1229 | + branch: wt.branch, |
| 1230 | + taskIds, |
| 1231 | + }; |
| 1232 | + }); |
| 1233 | + } |
| 1234 | + |
| 1235 | + async getWorktreeSize(worktreePath: string): Promise<{ sizeBytes: number }> { |
| 1236 | + try { |
| 1237 | + const { stdout } = await execFileAsync("du", ["-s", worktreePath]); |
| 1238 | + const [sizeStr] = stdout.trim().split("\t"); |
| 1239 | + const sizeBytes = sizeStr ? parseInt(sizeStr, 10) * 512 : 0; |
| 1240 | + return { sizeBytes }; |
| 1241 | + } catch (error) { |
| 1242 | + log.warn(`Failed to get size for ${worktreePath}:`, error); |
| 1243 | + return { sizeBytes: 0 }; |
| 1244 | + } |
| 1245 | + } |
| 1246 | + |
| 1247 | + async deleteWorktree( |
| 1248 | + mainRepoPath: string, |
| 1249 | + worktreePath: string, |
| 1250 | + ): Promise<void> { |
| 1251 | + const worktree = this.worktreeRepo.findByPath(worktreePath); |
| 1252 | + if (worktree) { |
| 1253 | + const workspace = this.workspaceRepo.findById(worktree.workspaceId); |
| 1254 | + if (workspace) { |
| 1255 | + await this.deleteWorkspace(workspace.taskId, mainRepoPath); |
| 1256 | + return; |
| 1257 | + } |
| 1258 | + } |
| 1259 | + |
| 1260 | + const worktreeBasePath = getWorktreeLocation(); |
| 1261 | + const manager = new WorktreeManager({ mainRepoPath, worktreeBasePath }); |
| 1262 | + await manager.deleteWorktree(worktreePath); |
| 1263 | + |
| 1264 | + if (worktree) { |
| 1265 | + this.worktreeRepo.deleteByWorkspaceId(worktree.workspaceId); |
| 1266 | + } |
| 1267 | + } |
| 1268 | + |
1200 | 1269 | private async cleanupWorktree( |
1201 | 1270 | taskId: string, |
1202 | 1271 | mainRepoPath: string, |
|
0 commit comments