Skip to content

fix(cmd): -C operates on the given directory, not the Git root - #194

Open
nicknsheth-beep wants to merge 1 commit into
JordanCoin:mainfrom
nicknsheth-beep:fix/dash-c-scope
Open

nicknsheth-beep wants to merge 1 commit into
JordanCoin:mainfrom
nicknsheth-beep:fix/dash-c-scope

Conversation

@nicknsheth-beep

Copy link
Copy Markdown

Problem

-C <dir> (documented as "Operate on code in <repo>") doesn't scope the operation to <dir> when <dir> is a subdirectory of a Git repo rather than a repo root. It silently re-anchors to the nearest enclosing Git root instead, so -C <subdir> applies the whole repo's scan scope.

Repro

repro/
├── .git/
├── root.js
└── sub/
    └── sub.js
codemap .              # Files: 2  (correct, whole-repo scope)
codemap -C sub .       # before: Files: 2  (wrong — re-anchored to repo root)
(cd sub && codemap .)  # Files: 1  (sub.js only — the correct answer for "operate on sub/")

-C sub . should match running codemap directly from inside sub/, the same way git -C <dir> behaves.

Root cause

The global root resolver walks upward from the given -C directory to the nearest ancestor containing .git, then chdirs there — not to the literal directory passed in.

Fix

cmd/root.go: InvocationRoots gains an Operate field — the literal, canonicalized directory the caller named, falling back to the existing git-root walk-up only when that literal directory doesn't exist (so a bad path still fails the same way as before). Project/Setup/Runtime keep their existing walk-up unchanged, since storage/config discovery (projectpath.Select) already self-heals by walking up from any cwd and doesn't need cwd itself to be a repo root. main.go now chdirs to roots.Operate instead of roots.Project.

TestApplyGlobalRootOptions in main_more_test.go asserted the old, buggy behavior directly; updated its expectation to match the documented contract. Every other existing root-option e2e test (linked worktree inheritance, submodule detection, explicit --setup-root, malformed-metadata rejection) passes unchanged — those scenarios resolve to the same directory either way, or rely purely on projectpath.Select's own walk-up, which this change doesn't touch.

Tests

root_options_e2e_test.go: TestDashCScopesToTheGivenDirectory — -C sub from the repo root now reports the same file count as running codemap directly from inside sub, and both differ from the whole-repo count.

go test ./... passes (all 17 packages), gofmt -l . and go vet ./... are clean.

Related: filing alongside two other independent fixes found during the same investigation (hidden-directory scanning, and coverage-honesty for unindexed files) — happy to merge in any order, this one has no dependency on the others.

`-C <dir>` (and --project-root) resolved the nearest enclosing Git
repository root and chdir'd there instead of into <dir> itself, so
pointing codemap at a subfolder that isn't its own Git repo (e.g. a
skill folder nested a few levels into a larger repo) silently
re-scoped the scan to the whole repository. `git -C` never
reinterprets its argument this way, and codemap's own --help text
already documents "-C <repo> Operate on code in <repo>" — the old
behavior didn't match its own contract.

cmd.InvocationRoots gains an `Operate` field: the literal, canonicalized
directory the caller named (falling back to the Git-root walk-up only
when that directory doesn't exist, so a bad path still fails the same
way). Project/Setup/Runtime keep the existing Git-boundary walk-up
unchanged, since storage/config/linked-worktree/submodule discovery
already self-heals by walking up from any cwd (projectpath.Select), so
it doesn't need cwd itself to be a repo root.

TestApplyGlobalRootOptions asserted the old (buggy) behavior directly;
updated its expectation to the documented contract. Added
TestDashCScopesToTheGivenDirectory, which confirms `-C sub` from the
repo root now reports the same file count as running codemap directly
from inside `sub`, and that both differ from the whole-repo count.

Every existing root-option e2e test (linked worktree inheritance,
submodule detection, explicit --setup-root, malformed-metadata
rejection) still passes unchanged, since those scenarios all resolve
to the same directory either way or rely on projectpath.Select's own
walk-up, which is untouched.

Copy link
Copy Markdown
Owner

Thanks — this one is well reasoned, and the PR body's honesty about which existing test asserted the old behavior made it easy to review. I found the behavior change is real and mostly does what you describe, but it also has a consequence the PR doesn't mention, and it's a contract decision rather than a bug, so I'm laying out what I measured.

What I ran

CI hasn't run (fork workflows await maintainer approval), so locally, comparing a build of main with a build of this branch, using a real ast-grep (0.42.1):

  • Full go test ./...: only the three known root-permission baseline failures. gofmt, go vet and staticcheck clean.
  • State placement is unaffected. -C sub setup and -C sub watch start put .codemap/ at the repo root on both builds, nothing stray lands in sub/, and the daemon records the same root and project id. That was my main worry going in.
  • No symlink bypass of the "inside a Git repository" check: a symlink inside the repo pointing at a non-repo directory is rejected the same way on both builds.
  • I diffed 13 commands run with -C sub across both builds. config show, hook session-start, blast-radius, find, skill list, --diff and context --json are identical; ., --deps and --importers are the ones that change.

The consequence worth deciding on

File arguments now resolve against -C <dir>, not the repo root. That fixes one false answer and creates its mirror image:

invocation main this PR
-C sub --importers b.js No files import b.js. (wrong) Imported by a.js (correct)
-C sub --importers sub/b.js correct No files import sub/b.js. Coverage: complete (confident zero)

And it can undercount with Coverage: complete. In a two-package monorepo where packages/api/a.js and packages/shared/local.js both require packages/shared/util.js:

main:  -C packages/shared --importers packages/shared/util.js  ->  Imported by 2 file(s)
PR:    -C packages/shared --importers util.js                  ->  Imported by 1 file(s)  (local.js only), Coverage: complete

The sibling package's importer is silently absent. That's the same thing cd packages/shared && codemap ... does today, so the PR is being consistent with cd — but -C/--project-root is what MCP configs and scripts use to name a project, and there the narrower answer arrives labeled complete.

Two smaller knock-on effects: --deps --json now reports "root": "<repo>/sub" with paths like a.js instead of sub/a.js, so anything parsing those paths relative to the repo root would break; and the README says -C "selects the repository Codemap operates on" and takes <repo> (lines 234, 243), which would need updating.

Suggestion

Whether -C <subdir> should mean "scope to that directory" (this PR) or "find the repo containing it" (today) is a product call for @JordanCoin — both are defensible and the code here is clean either way. If it goes ahead, I'd suggest landing #195's "not indexed" message first (or together): it turns the sub/b.js row above from a confident zero into an honest "not indexed", which removes the worst edge of the trade-off. A release note plus the README wording would cover the rest.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants