diff --git a/src/hooks/start-work/worktree-detector.test.ts b/src/hooks/start-work/worktree-detector.test.ts new file mode 100644 index 000000000..b02d5af1b --- /dev/null +++ b/src/hooks/start-work/worktree-detector.test.ts @@ -0,0 +1,79 @@ +/// + +import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test" +import * as childProcess from "node:child_process" +import { detectWorktreePath } from "./worktree-detector" + +describe("detectWorktreePath", () => { + let execFileSyncSpy: ReturnType + + beforeEach(() => { + execFileSyncSpy = spyOn(childProcess, "execFileSync").mockImplementation( + ((_file: string, _args: string[]) => "") as typeof childProcess.execFileSync, + ) + }) + + afterEach(() => { + execFileSyncSpy.mockRestore() + }) + + describe("when directory is a valid git worktree", () => { + test("#given valid git dir #when detecting #then returns worktree root path", () => { + execFileSyncSpy.mockImplementation( + ((_file: string, _args: string[]) => "/home/user/my-repo\n") as typeof childProcess.execFileSync, + ) + + // when + const result = detectWorktreePath("/home/user/my-repo/src") + + // then + expect(result).toBe("/home/user/my-repo") + }) + + test("#given git output with trailing newline #when detecting #then trims output", () => { + execFileSyncSpy.mockImplementation( + ((_file: string, _args: string[]) => "/projects/worktree-a\n\n") as typeof childProcess.execFileSync, + ) + + const result = detectWorktreePath("/projects/worktree-a") + + expect(result).toBe("/projects/worktree-a") + }) + + test("#given valid dir #when detecting #then calls git rev-parse with cwd", () => { + execFileSyncSpy.mockImplementation( + ((_file: string, _args: string[]) => "/repo\n") as typeof childProcess.execFileSync, + ) + + detectWorktreePath("/repo/some/subdir") + + expect(execFileSyncSpy).toHaveBeenCalledWith( + "git", + ["rev-parse", "--show-toplevel"], + expect.objectContaining({ cwd: "/repo/some/subdir" }), + ) + }) + }) + + describe("when directory is not a git worktree", () => { + test("#given non-git directory #when detecting #then returns null", () => { + execFileSyncSpy.mockImplementation((_file: string, _args: string[]) => { + throw new Error("not a git repository") + }) + + const result = detectWorktreePath("/tmp/not-a-repo") + + expect(result).toBeNull() + }) + + test("#given non-existent directory #when detecting #then returns null", () => { + execFileSyncSpy.mockImplementation((_file: string, _args: string[]) => { + throw new Error("ENOENT: no such file or directory") + }) + + const result = detectWorktreePath("/nonexistent/path") + + expect(result).toBeNull() + }) + }) +}) diff --git a/src/hooks/start-work/worktree-detector.ts b/src/hooks/start-work/worktree-detector.ts new file mode 100644 index 000000000..74c919593 --- /dev/null +++ b/src/hooks/start-work/worktree-detector.ts @@ -0,0 +1,14 @@ +import { execFileSync } from "node:child_process" + +export function detectWorktreePath(directory: string): string | null { + try { + return execFileSync("git", ["rev-parse", "--show-toplevel"], { + cwd: directory, + encoding: "utf-8", + timeout: 5000, + stdio: ["pipe", "pipe", "pipe"], + }).trim() + } catch { + return null + } +}