fix(workflow): detect nested git worktree in new-project bootstrap (#3491)
The `has_git` boolean returned by `init new-project` and `init ingest-docs` was derived from a shallow `pathExists(cwd, '.git')` check, so a subdirectory of an existing repo reported `has_git: false`. The workflow then ran `git init`, creating a nested `.git` inside the outer worktree and silently diverting subsequent `gsd-sdk commit` calls into the nested repo. Replace the shallow check with `git rev-parse --is-inside-work-tree` semantics in both CJS (`get-shit-done/bin/lib/init.cjs`) and TS (`sdk/src/query/init.ts`, `sdk/src/query/init-complex.ts`) handlers via a new shared `gitWorktreeInfoInternal` helper, and expose `git_worktree_root` + `in_nested_subdir` so the workflows can refuse `git init` inside an existing worktree and warn that planning files will track to the outer repo. Regression test: `tests/bug-3491-nested-git-worktree.test.cjs`. Fixes #3491 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync, statSync, type Dirent } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join, relative } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
@@ -61,6 +62,40 @@ function pathExists(base: string, relPath: string): boolean {
|
||||
return existsSync(join(base, relPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bug #3491: detect whether `base` is inside any git worktree, and if so,
|
||||
* return the absolute worktree root. Mirrors the CJS `gitWorktreeInfoInternal`
|
||||
* in get-shit-done/bin/lib/core.cjs — keep these two implementations behaviour-
|
||||
* identical so the SDK and CJS init handlers emit the same has_git semantics.
|
||||
*
|
||||
* Returns { inside, worktreeRoot } — both fall back to false/null on any error
|
||||
* (git unavailable, not a repo, timeout) so callers see the conservative
|
||||
* default that preserves pre-fix behaviour for non-git environments.
|
||||
*/
|
||||
function gitWorktreeInfo(base: string): { inside: boolean; worktreeRoot: string | null } {
|
||||
try {
|
||||
const inside = execSync('git rev-parse --is-inside-work-tree', {
|
||||
cwd: base,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||
}).trim();
|
||||
if (inside !== 'true') return { inside: false, worktreeRoot: null };
|
||||
const root = execSync('git rev-parse --show-toplevel', {
|
||||
cwd: base,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||
}).trim();
|
||||
return { inside: true, worktreeRoot: root || null };
|
||||
} catch {
|
||||
return { inside: false, worktreeRoot: null };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const NEW_PROJECT_REQUIRED_AGENTS = [
|
||||
'gsd-project-researcher',
|
||||
'gsd-research-synthesizer',
|
||||
@@ -269,7 +304,13 @@ export const initNewProject: QueryHandler = async (_args, projectDir, workstream
|
||||
needs_codebase_map:
|
||||
(hasExistingCode || hasPackageFile) && !pathExists(projectDir, '.planning/codebase'),
|
||||
|
||||
has_git: pathExists(projectDir, '.git'),
|
||||
// Bug #3491: detect parent worktree to avoid nested .git init.
|
||||
has_git: (() => gitWorktreeInfo(projectDir).inside)(),
|
||||
git_worktree_root: (() => gitWorktreeInfo(projectDir).worktreeRoot)(),
|
||||
in_nested_subdir: (() => {
|
||||
const info = gitWorktreeInfo(projectDir);
|
||||
return info.inside && info.worktreeRoot !== null && info.worktreeRoot !== projectDir;
|
||||
})(),
|
||||
|
||||
brave_search_available: hasBraveSearch,
|
||||
firecrawl_available: hasFirecrawl,
|
||||
|
||||
@@ -78,6 +78,40 @@ function pathExists(base: string, relPath: string): boolean {
|
||||
return existsSync(join(base, relPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bug #3491: detect whether `base` is inside any git worktree, and if so,
|
||||
* return the absolute worktree root. Mirrors the CJS `gitWorktreeInfoInternal`
|
||||
* in get-shit-done/bin/lib/core.cjs — keep these two implementations behaviour-
|
||||
* identical so the SDK and CJS init handlers emit the same has_git semantics.
|
||||
*
|
||||
* Returns { inside, worktreeRoot } — both fall back to false/null on any error
|
||||
* (git unavailable, not a repo, timeout) so callers see the conservative
|
||||
* default that preserves pre-fix behaviour for non-git environments.
|
||||
*/
|
||||
function gitWorktreeInfo(base: string): { inside: boolean; worktreeRoot: string | null } {
|
||||
try {
|
||||
const inside = execSync('git rev-parse --is-inside-work-tree', {
|
||||
cwd: base,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||
}).trim();
|
||||
if (inside !== 'true') return { inside: false, worktreeRoot: null };
|
||||
const root = execSync('git rev-parse --show-toplevel', {
|
||||
cwd: base,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||
}).trim();
|
||||
return { inside: true, worktreeRoot: root || null };
|
||||
} catch {
|
||||
return { inside: false, worktreeRoot: null };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compute the canonical phase directory name for a known phase entry from the
|
||||
* roadmap when no directory exists yet. Applies the project_code prefix so
|
||||
@@ -1197,7 +1231,13 @@ export const initIngestDocs: QueryHandler = async (_args, projectDir) => {
|
||||
const result: Record<string, unknown> = {
|
||||
project_exists: pathExists(projectDir, '.planning/PROJECT.md'),
|
||||
planning_exists: pathExists(projectDir, '.planning'),
|
||||
has_git: pathExists(projectDir, '.git'),
|
||||
// Bug #3491: detect parent worktree to avoid nested .git init.
|
||||
has_git: (() => gitWorktreeInfo(projectDir).inside)(),
|
||||
git_worktree_root: (() => gitWorktreeInfo(projectDir).worktreeRoot)(),
|
||||
in_nested_subdir: (() => {
|
||||
const info = gitWorktreeInfo(projectDir);
|
||||
return info.inside && info.worktreeRoot !== null && info.worktreeRoot !== projectDir;
|
||||
})(),
|
||||
project_path: '.planning/PROJECT.md',
|
||||
commit_docs: config.commit_docs,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user