Skip to content

fix(scanner,config): skip nested worktrees, union --exclude with config, keep .codemap/ out of git status - #198

Merged
JordanCoin merged 2 commits into
mainfrom
fix/scan-hygiene
Oct 7, 2026
Merged

JordanCoin merged 2 commits into
mainfrom
fix/scan-hygiene

Conversation

@JordanCoin

Copy link
Copy Markdown
Owner

Three scan-hygiene fixes, one commit.

Bug A: nested git worktrees were scanned as project files (#131)

scanner.IgnoredDirs only skipped a directory literally named .git; a linked worktree's .git is a file (gitdir: ...), and findNestedGitRepos only looked one level down, skipped dotted directories and required .git to be a directory.

Fix. scanner.IsNestedGitRepo(dir) treats a .git entry that is a directory or a regular file as a repository boundary. scanner.NestedGitRepos(root) walks the tree (any depth, dotted directories included, not entering IgnoredDirs, gitignored directories, or a nested repo's own subtree) and returns the root-relative paths. One implementation now feeds ScanFiles (so codemap ., context, handoff, config init, the daemon), findNestedGitRepos (ast-grep --globs "!<rel>/**", still with cwd=root and . from #193, and the Go fallback), and ReadExternalDeps. The scan root's own .git is never a boundary, so scanning a linked worktree as root still works.

Before/after on a clone of this repo with git worktree add .claude/worktrees/agent-x HEAD and a plain clone at sub-clone/:

main this branch
codemap . Files: 259 → 518 (worktree) → 777 (+ clone) Files: 259
codemap --deps --json . file entries 458 229 (0 under .claude/worktrees/ or sub-clone/)

No printed line changes.

Bug B: --exclude replaced the config's exclude list (#150)

main.go only used projCfg.Exclude when the CLI list was empty, so --exclude ZZZNOMATCH silently re-admitted everything the config excluded.

Fix. config.MergeExcludes(configured, cli) unions config patterns (first) with CLI patterns, dropping blanks and duplicates; main.go always applies it. --only keeps its override semantics on purpose: only is an allow-list and "just Swift for this run" is the obvious intent, and I found no equivalent bug there. --depth is unchanged. Hook subprocesses that already forward the config's excludes now just dedupe.

Before/after (config {"exclude":["vendorx"]}, 7 files + config): codemap . → Files: 5; --exclude ZZZNOMATCH → was Files: 8, now Files: 5; --exclude src → was Files: 6, now Files: 3.

README's "Project config" note now reads: --only and --depth override the config's values for one run; --exclude adds to the config's exclude list… (replacing "CLI flags always override config"). The existing TestBinaryDependencyModesHonorCLIFiltersOverConfig locked in the old override behaviour; it is renamed ...MergeCLIFiltersWithConfig and asserts the union.

Bug C: an unignored .codemap/ left in every repo codemap touches (exit test of #184)

cmd.EnsureProjectConfig (#189) deliberately auto-writes .codemap/config.json; that stays. The fallback from the issue is implemented instead.

Fix. New internal/gitexclude (the git-exclude helpers from cmd/gitignore.go, moved so watch, handoff and topology can use them; cmd/gitignore.go keeps its names as thin wrappers, so codemap setup/config init behave exactly as before, tests untouched):

  • EnsureIgnoredIfNeeded(root): git check-ignore -v .codemap/; only when nothing ignores it, append .codemap/ to <git rev-parse --git-common-dir>/info/exclude (shared by linked worktrees; verified a worktree writes to the primary's .git/info/exclude). Never creates or edits a tracked .gitignore; a repo whose .gitignore already covers .codemap/ gets no redundant local rule.
  • MkdirAll(dir, perm): os.MkdirAll, and if that just created the .codemap directory (dir or an ancestor), run EnsureIgnoredIfNeeded on its parent. Best-effort, no git call once .codemap exists. Used by EnsureProjectConfig, hook state (pre-compact, session leases, agent-edit log), the watch daemon/pid/transition writes, handoff WriteLatest, topology cache and skill init.
  • TrackedStateFiles(root) (git ls-files -z -- .codemap) backs the new doctor check.

Before/after on a fresh clone of a one-file repo with no codemap config, git status --porcelain after each command:

command main this branch
codemap . ?? .codemap/ empty
codemap --deps . ?? .codemap/ empty
codemap find main . ?? .codemap/ empty
context, handoff --no-save, blast-radius empty on this fixture empty

New printed lines

  • stderr, TTY only, right after the existing auto-init notice: codemap: added .codemap/ to /path/.git/info/exclude so git status stays clean; verify with: git check-ignore -v .codemap — sourced (names the file written and the command that reproduces the fact), precise (one rule, one path), bounded (only printed when the rule was written this run; silent when a rule already existed or outside git), useful (the verify command).
  • codemap doctor: MISS tracked codemap state: 1 file(s) under .codemap/ are committed; run: git rm -r --cached .codemap — sourced (git ls-files .codemap), precise (exact count), bounded (scoped to .codemap/, index state only), useful (the exact repair command). Counts as a doctor failure.
  • codemap doctor: OK tracked codemap state: 0 files under .codemap/ are committed (git ls-files .codemap) — sourced (names the command), precise (0), bounded (says what it checked), useful (confirms the ignore rule is sufficient; appears next to local Codemap ignore: effective via ...).

The state-location question in #184 (whether .codemap/ should live in the repo at all) is intentionally left open.

Tests

  • scanner: TestScanFilesSkipsNestedWorktreesAndClones, TestNestedGitReposFindsWorktreesAtAnyDepth, TestReadExternalDepsSkipsNestedRepos (fixture built with real git worktree add under .claude/worktrees/ plus git clone two levels down), TestScanDirectoryExcludesNestedWorktreesByPath (captured ast-grep args contain --globs !.claude/worktrees/agent-x/** and !tools/sub-clone/**), TestScanDirectoryRealAstGrepSkipsNestedWorktree (real ast-grep indexes only the root file).
  • config: TestMergeExcludesUnionsConfigAndCLI. root: TestMainExcludeFlagAddsToConfigExcludes (config exclude + non-matching CLI exclude leaves the count unchanged; matching removes both sets).
  • internal/gitexclude: fresh clone stays clean after a state write, no local rule when .gitignore covers it, non-git and no-.codemap paths are plain MkdirAll, linked worktree writes the primary's shared exclude, tracked-file count.
  • cmd: TestEnsureProjectConfigKeepsFreshCloneClean, TestRunDoctorReportsTrackedCodemapState. root: TestMainFreshCloneLeavesGitStatusClean — the exit test from hygiene: codemap leaves .codemap/ state in every repo it touches, unignored and sometimes committed; agent worktrees pollute scans #184: fresh clone, codemap ., git status --porcelain empty, no .gitignore created.
  • go test ./...: only the 3 pre-existing root-permission failures remain (TestRunSetupCreatesConfigAndHooks, TestSelectReportsInaccessiblePrimarySetup, TestListProjectsPreservesPerChildScanErrors). gofmt -l ., go vet ./..., staticcheck ./... clean.

Closes #131
Closes #150
Addresses the exit test of #184

🤖 Generated with Claude Code

https://claude.ai/code/session_0159tcanCgA3jxi2E7DdRVWJ


Generated by Claude Code

…ig, keep .codemap/ out of git status

Nested git repositories (linked worktrees whose .git is a gitdir file,
plain clones, submodules) are repository boundaries at any depth, under
dotted directories too; the file walker, the ast-grep scan, the Go
fallback and the manifest reader share one detector (issue #131).

--exclude now adds to the project config's exclude list instead of
replacing it; --only and --depth keep overriding (issue #150).

Every codemap write that creates .codemap/ in a git checkout appends
.codemap/ to <git-common-dir>/info/exclude when no rule ignores it yet,
never touching a tracked .gitignore; doctor reports tracked .codemap/
files (exit test of issue #184).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159tcanCgA3jxi2E7DdRVWJ
Copilot AI balanced review requested due to automatic review settings October 7, 2026 03:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…ot fail the worktree test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159tcanCgA3jxi2E7DdRVWJ
@JordanCoin
JordanCoin merged commit cb0f740 into main Oct 7, 2026
12 checks passed
JordanCoin added a commit that referenced this pull request Oct 7, 2026
Resolves the one conflict in mcp/main.go handleGetImporters: the #140
not-indexed check runs first inside the empty-importers branch (an
unindexed file has no Go package to be a sibling of), and an indexed
file with zero importers falls through to #199's Go package-granularity
wording.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159tcanCgA3jxi2E7DdRVWJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants