diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 3101f02e5..edcf95e3c 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -57,7 +57,18 @@ function findProjectRoot(startDir) { const resolved = path.resolve(startDir); const root = path.parse(resolved).root; const homedir = require('os').homedir(); - const startHasGit = fs.existsSync(path.join(resolved, '.git')); + + // Check if startDir or any of its ancestors (up to but not including a + // candidate project root) contains a .git directory. This handles both + // `backend/` (direct sub-repo) and `backend/src/modules/` (nested inside). + function isInsideGitRepo(candidateParent) { + let d = resolved; + while (d !== candidateParent && d !== root) { + if (fs.existsSync(path.join(d, '.git'))) return true; + d = path.dirname(d); + } + return false; + } let dir = resolved; while (dir !== root) { @@ -82,15 +93,15 @@ function findProjectRoot(startDir) { } // Check legacy multiRepo flag - if (config.multiRepo === true && startHasGit) { + if (config.multiRepo === true && isInsideGitRepo(parent)) { return parent; } } catch { // config.json missing or malformed — fall back to .git heuristic } - // Heuristic: parent has .planning/ and startDir has its own .git - if (startHasGit) { + // Heuristic: parent has .planning/ and we're inside a git repo + if (isInsideGitRepo(parent)) { return parent; } } diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index b19d3d1d8..6083dd908 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -57,6 +57,7 @@ function cmdInitExecutePhase(cwd, phase, raw) { // Config flags commit_docs: config.commit_docs, + sub_repos: config.sub_repos, parallelization: config.parallelization, context_window: config.context_window, branching_strategy: config.branching_strategy, diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index b5cb42482..9f1bd89d2 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -535,12 +535,12 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "chore: add project Check for directories with their own `.git` folders (separate repos within the workspace): ```bash -find . -maxdepth 2 -type d -name ".git" -not -path "./.git" +find . -maxdepth 1 -type d -not -name ".*" -not -name "node_modules" -exec test -d "{}/.git" \; -print ``` **If sub-repos found:** -Strip the `/.git` suffix and `./` prefix to get directory names (e.g., `./backend/.git` → `backend`). +Strip the `./` prefix to get directory names (e.g., `./backend` → `backend`). Use AskUserQuestion: - header: "Multi-Repo Workspace" diff --git a/tests/core.test.cjs b/tests/core.test.cjs index eca98f1f4..77fb46956 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -1167,6 +1167,53 @@ describe('findProjectRoot', () => { assert.strictEqual(findProjectRoot(backendDir), projectRoot); }); + test('walks up from nested path inside sub-repo via .git heuristic', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + + // Sub-repo with .git at its root + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(path.join(backendDir, '.git'), { recursive: true }); + + // Nested path deep inside the sub-repo + const nestedDir = path.join(backendDir, 'src', 'modules', 'auth'); + fs.mkdirSync(nestedDir, { recursive: true }); + + // isInsideGitRepo walks up and finds backend/.git + assert.strictEqual(findProjectRoot(nestedDir), projectRoot); + }); + + test('walks up from nested path inside sub-repo via sub_repos config', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ sub_repos: ['backend'] }) + ); + + // Nested path deep inside the sub-repo + const nestedDir = path.join(projectRoot, 'backend', 'src', 'modules'); + fs.mkdirSync(nestedDir, { recursive: true }); + + // With sub_repos config, it checks topSegment of relative path + assert.strictEqual(findProjectRoot(nestedDir), projectRoot); + }); + + test('walks up from nested path via legacy multiRepo flag', () => { + fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, '.planning', 'config.json'), + JSON.stringify({ multiRepo: true }) + ); + + const backendDir = path.join(projectRoot, 'backend'); + fs.mkdirSync(path.join(backendDir, '.git'), { recursive: true }); + + // Nested inside sub-repo — isInsideGitRepo walks up and finds backend/.git + const nestedDir = path.join(backendDir, 'src'); + fs.mkdirSync(nestedDir, { recursive: true }); + + assert.strictEqual(findProjectRoot(nestedDir), projectRoot); + }); + test('does not walk up for dirs without .git when no sub_repos config', () => { fs.mkdirSync(path.join(projectRoot, '.planning'), { recursive: true });