Skip to content

fix(sync): detect changes when the project lives in a subdirectory of its git repo - #1636

Open
innoxxicide wants to merge 1 commit into
colbymchenry:mainfrom
innoxxicide:fix/git-status-subdir-project
Open

fix(sync): detect changes when the project lives in a subdirectory of its git repo#1636
innoxxicide wants to merge 1 commit into
colbymchenry:mainfrom
innoxxicide:fix/git-status-subdir-project

Conversation

@innoxxicide

Copy link
Copy Markdown

No matching issue — found while using CodeGraph on a repository whose project
lives one directory down. I couldn't find an existing report for it, so the
repro is written out in full below.

codegraph status printed Index is up to date no matter how many files had
changed, whenever the indexed project sat below its repository root — a
monorepo package, an app/ folder beside a server/ one. codegraph sync run
a second later found the same files and reindexed them, so the two commands
flatly contradicted each other, and anything trusting the status count (the
staleness reminder, a scripted check) read the index as clean forever.

Reproduced both ways on the released build before touching anything:

layout edit codegraph status
project is the repo root 1 file modified Modified: 1 files
project in app/ of the repo 1 file modified Index is up to date

What was wrong

getGitChangedFiles runs git status --porcelain with cwd set to the
project root. Porcelain is indifferent to cwd in both directions: it
prints repository-relative paths (the format deliberately ignores
status.relativePaths) and it reports the whole repository, not just cwd.
Those paths went straight into path.join(projectRoot, filePath), producing
<repo>/<sub>/<sub>/… — a path that cannot exist. fs.readFileSync threw, and
the entry was swallowed by the logDebug('Skipping unreadable file while detecting changes') guard, so added and modified came back empty for
every edit
. The failure was silent and total, not partial.

getGitVisibleFiles — the scan/index path — was never affected, because
git ls-files is both cwd-relative and cwd-scoped. That asymmetry between the
two git fast paths is exactly why status and sync disagreed, and it is now
pinned by a test so it stays deliberate.

What this does

Detection is fully automatic — no codegraph.json option added, nothing for a
user to opt into. Scoping git status to the subtree is also strictly cheaper in
a large monorepo than reporting the whole repository and discarding most of it.

Tests

New __tests__/git-changed-subdir-project.test.ts, 11 cases. 8 of them
fail on unmodified main
(expected [ 'app/src/index.js' ] to include 'src/index.js', and expected [] to include 'src/index.js' through the public
CodeGraph.getChangedFiles) and pass here; the other 3 are regression guards for
behaviour that already worked.

Modified / added / deleted reported with project-relative paths · sibling-package
edits ignored · status agreeing with sync end to end through the public
API
· the embedded-repo recursion below a subdirectory project (#1213) · the
project's own .gitignore (#766) · the gitignored-project fallback · project ==
repository root · a project not in git at all.

npx vitest run __tests__/git-changed-subdir-project.test.ts
     Tests  11 passed (11)

npx vitest run __tests__/git-changed-untracked-dir.test.ts __tests__/git-hooks.test.ts \
              __tests__/include-ignored-config.test.ts __tests__/sync.test.ts \
              __tests__/sync-rebuild-convergence.test.ts
     Tests  69 passed (69)

Full suite (Windows): 3004 passed / 27 failed / 219 skipped. Those 27 failures
reproduce test-for-test on unmodified HEAD — 26 are the documented Windows
EPERM temp-dir-cleanup quirk, the rest the explore-* budget gates — so none is
introduced here. tsc --noEmit clean.

Also

A user-facing CHANGELOG.md entry under [Unreleased] → Fixes, per the
CHANGELOG rules in CLAUDE.md. No version bump.

🤖 Generated with Claude Code

… its git repo

## Problem

`codegraph status` printed `Index is up to date` no matter how many files had
changed, whenever the indexed project sat below its repository root (a monorepo
package, an `app/` folder beside a `server/` one). `codegraph sync` run
immediately after found the same files and reindexed them, so the two commands
flatly contradicted each other, and anything trusting the status count — the
staleness reminder, a scripted check — read the index as clean forever.

`getGitChangedFiles` runs `git status --porcelain` with `cwd` set to the PROJECT
root. Porcelain prints REPOSITORY-relative paths from any cwd (the format
deliberately ignores `status.relativePaths`) and reports the whole repository
rather than just `cwd`. `getChangedFiles` then did
`path.join(projectRoot, filePath)`, producing `<repo>/<sub>/<sub>/...`. No such
file exists, `fs.readFileSync` threw, and the entry was dropped by
`logDebug('Skipping unreadable file while detecting changes')` — so `added` and
`modified` came back empty for every edit. The failure was silent and total, not
partial.

`getGitVisibleFiles` (the scan/index path) was never affected: `git ls-files` is
both cwd-relative and cwd-scoped. That asymmetry is exactly why `status` and
`sync` disagreed, and it is now pinned by a test.

Reproduced both ways before the fix: project in a subdirectory → "up to date"
with a modified file; project at the repository root → "Modified: 1 files".

## Fix

- `collectGitStatus` resolves where its directory sits inside the repository
  (`git rev-parse --show-prefix`), scopes `git status` to that subtree with a
  `-- .` pathspec, strips the prefix from every reported path, and drops
  anything outside — a sibling package's edits are no longer counted as this
  project's. A project that IS the repository root runs the byte-identical
  command it ran before and skips the prefix logic entirely.
- The prefix is resolved per recursion level rather than threaded from the
  caller, so the embedded-repo recursion (colbymchenry#1213, colbymchenry#970, colbymchenry#976) keeps working
  unchanged: those calls land on real repo roots and resolve an empty prefix.
- The ignore matchers (colbymchenry#766) now compare against a path that is genuinely
  relative to the directory whose `.gitignore` built them — before the fix a
  subdirectory project matched its own rules against repo-relative paths.
- A project inside a directory its parent repository GITIGNORES now declines the
  git fast path instead of reading git's silence as "nothing changed".
  `getGitVisibleFiles` already made that call; the shared `gitScopeIsIgnored`
  helper makes both paths make it identically, which is the invariant this bug
  violated.

Detection is fully automatic — no `codegraph.json` option was added. Scoping
`git status` to the subtree also makes it cheaper in a large monorepo than
reporting the whole repository and discarding most of it.

## Tests

New `__tests__/git-changed-subdir-project.test.ts`, 11 cases: modified / added /
deleted with project-relative paths, sibling-package edits ignored, `status`
agreeing with `sync` end to end through the public API, the embedded-repo
recursion below a subdirectory project, the project's own `.gitignore`, the
gitignored-project fallback, project == repository root, and a non-git project.
8 of the 11 fail on the unmodified code (`expected [ 'app/src/index.js' ] to
include 'src/index.js'`, and `expected [] to include 'src/index.js'` through
`CodeGraph.getChangedFiles`); all 11 pass after.

Full suite on Windows: 3004 passed / 27 failed / 219 skipped. Those 27 failures
reproduce test-for-test on unmodified HEAD — 26 are the documented Windows
`EPERM` temp-dir-cleanup quirk, the rest the `explore-*` budget gates — so none
is introduced here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant