fix(task): canonicalize the shared-common-dir gate in detachGitDir

rev-parse --git-common-dir resolves symlinks while ensureIsolation derives
sourceCommonDir lexically from the session cwd (resolveRepository walks
path.resolve'd components). On any symlinked repo path (macOS /tmp,
symlinked project dirs) the lexical comparison missed, detachGitDir
returned "independent", and the parent-mutation leak silently survived.
Realpath both sides before comparing; regression test drives the gate
through a symlink alias.
This commit is contained in:
can1357
2026-07-18 19:42:36 +02:00
parent 53437aff3d
commit df9b04a0eb
2 changed files with 37 additions and 8 deletions
+12 -8
View File
@@ -1430,14 +1430,18 @@ export async function detachGitDir(worktreeRoot: string, sourceCommonDir: string
if (isEnoent(err)) return "no-git";
throw err;
}
const parentCommon = path.resolve(sourceCommonDir);
const isoCommon = path.resolve(
(
await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
readOnly: true,
})
).trim(),
);
// Canonicalize both sides before comparing: `rev-parse` resolves symlinks
// (macOS `/tmp` → `/private/tmp`) while callers derive `sourceCommonDir`
// lexically from the session cwd. A lexical mismatch here would silently
// classify a shared linked-worktree copy as "independent" and skip the
// detach entirely — leaving the parent-mutation leak in place.
const parentCommon = await fs.promises.realpath(sourceCommonDir).catch(() => path.resolve(sourceCommonDir));
const isoCommonRaw = (
await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
readOnly: true,
})
).trim();
const isoCommon = await fs.promises.realpath(isoCommonRaw).catch(() => path.resolve(isoCommonRaw));
// A full-copy `.git` already resolves to its own object DB — leave it alone.
if (isoCommon !== parentCommon) return "independent";