diff --git a/src/pwd.mjs b/src/pwd.mjs index 17cc68a..7cbb3b0 100644 --- a/src/pwd.mjs +++ b/src/pwd.mjs @@ -19,6 +19,20 @@ function resolveGitDir(dotgit, dir) { return null; } +// A linked worktree's git dir (.git/worktrees/) holds only that +// worktree's own HEAD and index — the shared config, and therefore the +// remotes, stay in the main repo's git dir. Git names it in a `commondir` +// file, usually as a path relative to the worktree's git dir. Resolve it so +// origin is found from inside a worktree; outside one there is no such file +// and the git dir is already the common one. +function commonGitDir(gitDir) { + try { + const pointer = fs.readFileSync(path.join(gitDir, "commondir"), "utf8").trim(); + if (pointer) return path.resolve(gitDir, pointer); + } catch { /* not a linked worktree */ } + return gitDir; +} + // Walk up from `start` until we find a `.git` with a readable HEAD (a real // repo). A bare `.git` dir with no HEAD — e.g. a stray /tmp/.git — is skipped, // not mistaken for a repo. Returns { root, gitDir } or null at the fs root. @@ -74,8 +88,9 @@ export function locate(cwd = process.cwd()) { git: { root: found.root, name: path.basename(found.root), + // HEAD is per-worktree; config (and its remotes) is shared. branch: readBranch(found.gitDir), - origin: readOrigin(found.gitDir), + origin: readOrigin(commonGitDir(found.gitDir)), }, }; } diff --git a/test/pwd.test.mjs b/test/pwd.test.mjs index 65806db..753a462 100644 --- a/test/pwd.test.mjs +++ b/test/pwd.test.mjs @@ -1,8 +1,10 @@ import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import test from "node:test"; -import { tilde } from "../src/pwd.mjs"; +import { locate, tilde } from "../src/pwd.mjs"; test("tilde shortens the home directory itself", () => { const home = path.join("C:", "Users", "mosh"); @@ -23,3 +25,51 @@ test("tilde does not shorten sibling paths with the same prefix", () => { assert.equal(tilde(sibling, home), sibling); }); + +// Build a main repo plus one linked worktree the way git lays them out on disk, +// without needing git on PATH: the worktree's `.git` is a file pointing at +// .git/worktrees/, which carries its own HEAD and a `commondir` back to +// the main git dir that holds the shared config. +function fakeWorktree(root, { commondir }) { + const mainGit = path.join(root, "main", ".git"); + const linkedGit = path.join(mainGit, "worktrees", "feature"); + fs.mkdirSync(linkedGit, { recursive: true }); + fs.writeFileSync(path.join(mainGit, "HEAD"), "ref: refs/heads/main\n"); + fs.writeFileSync( + path.join(mainGit, "config"), + '[core]\n\tbare = false\n[remote "origin"]\n\turl = https://github.com/moshcoder/moshcode.git\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n', + ); + fs.writeFileSync(path.join(linkedGit, "HEAD"), "ref: refs/heads/feature\n"); + fs.writeFileSync(path.join(linkedGit, "commondir"), `${commondir}\n`); + + const worktree = path.join(root, "feature"); + fs.mkdirSync(worktree, { recursive: true }); + fs.writeFileSync(path.join(worktree, ".git"), `gitdir: ${linkedGit}\n`); + return worktree; +} + +test("locate reports origin from inside a linked worktree", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "moshcode-worktree-")); + try { + // git writes commondir relative to the worktree's own git dir. + const worktree = fakeWorktree(root, { commondir: path.join("..", "..") }); + const { git } = locate(worktree); + + assert.equal(git.branch, "feature"); + assert.equal(git.origin, "https://github.com/moshcoder/moshcode.git"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("locate follows an absolute commondir too", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "moshcode-worktree-")); + try { + const worktree = fakeWorktree(root, { commondir: path.join(root, "main", ".git") }); + const { git } = locate(worktree); + + assert.equal(git.origin, "https://github.com/moshcoder/moshcode.git"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +});