fix(sync): detect changes when the project lives in a subdirectory of its git repo - #1636
Open
innoxxicide wants to merge 1 commit into
Open
fix(sync): detect changes when the project lives in a subdirectory of its git repo#1636innoxxicide wants to merge 1 commit into
innoxxicide wants to merge 1 commit into
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 statusprintedIndex is up to dateno matter how many files hadchanged, whenever the indexed project sat below its repository root — a
monorepo package, an
app/folder beside aserver/one.codegraph syncruna 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:
codegraph statusModified: 1 filesapp/of the repoIndex is up to dateWhat was wrong
getGitChangedFilesrunsgit status --porcelainwithcwdset to theproject root. Porcelain is indifferent to
cwdin both directions: itprints repository-relative paths (the format deliberately ignores
status.relativePaths) and it reports the whole repository, not justcwd.Those paths went straight into
path.join(projectRoot, filePath), producing<repo>/<sub>/<sub>/…— a path that cannot exist.fs.readFileSyncthrew, andthe entry was swallowed by the
logDebug('Skipping unreadable file while detecting changes')guard, soaddedandmodifiedcame back empty forevery edit. The failure was silent and total, not partial.
getGitVisibleFiles— the scan/index path — was never affected, becausegit ls-filesis both cwd-relative and cwd-scoped. That asymmetry between thetwo git fast paths is exactly why
statusandsyncdisagreed, and it is nowpinned by a test so it stays deliberate.
What this does
collectGitStatusresolves where its directory sits inside the repository(
git rev-parse --show-prefix), scopes the report to that subtree with a-- .pathspec, strips the prefix from every reported path, and drops anythingoutside it — so 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 caller, so the embedded-repo recursion (
codegraph syncsilently skips all untracked files #1213, Allow opting out of embedded git repo discovery in gitignored directories #970, [BUG] Currently, it is impossible to turn off the indexing of nested repositories #976) keeps workingunchanged: those calls land on real repo roots and resolve an empty prefix.
relative to the directory whose
.gitignorebuilt them. Before this, asubdirectory project matched its own rules against repository-relative paths,
so its
.gitignoresilently didn't apply.the git fast path instead of reading git's silence as "nothing changed".
getGitVisibleFilesalready made that call; the extractedgitScopeIsIgnoredhelper (lifted verbatim out of it, not reimplemented) makes both paths make it
identically. That shared invariant is the one this bug violated, so it
seemed worth enforcing structurally rather than fixing the one symptom.
Detection is fully automatic — no
codegraph.jsonoption added, nothing for auser to opt into. Scoping
git statusto the subtree is also strictly cheaper ina 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 themfail on unmodified
main(expected [ 'app/src/index.js' ] to include 'src/index.js', andexpected [] to include 'src/index.js'through the publicCodeGraph.getChangedFiles) and pass here; the other 3 are regression guards forbehaviour that already worked.
Modified / added / deleted reported with project-relative paths · sibling-package
edits ignored ·
statusagreeing withsyncend to end through the publicAPI · 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.
Full suite (Windows): 3004 passed / 27 failed / 219 skipped. Those 27 failures
reproduce test-for-test on unmodified
HEAD— 26 are the documented WindowsEPERMtemp-dir-cleanup quirk, the rest theexplore-*budget gates — so none isintroduced here.
tsc --noEmitclean.Also
A user-facing
CHANGELOG.mdentry under[Unreleased] → Fixes, per theCHANGELOG rules in
CLAUDE.md. No version bump.🤖 Generated with Claude Code