diff --git a/src/application/ReferenceSourceVcsAdapter.ts b/src/application/ReferenceSourceVcsAdapter.ts index 8b0d1466be..42ab4e7388 100644 --- a/src/application/ReferenceSourceVcsAdapter.ts +++ b/src/application/ReferenceSourceVcsAdapter.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; -import { lstat } from "node:fs/promises"; -import { join } from "node:path"; +import { lstat, readFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; import { resolveRef } from "isomorphic-git"; @@ -31,7 +31,7 @@ export const readReferenceSourceVcs = async ( : { kind: "unknown", head: null, dirty: null }; } try { - const head = await resolveRef({ fs, dir: root, ref: "HEAD" }); + const head = await resolveSourceHead(root); if (isAborted(signal)) return { kind: "unknown", head: null, dirty: null }; return { kind: "git", head, dirty: null }; } catch (cause: unknown) { @@ -41,6 +41,42 @@ export const readReferenceSourceVcs = async ( } }; +const resolveSourceHead = async (root: string): Promise => { + const dotgit = join(root, ".git"); + if (!(await lstat(dotgit)).isFile()) + return resolveRef({ fs, dir: root, ref: "HEAD" }); + const pointer = (await readFile(dotgit, "utf8")).replace(/\r?\n$/u, ""); + if (!pointer.startsWith("gitdir: ") || pointer.length === 8) + throw new Error("Invalid Git directory pointer"); + const gitdir = resolve(root, pointer.slice(8)); + let commonDirectory: string; + try { + commonDirectory = resolve( + gitdir, + (await readFile(join(gitdir, "commondir"), "utf8")).replace( + /\r?\n$/u, + "", + ), + ); + } catch (cause: unknown) { + if (errorCode(cause) !== "ENOENT") throw cause; + return resolveRef({ fs, dir: root, gitdir, ref: "HEAD" }); + } + const head = (await readFile(join(gitdir, "HEAD"), "utf8")).trim(); + if (!head.startsWith("ref: ")) + return resolveRef({ fs, dir: root, gitdir, ref: "HEAD" }); + const ref = head.slice(5); + const privateRef = ["refs/bisect/", "refs/rewritten/", "refs/worktree/"].some( + (prefix) => ref.startsWith(prefix), + ); + return resolveRef({ + fs, + dir: root, + gitdir: privateRef ? gitdir : commonDirectory, + ref, + }); +}; + const errorCode = (cause: unknown): string | undefined => typeof cause === "object" && cause !== null && "code" in cause ? String(cause.code) diff --git a/tests/boundary/filesystem/referenceSourceVcs.test.ts b/tests/boundary/filesystem/referenceSourceVcs.test.ts new file mode 100644 index 0000000000..ad04133bfb --- /dev/null +++ b/tests/boundary/filesystem/referenceSourceVcs.test.ts @@ -0,0 +1,94 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { execFileOutput } from "../../../src/process/ExecFileOutput.js"; +import { readReferenceSourceVcs } from "../../../src/application/ReferenceSourceVcsAdapter.js"; + +it("reads a linked worktree's own branch and detached HEAD through shared Git refs", async () => { + const root = await createTestTempDirectory("rea-reference-worktree-"); + const repository = join(root, "repository"); + await mkdir(repository); + const git = (...args: string[]) => + execFileOutput("git", args, { cwd: repository }); + await git("init", "--initial-branch=main"); + const commit = () => + git( + "-c", + "user.name=REA fixture", + "-c", + "user.email=fixture@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-m", + "fixture", + ); + await commit(); + const mainHead = (await git("rev-parse", "HEAD")).stdout.trim(); + const linked = join(root, "linked"); + await git("worktree", "add", "-b", "linked", linked); + await execFileOutput( + "git", + [ + "-c", + "user.name=REA fixture", + "-c", + "user.email=fixture@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-m", + "linked fixture", + ], + { cwd: linked }, + ); + const linkedHead = ( + await execFileOutput("git", ["rev-parse", "HEAD"], { cwd: linked }) + ).stdout.trim(); + expect(linkedHead).not.toBe(mainHead); + expect(await readReferenceSourceVcs(repository)).toEqual({ + kind: "git", + head: mainHead, + dirty: null, + }); + expect(await readReferenceSourceVcs(linked)).toEqual({ + kind: "git", + head: linkedHead, + dirty: null, + }); + await git("pack-refs", "--all", "--prune"); + expect(await readReferenceSourceVcs(linked)).toEqual({ + kind: "git", + head: linkedHead, + dirty: null, + }); + await execFileOutput("git", ["checkout", "--detach", mainHead], { + cwd: linked, + }); + expect(await readReferenceSourceVcs(linked)).toEqual({ + kind: "git", + head: mainHead, + dirty: null, + }); + const relativePointer = join(root, "relative"); + await mkdir(relativePointer); + await writeFile( + join(relativePointer, ".git"), + "gitdir: ../repository/.git\r\n", + ); + expect(await readReferenceSourceVcs(relativePointer)).toEqual({ + kind: "git", + head: mainHead, + dirty: null, + }); + const controller = new AbortController(); + controller.abort(); + expect(await readReferenceSourceVcs(linked, controller.signal)).toEqual({ + kind: "unknown", + head: null, + dirty: null, + }); +});