fix(gsd): prevent ensureGsdSymlink from creating subdirectory .gsd when git-root .gsd exists
When running GSD from a subdirectory (e.g. `cd src/ && gsd`), ensureGsdSymlink would create a new `.gsd` symlink in the subdirectory even though a valid `.gsd` already exists at the git root. On macOS APFS this triggers the `.gsd 2` collision variant problem from #2205. Add an early guard that detects when projectPath is a plain subdirectory (not a worktree) of a git repo that already has `.gsd` at its root, and returns the existing root .gsd target instead of creating a duplicate. Fixes #2380 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a53d021864
commit
a3250c4103
2 changed files with 60 additions and 0 deletions
|
|
@ -378,6 +378,34 @@ export function ensureGsdSymlink(projectPath: string): string {
|
|||
return localGsd;
|
||||
}
|
||||
|
||||
// Guard: If projectPath is a plain subdirectory (not a worktree) of a git
|
||||
// repo that already has a .gsd at the git root, do not create a duplicate
|
||||
// symlink in the subdirectory — that causes `.gsd 2` collision variants on
|
||||
// macOS (#2380). Worktrees are excluded because they legitimately need their
|
||||
// own .gsd symlink pointing at the shared external state dir.
|
||||
if (!inWorktree) {
|
||||
try {
|
||||
const gitRoot = resolveGitRoot(projectPath);
|
||||
const normalizedProject = canonicalizeExistingPath(projectPath);
|
||||
const normalizedRoot = canonicalizeExistingPath(gitRoot);
|
||||
if (normalizedProject !== normalizedRoot) {
|
||||
const rootGsd = join(gitRoot, ".gsd");
|
||||
if (existsSync(rootGsd)) {
|
||||
try {
|
||||
const rootStat = lstatSync(rootGsd);
|
||||
if (rootStat.isSymbolicLink() || rootStat.isDirectory()) {
|
||||
return rootStat.isSymbolicLink() ? realpathSync(rootGsd) : rootGsd;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to normal logic if we can't stat root .gsd
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If git root detection fails, fall through to normal logic
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up macOS numbered collision variants (.gsd 2, .gsd 3, etc.) before
|
||||
// any existence checks — otherwise they accumulate and confuse state (#2205).
|
||||
cleanNumberedGsdVariants(projectPath);
|
||||
|
|
|
|||
|
|
@ -184,6 +184,38 @@ test('subdirectory of parent repo gets unique identity after git init (#1639)',
|
|||
rmSync(parentRepo, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('ensureGsdSymlink from subdirectory does not create .gsd in subdir when git-root .gsd exists (#2380)', () => {
|
||||
const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-subdir-symlink-")));
|
||||
run("git init -b main", repo);
|
||||
run('git config user.name "Pi Test"', repo);
|
||||
run('git config user.email "pi@example.com"', repo);
|
||||
run('git remote add origin git@github.com:example/subdir-test.git', repo);
|
||||
writeFileSync(join(repo, "README.md"), "# Subdir Test\n", "utf-8");
|
||||
run("git add README.md", repo);
|
||||
run('git commit -m "init"', repo);
|
||||
|
||||
// Set up .gsd symlink at the git root (normal project initialisation)
|
||||
ensureGsdSymlink(repo);
|
||||
assert.ok(existsSync(join(repo, ".gsd")), "root .gsd exists after ensureGsdSymlink");
|
||||
assert.ok(lstatSync(join(repo, ".gsd")).isSymbolicLink(), "root .gsd is a symlink");
|
||||
|
||||
// Create a subdirectory and call ensureGsdSymlink from there
|
||||
const subdir = join(repo, "src", "lib");
|
||||
mkdirSync(subdir, { recursive: true });
|
||||
ensureGsdSymlink(subdir);
|
||||
|
||||
// ensureGsdSymlink should NOT create a .gsd in the subdirectory
|
||||
// because the git root already has a valid .gsd symlink.
|
||||
assert.ok(!existsSync(join(subdir, ".gsd")), "no .gsd created in subdirectory when git-root .gsd exists (#2380)");
|
||||
assert.ok(!existsSync(join(repo, "src", ".gsd")), "no .gsd created in intermediate directory");
|
||||
|
||||
// The root .gsd should still be intact
|
||||
assert.ok(existsSync(join(repo, ".gsd")), "root .gsd still exists");
|
||||
assert.ok(lstatSync(join(repo, ".gsd")).isSymbolicLink(), "root .gsd is still a symlink");
|
||||
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('validateProjectId rejects invalid values', () => {
|
||||
for (const invalid of ["has spaces", "path/traversal", "dot..dot", "back\\slash"]) {
|
||||
assert.ok(!validateProjectId(invalid), `validateProjectId rejects invalid value: "${invalid}"`);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue