Repository navigation
fix(sync): detect changes when the project lives in a subdirectory of its git repo - #1636
Closed
innoxxicide wants to merge 1 commit into
Closed
innoxxicide 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>
colbymchenry
added a commit
that referenced
this pull request
Oct 27, 2026
… changes When the indexed project is a folder of its git repository (a monorepo package, an app/ folder beside a server/ one), `codegraph status`, `status --json` and the freshness check behind codegraph_status reported the index up to date while `codegraph sync` found the very same changes. `git status --porcelain` prints paths from the repository root whatever folder it runs in, and reports the whole repository. Change detection joined `app/src/index.js` under the project, found nothing at `app/app/src/index.js`, and dropped it. The dirty paths a sync records for the restore check (from PR #1878) were stored the same way, so a file put back with git after a sync was missed too. And a project in a folder its repository gitignores got a permanently clean answer from git, which says nothing about an ignored folder. Change detection now asks where the project sits in its repository (`git rev-parse --show-prefix`, kept per folder, and skipped when the folder has its own .git). Below the root it runs git status with a `-- .` pathspec, takes the prefix off every path and leaves out anything outside the folder. The project's ignore rules match those project-relative paths, the recorded dirty paths are project-relative, and a project its repository ignores declines the git path as the scan already did, so callers take the full scan. A project at its repository root runs exactly the git command it ran before. Validation: - git-status-subdir-project.test.ts (new, 7 cases): edited, added and deleted files in a subdir project, matching sync; edits outside the project left out, with the pathspec asserted; the restore case, across a reopen; the project's own anchored .gitignore rule; an embedded repo inside the project; the gitignored-project fallback; and a repo-root project running exactly today's status command with no prefix call. 6 fail on main's source; the repo-root control passes on both. Four mutations (no pathspec, ignore rules on repo-relative paths, no fallback, no .git shortcut) each fail the test. - CLI repros: after an edit and an added file in app/, status --json reads 0/0/0 on main and 1 added + 1 modified with the fix; the same in a gitignored project; a subdir restore reads 0 on main and 1 modified with the fix (a repo-root project reads 1 on both). - The MCP freshness worker on a subdir project: 0/0/0 on main, 1 added + 1 modified with the fix. Reported in PR #1636 by @innoxxicide. This reimplements the fix independently, on top of the restore check that landed after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
added a commit
that referenced
this pull request
Oct 27, 2026
… changes When the indexed project is a folder of its git repository (a monorepo package, an app/ folder beside a server/ one), `codegraph status`, `status --json` and the freshness check behind codegraph_status reported the index up to date while `codegraph sync` found the very same changes. `git status --porcelain` prints paths from the repository root whatever folder it runs in, and reports the whole repository. Change detection joined `app/src/index.js` under the project, found nothing at `app/app/src/index.js`, and dropped it. The dirty paths a sync records for the restore check (from PR #1878) were stored the same way, so a file put back with git after a sync was missed too. And a project in a folder its repository gitignores got a permanently clean answer from git, which says nothing about an ignored folder. Change detection now asks where the project sits in its repository (`git rev-parse --show-prefix`, kept per folder, and skipped when the folder has its own .git). Below the root it runs git status with a `-- .` pathspec, takes the prefix off every path and leaves out anything outside the folder. The project's ignore rules match those project-relative paths, the recorded dirty paths are project-relative, and a project its repository ignores declines the git path as the scan already did, so callers take the full scan. A project at its repository root runs exactly the git command it ran before. Validation: - git-status-subdir-project.test.ts (new, 7 cases): edited, added and deleted files in a subdir project, matching sync; edits outside the project left out, with the pathspec asserted; the restore case, across a reopen; the project's own anchored .gitignore rule; an embedded repo inside the project; the gitignored-project fallback; and a repo-root project running exactly today's status command with no prefix call. 6 fail on main's source; the repo-root control passes on both. Four mutations (no pathspec, ignore rules on repo-relative paths, no fallback, no .git shortcut) each fail the test. - CLI repros: after an edit and an added file in app/, status --json reads 0/0/0 on main and 1 added + 1 modified with the fix; the same in a gitignored project; a subdir restore reads 0 on main and 1 modified with the fix (a repo-root project reads 1 on both). - The MCP freshness worker on a subdir project: 0/0/0 on main, 1 added + 1 modified with the fix. Reported in PR #1636 by @innoxxicide. This reimplements the fix independently, on top of the restore check that landed after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
added a commit
that referenced
this pull request
Oct 27, 2026
… changes (#2932) When the indexed project is a folder of its git repository (a monorepo package, an app/ folder beside a server/ one), `codegraph status`, `status --json` and the freshness check behind codegraph_status reported the index up to date while `codegraph sync` found the very same changes. `git status --porcelain` prints paths from the repository root whatever folder it runs in, and reports the whole repository. Change detection joined `app/src/index.js` under the project, found nothing at `app/app/src/index.js`, and dropped it. The dirty paths a sync records for the restore check (from PR #1878) were stored the same way, so a file put back with git after a sync was missed too. And a project in a folder its repository gitignores got a permanently clean answer from git, which says nothing about an ignored folder. Change detection now asks where the project sits in its repository (`git rev-parse --show-prefix`, kept per folder, and skipped when the folder has its own .git). Below the root it runs git status with a `-- .` pathspec, takes the prefix off every path and leaves out anything outside the folder. The project's ignore rules match those project-relative paths, the recorded dirty paths are project-relative, and a project its repository ignores declines the git path as the scan already did, so callers take the full scan. A project at its repository root runs exactly the git command it ran before. Validation: - git-status-subdir-project.test.ts (new, 7 cases): edited, added and deleted files in a subdir project, matching sync; edits outside the project left out, with the pathspec asserted; the restore case, across a reopen; the project's own anchored .gitignore rule; an embedded repo inside the project; the gitignored-project fallback; and a repo-root project running exactly today's status command with no prefix call. 6 fail on main's source; the repo-root control passes on both. Four mutations (no pathspec, ignore rules on repo-relative paths, no fallback, no .git shortcut) each fail the test. - CLI repros: after an edit and an added file in app/, status --json reads 0/0/0 on main and 1 added + 1 modified with the fix; the same in a gitignored project; a subdir restore reads 0 on main and 1 modified with the fix (a repo-root project reads 1 on both). - The MCP freshness worker on a subdir project: 0/0/0 on main, 1 added + 1 modified with the fix. Reported in PR #1636 by @innoxxicide. This reimplements the fix independently, on top of the restore check that landed after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
|
Thank you, @innoxxicide, for tracking this down so carefully: both root causes you found were right, the repository-relative porcelain paths and the gitignored project folder. We've landed the fix in #2932, reimplemented on top of the newer restore check (#1878), with credit to you in the changelog. A project at its repository root still runs exactly the same git command, and a project in a subfolder now asks git about its own folder only. Closing this in favor of it; it ships in the next release. |
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