Title
git repo discovery: root walk, worktree dedup
Summary
Implement repository discovery: depth-limited walk of configured roots,
.git dir/file detection, exclusion rules, symlink safety, and worktree
deduplication via git rev-parse --git-common-dir (DESIGN.md §7.2
"Discovery").
Context
The user's environment keeps repos under ~/dev and uses git worktrees
heavily (1 task = 1 worktree policy). Naive discovery would list a main repo
and each of its worktrees as separate repos and duplicate every commit in
reports. The common-dir dedup rule here is what prevents that.
Scope
internal/source/gitsrc/discover.go + tests (fixture repos built in
t.TempDir() by test helpers).
Detailed Requirements
type Repo struct { WorkDir string; CommonDir string; Name string } —
Name = base name of the canonical work dir.
Discover(ctx context.Context, cfg config.SourcesGit, runner Runner) ([]Repo, []model.Warning):
- Walk each root from
cfg.GitRoots() (tilde-expanded by config
accessors): breadth-first, max depth cfg.MaxDepth (root itself =
depth 0).
- Skip (do not descend into): directories whose base name is in
exclude_dirs; all dot-directories (.foo) except that a .git
entry marks its parent as a repo; symlinked directories (Lstat
check — never follow, cycle prevention).
- Repo detection: directory containing
.git (dir or file). On
detection, record candidate and do NOT descend further into it.
- Missing/non-directory root → warning
git_root_missing (already part
of the canonical DESIGN §13 taxonomy; the constant exists in
internal/model from issue 03).
- Explicit
cfg.Repos paths join the candidate list (missing path →
git_root_missing warning naming it).
- Dedup: for each candidate run
git --no-optional-locks -C <dir> rev-parse --path-format=absolute --git-common-dir via the injected Runner (interface
Run(ctx, dir string, args ...string) (stdout string, err error) — real
impl in issue 12, test impl fake). Canonical key = cleaned common-dir
path. First candidate wins per key; candidates whose rev-parse fails →
warning git_repo_failed (skip). WorkDir of the kept entry: if the
common dir is <X>/.git, use X; else (detached/odd layouts) keep the
candidate dir.
- Deterministic output: sort Repos by Name then WorkDir.
git binary absence is issue 12's Runner concern: Discover surfaces
it as a single git_binary_missing warning and returns zero repos
(detected on first Run error of type ErrGitNotFound).
- Walk must be allocation-sane on big trees: use
os.ReadDir, check
ctx.Err() per directory.
Acceptance Criteria
Validation
go test -race -cover ./internal/source/gitsrc/ in PR (helper-built
fixtures; requires system git on the test machine — CI has it).
Dependencies
01, 03.
Non-goals
Commit extraction (12). Submodules: because the walk never descends past a
detected repo boundary, submodules inside a discovered repo are NOT
enumerated (their commits belong to their own repos; users who want them add
the submodule path to repos explicitly). A submodule checkout sitting
directly under a root (unusual) is treated as an ordinary repo candidate.
Bare repos: not supported in v1 (no worktree = no daily-work relevance);
rev-parse-based dedup keeps them from duplicating anything.
Design References
docs/DESIGN.md §7.2 (Discovery), §13
docs/decisions/ADR-003-read-only-sources-and-stateless-runs.md
Source of truth: docs/issues/11-git-repo-discovery.md (PR #1, branch docs/v1-design). If this issue and the repo docs disagree, the docs win. Execution order and dependencies: docs/ISSUE_PLAN.md (this is issue 11 of 33).
Title
git repo discovery: root walk, worktree dedup
Summary
Implement repository discovery: depth-limited walk of configured roots,
.gitdir/file detection, exclusion rules, symlink safety, and worktreededuplication via
git rev-parse --git-common-dir(DESIGN.md §7.2"Discovery").
Context
The user's environment keeps repos under
~/devand uses git worktreesheavily (1 task = 1 worktree policy). Naive discovery would list a main repo
and each of its worktrees as separate repos and duplicate every commit in
reports. The common-dir dedup rule here is what prevents that.
Scope
internal/source/gitsrc/discover.go+ tests (fixture repos built int.TempDir()by test helpers).Detailed Requirements
type Repo struct { WorkDir string; CommonDir string; Name string }—Name= base name of the canonical work dir.Discover(ctx context.Context, cfg config.SourcesGit, runner Runner) ([]Repo, []model.Warning):cfg.GitRoots()(tilde-expanded by configaccessors): breadth-first, max depth
cfg.MaxDepth(root itself =depth 0).
exclude_dirs; all dot-directories (.foo) except that a.gitentry marks its parent as a repo; symlinked directories (
Lstatcheck — never follow, cycle prevention).
.git(dir or file). Ondetection, record candidate and do NOT descend further into it.
git_root_missing(already partof the canonical DESIGN §13 taxonomy; the constant exists in
internal/modelfrom issue 03).cfg.Repospaths join the candidate list (missing path →git_root_missingwarning naming it).git --no-optional-locks -C <dir> rev-parse --path-format=absolute --git-common-dirvia the injectedRunner(interfaceRun(ctx, dir string, args ...string) (stdout string, err error)— realimpl in issue 12, test impl fake). Canonical key = cleaned common-dir
path. First candidate wins per key; candidates whose rev-parse fails →
warning
git_repo_failed(skip).WorkDirof the kept entry: if thecommon dir is
<X>/.git, useX; else (detached/odd layouts) keep thecandidate dir.
gitbinary absence is issue 12'sRunnerconcern:Discoversurfacesit as a single
git_binary_missingwarning and returns zero repos(detected on first Run error of type ErrGitNotFound).
os.ReadDir, checkctx.Err()per directory.Acceptance Criteria
max_depth (excluded), one under
node_modules(excluded), onesymlinked dir (not followed), one plain dir (not a repo) → exactly the
2 expected repos.
git worktree addsibling → ONE Repo,Name= main repo dir name (helper builds real worktrees with thesystem git).
reposentry pointing at a worktree of an already-discoveredmain repo → still one Repo (dedup across discovery+explicit).
git_root_missingwarnings,others unaffected.
skipped with
git_repo_failed, others kept.sorted output.
Validation
go test -race -cover ./internal/source/gitsrc/in PR (helper-builtfixtures; requires system git on the test machine — CI has it).
Dependencies
01, 03.
Non-goals
Commit extraction (12). Submodules: because the walk never descends past a
detected repo boundary, submodules inside a discovered repo are NOT
enumerated (their commits belong to their own repos; users who want them add
the submodule path to
reposexplicitly). A submodule checkout sittingdirectly under a root (unusual) is treated as an ordinary repo candidate.
Bare repos: not supported in v1 (no worktree = no daily-work relevance);
rev-parse-based dedup keeps them from duplicating anything.
Design References
docs/DESIGN.md§7.2 (Discovery), §13docs/decisions/ADR-003-read-only-sources-and-stateless-runs.mdSource of truth:
docs/issues/11-git-repo-discovery.md(PR #1, branchdocs/v1-design). If this issue and the repo docs disagree, the docs win. Execution order and dependencies:docs/ISSUE_PLAN.md(this is issue 11 of 33).