Skip to content

fix(status): a project below its repository root sees its uncommitted changes - #2932

Merged
colbymchenry merged 1 commit into
mainfrom
claude/fix-status-subdir-project
Oct 27, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/fix-status-subdir-project

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

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 line of codegraph_status said the index was up to date while codegraph sync found the very same edits, new files and deletions. They now report them. A file put back with git after a sync now shows as pending too, and a project in a folder that its repository's .gitignore leaves out is checked by the full scan instead of always reading as unchanged.

Reported in #1636 by @innoxxicide; this reimplements the fix independently, on current main (which since gained the restore check from #1878 and the committed-changes half from #1829).

Root cause

collectGitStatus runs git status --porcelain -z -uall in the project folder. Porcelain output is the same from any folder: paths run from the repository root, and the report covers the whole repository. For a project in app/, getChangedFiles joined app/src/index.js under the project, found nothing at app/app/src/index.js and dropped the entry, so every working-tree change disappeared. Three things followed:

The committed half (git diff --relative -- ., #1829) was already project-relative.

Fix

  • gitRepoPrefix(dir): where the folder sits in its repository (git rev-parse --show-prefix), kept per folder for the process's life. A folder with its own .git is a repository or worktree root and costs no git call at all.
  • Below the root, git status gets a -- . pathspec, so git reports only the project's folder. The prefix comes off every path, anything outside the folder is left out, and the ignore rules match the resulting project-relative paths. Embedded repos found under the project are repository roots and recurse with no prefix, as before.
  • indexed_dirty_paths comes from the same function, so it is project-relative now, and the restore check works for a subdir project. An index written by an older build may hold repository-relative dirty paths: they point at nothing, are skipped, and the next sync or index rewrites them.
  • The "does the enclosing repository ignore this folder" check (git check-ignore) moved into one helper that the scan and change detection both use. When it answers yes, getGitChangedFiles returns null and callers take the full scan that indexed the project.
  • A project at its repository root runs exactly the git command it ran before. It makes no extra git call; a test pins both.
  • Nothing here writes from codegraph status, which now opens read-only (fix(cli): codegraph status opens the index read-only, and says when a rebuild is running #2930). The dirty paths are still recorded only by index and sync.

Cost: a project below its repository root adds one rev-parse --show-prefix (about 29 ms here; once per process, but once per measurement in the MCP freshness worker, which starts fresh each time) and one check-ignore (about 29 ms) per change check. A repo-root project adds one existsSync.

Validation

  • __tests__/git-status-subdir-project.test.ts (new, 7 cases):

    • a subdir project: an edited, an added and a deleted file, from getGitChangedFiles and getChangedFiles, then sync agrees and status reads clean;
    • edits outside the project are left out, and git is asked with the -- . pathspec;
    • the restore case: a synced edit put back with git checkout reads as modified, also after a reopen, and a sync clears it;
    • the project's own .gitignore (an anchored /generated/ rule over a tracked file) is matched against project-relative paths;
    • an embedded repository inside the project;
    • a project its repository gitignores falls back to the full scan, which sees the edit and the new file;
    • a repo-root project runs exactly git status --porcelain --no-renames -z -uall, with no --show-prefix call over the index and status lifecycle and no check-ignore during status.
  • Red: 6 of 7 fail on main's src/extraction/index.ts. The repo-root control passes on both, as it should.

  • Mutations, each caught by the test: no pathspec; ignore rules matched on repository-relative paths; no gitignored fallback; no .git shortcut.

  • Existing related files pass: git-*, status-*, mcp-status-freshness, mcp-freshness-bounds, worktree* (11 files, 85 tests).

  • CLI, with the triage repro scripts, main 7ff2841 vs this branch:

    repro main this branch
    app/ project: edit + new file, status --json 0 added, 0 modified ("Index is up to date") 1 added, 1 modified
    after sync 0 0
    edits outside app/ only 0 0
    app/ gitignored by the repository: edit + new file 0, 0 1 added, 1 modified
    sync, then git checkout the file: repo-root project 1 modified 1 modified
    the same in an app/ project 0 1 modified
  • The MCP freshness worker (what codegraph_status reports as "Changes since index") on an app/ project after an edit and a new file: 0/0/0 on main, 1 added, 1 modified with the fix.

  • Full suite on the pushed commit 6e9834e (main 0174f80), with the kernel and CODEGRAPH_NO_DAEMON unset: 770 files, 9,089 passed, 0 failed, 85 skipped.

Left out

  • In a long-lived process, the prefix is kept per folder. A folder that later gets its own .git is picked up at once by the .git check. A repository created between the project and its old root while the process runs is not; that one needs a restart.

🤖 Generated with Claude Code

@colbymchenry
colbymchenry force-pushed the claude/fix-status-subdir-project branch from 8161073 to 76072ed Compare October 27, 2026 18:28
… 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
colbymchenry force-pushed the claude/fix-status-subdir-project branch from 76072ed to 6e9834e Compare October 27, 2026 18:40
@colbymchenry
colbymchenry merged commit 1cb4aab into main Oct 27, 2026
@colbymchenry
colbymchenry deleted the claude/fix-status-subdir-project branch October 27, 2026 18:56
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