fix: prevent parallel worktree path resolution from escaping to home directory (#1677)
Fixes #1676
This commit is contained in:
parent
e23a27c025
commit
57b92dee43
6 changed files with 42 additions and 16 deletions
|
|
@ -170,6 +170,20 @@ export function escapeStaleWorktree(base: string): string {
|
|||
|
||||
// base is inside .gsd/worktrees/<something> — extract the project root
|
||||
const projectRoot = base.slice(0, idx);
|
||||
|
||||
// Guard: If the candidate project root's .gsd IS the user-level ~/.gsd,
|
||||
// the string-slice heuristic matched the wrong /.gsd/ boundary. This happens
|
||||
// when .gsd is a symlink into ~/.gsd/projects/<hash> and process.cwd()
|
||||
// resolved through the symlink. Returning ~ would be catastrophic (#1676).
|
||||
const candidateGsd = join(projectRoot, ".gsd").replaceAll("\\", "/");
|
||||
const gsdHomePath = gsdHome.replaceAll("\\", "/");
|
||||
if (candidateGsd === gsdHomePath || candidateGsd.startsWith(gsdHomePath + "/")) {
|
||||
// Don't chdir to home — return base unchanged.
|
||||
// resolveProjectRoot() in worktree.ts has the full git-file-based recovery
|
||||
// and will be called by the caller (startAuto → projectRoot()).
|
||||
return base;
|
||||
}
|
||||
|
||||
try {
|
||||
process.chdir(projectRoot);
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -243,6 +243,15 @@ export function ensureGsdSymlink(projectPath: string): string {
|
|||
const localGsd = join(projectPath, ".gsd");
|
||||
const inWorktree = isInsideWorktree(projectPath);
|
||||
|
||||
// Guard: Never create a symlink at ~/.gsd — that's the user-level GSD home,
|
||||
// not a project .gsd. This can happen if resolveProjectRoot() or
|
||||
// escapeStaleWorktree() returned ~ as the project root (#1676).
|
||||
const localGsdNormalized = localGsd.replaceAll("\\", "/");
|
||||
const gsdHomePath = gsdHome.replaceAll("\\", "/");
|
||||
if (localGsdNormalized === gsdHomePath) {
|
||||
return localGsd;
|
||||
}
|
||||
|
||||
// Ensure external directory exists
|
||||
mkdirSync(externalPath, { recursive: true });
|
||||
|
||||
|
|
|
|||
|
|
@ -204,6 +204,9 @@ async function main(): Promise<void> {
|
|||
"/real/project",
|
||||
"uses GSD_PROJECT_ROOT when set",
|
||||
);
|
||||
delete process.env.GSD_PROJECT_ROOT;
|
||||
|
||||
// Without GSD_PROJECT_ROOT, direct layout still works (no ~/.gsd collision)
|
||||
assertEq(
|
||||
resolveProjectRoot("/some/repo"),
|
||||
"/some/repo",
|
||||
|
|
|
|||
|
|
@ -123,16 +123,15 @@ export function detectWorktreeName(basePath: string): string | null {
|
|||
* operate against the real project root, not a worktree subdirectory.
|
||||
*/
|
||||
export function resolveProjectRoot(basePath: string): string {
|
||||
const normalizedPath = basePath.replaceAll("\\", "/");
|
||||
const seg = findWorktreeSegment(normalizedPath);
|
||||
if (!seg) return basePath;
|
||||
|
||||
// Layer 1: If the coordinator passed the real project root, use it.
|
||||
// Only apply this override when basePath actually looks like a worktree path.
|
||||
if (process.env.GSD_PROJECT_ROOT) {
|
||||
return process.env.GSD_PROJECT_ROOT;
|
||||
}
|
||||
|
||||
const normalizedPath = basePath.replaceAll("\\", "/");
|
||||
const seg = findWorktreeSegment(normalizedPath);
|
||||
if (!seg) return basePath;
|
||||
|
||||
// Candidate root via the string-slice heuristic
|
||||
const sepChar = basePath.includes("\\") ? "\\" : "/";
|
||||
const gsdMarker = `${sepChar}.gsd${sepChar}`;
|
||||
|
|
@ -173,7 +172,7 @@ function resolveProjectRootFromGitFile(worktreePath: string): string | null {
|
|||
try {
|
||||
// Walk up from the worktree path to find the .git file
|
||||
let dir = worktreePath;
|
||||
while (true) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const gitPath = join(dir, ".git");
|
||||
if (existsSync(gitPath)) {
|
||||
const content = readFileSync(gitPath, "utf8").trim();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function findWorktreeSegment(normalizedPath) {
|
|||
function resolveProjectRootFromGitFile(worktreePath) {
|
||||
try {
|
||||
let dir = worktreePath;
|
||||
while (true) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const gitPath = join(dir, ".git");
|
||||
if (existsSync(gitPath)) {
|
||||
const content = readFileSync(gitPath, "utf8").trim();
|
||||
|
|
@ -71,15 +71,15 @@ function normalizePathForCompare(path) {
|
|||
}
|
||||
|
||||
function resolveProjectRoot(basePath) {
|
||||
const normalizedPath = basePath.replaceAll("\\", "/");
|
||||
const seg = findWorktreeSegment(normalizedPath);
|
||||
if (!seg) return basePath;
|
||||
|
||||
// Layer 1: If the coordinator passed the real project root, use it.
|
||||
if (process.env.GSD_PROJECT_ROOT) {
|
||||
return process.env.GSD_PROJECT_ROOT;
|
||||
}
|
||||
|
||||
const normalizedPath = basePath.replaceAll("\\", "/");
|
||||
const seg = findWorktreeSegment(normalizedPath);
|
||||
if (!seg) return basePath;
|
||||
|
||||
const sepChar = basePath.includes("\\") ? "\\" : "/";
|
||||
const gsdMarker = `${sepChar}.gsd${sepChar}`;
|
||||
const gsdIdx = basePath.indexOf(gsdMarker);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function findWorktreeSegment(normalizedPath) {
|
|||
function resolveProjectRootFromGitFile(worktreePath) {
|
||||
try {
|
||||
let dir = worktreePath;
|
||||
while (true) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const gitPath = join(dir, ".git");
|
||||
if (existsSync(gitPath)) {
|
||||
const content = readFileSync(gitPath, "utf8").trim();
|
||||
|
|
@ -81,14 +81,15 @@ function normalizePathForCompare(path) {
|
|||
}
|
||||
|
||||
function resolveProjectRoot(basePath) {
|
||||
const normalizedPath = basePath.replaceAll("\\", "/");
|
||||
const seg = findWorktreeSegment(normalizedPath);
|
||||
if (!seg) return basePath;
|
||||
|
||||
// Layer 1: If the coordinator passed the real project root, use it.
|
||||
if (process.env.GSD_PROJECT_ROOT) {
|
||||
return process.env.GSD_PROJECT_ROOT;
|
||||
}
|
||||
|
||||
const normalizedPath = basePath.replaceAll("\\", "/");
|
||||
const seg = findWorktreeSegment(normalizedPath);
|
||||
if (!seg) return basePath;
|
||||
|
||||
const sepChar = basePath.includes("\\") ? "\\" : "/";
|
||||
const gsdMarker = `${sepChar}.gsd${sepChar}`;
|
||||
const gsdIdx = basePath.indexOf(gsdMarker);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue