Repository navigation
fix(git): count the whole repository in the git count widgets - #823
Open
eric-engberg wants to merge 7 commits into
Open
eric-engberg wants to merge 7 commits into
eric-engberg wants to merge 7 commits into
Conversation
…elative set Git Changes, Git Insertions and Git Deletions add up `git diff --shortstat` and `git diff --cached --shortstat`, run from the session's directory. With `diff.relative = true` in the user's git config (git 2.28 and later), both commands only look at the current directory. A session in a subdirectory then counts only the lines changed under it: with +2 in a/x and +1 in b/y, a session in b/ showed (+1,-0), and one in an unchanged directory showed (+0,-0) or hid the widget under its zero state, while Git Unstaged Files on the same line, which reads the repo-wide `git status`, showed both files. Every diff the status line runs now gets `-c diff.relative=false`, so the counts cover the whole repository, as they do without that setting. The override goes in as config rather than as --no-relative because git before 2.28 rejects --no-relative but ignores the unknown setting.
Git Conflicts counts the paths `git ls-files --unmerged` prints, run from the session's directory. Without a pathspec, ls-files only lists that directory's subtree, so during a merge a session in a subdirectory missed every conflict outside it. With top.txt and sub/f.txt both conflicted, a session in sub/ showed 1, and one in docs/ showed 0, the clean glyph, or nothing under the zero hide state, while Git Status on the same line showed `!` from the repo-wide `git status`. The `:/` pathspec makes ls-files list the whole repository from any directory, so the count matches what Git Status reports. ls-files also gets --no-literal-pathspecs: with GIT_LITERAL_PATHSPECS=1 in the environment, git reads `:/` as a file name, and the count would drop to 0 even at the root, where it was right before. Git added that option together with the variable, in 1.8.2. With this and the previous change, every git count widget covers the whole repository, which the usage docs now say.
Git Untracked Files adds one for every `??` entry of `git status --porcelain`. In git's default untracked mode, a new directory is a single `?? dir/` entry however many files it holds, so a new folder with three files showed ?:1, and Git Staged Files jumped by three once they were added. With status.showUntrackedFiles=no in the user's config, the count was always 0. The count now comes from `git ls-files --others --exclude-standard -z -- :/`, which lists every untracked file in the repository, one by one, whatever that setting says, and skips what .gitignore excludes as git status does. Only Git Untracked Files runs it: Git Staged and Unstaged Files keep reading the `git status` call they share with Git Status, so they cost what they did, and a listing that fails can't take their counts down with it. The listing runs to megabytes when a large tree isn't ignored, such as a node_modules or .venv made before its .gitignore entry: a copy of this repo's node_modules, 36,530 files, lists in 2.25 MB, past execFileSync's 1 MiB default. The call gets room for about a million paths (64 MiB), and the git cache keeps the count rather than the listing, which every later read of the cache file would otherwise have to parse.
Git Untracked Files counted from `git ls-files --others`, which ignores status.showUntrackedFiles. Under `no` the line could show ?:5 next to a Git Status with no `?`, and someone who set `no` to skip the untracked walk in a large tree paid for that walk whenever the widget was on the line. The listing now runs only when `git status --porcelain -z`, the cached call Git Status and the staged and unstaged counts already share, lists an untracked entry. Without one the count is 0, as git status says: nothing is untracked, or status.showUntrackedFiles=no hides it all. In the default mode a new directory still counts each file inside it. When git status fails the count is 0 without a listing, as the staged and unstaged counts are.
Git Untracked Files now asks git status first and lists untracked files only when git status shows one. "returns zero when git fails" makes every git call throw, so it stops at git status and no longer reaches the listing. Nothing covered a listing that fails after git status showed an untracked file, such as one cut off by the 5 s timeout every git call has, where the count falls back to 0. A new test covers it: a status with `?? a.ts`, then a listing that throws, gives 0 after both calls. The test before it queued a listing output that was never used, since no listing runs when git status fails. vi.clearAllMocks() keeps queued values, so that output became the next test's git status. Its call count check already shows no listing runs, so the queued value is gone, and the new test queues only values it uses.
# Conflicts: # src/utils/__tests__/git.test.ts # src/utils/git.ts # src/widgets/__tests__/GitConflicts.test.ts
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.
What
Five git count widgets reported less than the repository holds:
⚠0/ the clean glyph / nothing, while Git Status on the same line showed!.diff.relative = true.All the git count widgets now count the whole repository, as Git Status does, wherever the session is. Git Untracked Files counts files, and still shows 0 under
status.showUntrackedFiles = no, wheregit statuslists no untracked files.docs/USAGE.mdsays so.Why
Claude Code sends the session's working directory as
cwd. That is often a subdirectory: a monorepo package the session was started in, or one Claude moved into. The other count widgets already read the repo-widegit status, so one line contradicted itself. Piped renders onmain(scratch repos):top.txtandsub/f.txtbothUU, line Git Status | Git Conflicts:!+* | ⚠2sub/:!+* | ⚠1docs/:!+* | ⚠0git config diff.relative true, +2 lines ina/xand +1 inb/y, line Git Changes | Git Unstaged Files:(+3,-0) | M:2b/:(+1,-0) | M:2c/:(+0,-0) | M:2, or Git Changes hidden under its zero statemkdir d && touch d/1 d/2 d/3: Git Untracked Files shows?:1. Runninggit add dthen moves Git Staged Files fromS:0toS:3.The causes:
git ls-files --unmergedwithout a pathspec lists only the current directory's subtree.diff.relativelimitsgit diff --shortstatto the current directory.git status --porcelainprints a new directory as a single?? d/entry.How
git ls-files --unmerged -- :/. The:/pathspec covers the whole repository from any directory.execGitputs--no-literal-pathspecsbefore everyls-files. WithGIT_LITERAL_PATHSPECS=1in the environment, git reads:/as a file name, and both counts that use it would be 0 even at the root. Git added that option in the same release as the variable (1.8.2).execGitputs-c diff.relative=falsebefore everydiff. It's set as config rather than--no-relativebecause git before 2.28 rejects--no-relativebut ignores an unknown setting. The other diff options (no external diff, no textconv, submodules) are unchanged.git ls-files --others --exclude-standard -z -- :/. That lists every untracked file in the repository one by one and skips what.gitignoreexcludes, as git status does. The listing runs only when thegit status --porcelain -zoutput that Git Status and Git Staged and Unstaged Files already share (the same cached call) lists an untracked??entry. Without one the count is 0, as git status says: nothing is untracked, orstatus.showUntrackedFiles = nokeeps untracked files out of it. When git status fails, the count is 0 without a listing, as Git Staged and Unstaged Files show then. A listing that fails, such as one cut off by the 5 s timeout every git call has, also shows 0. Git Staged and Unstaged Files keep reading that git status call, so they cost what they did before, and a listing that fails can't zero their counts.node_modulesor.venvcreated before its.gitignoreentry. A copy of this repo'snode_modules(36,530 files) lists in 2.25 MB, past execFileSync's default buffer. The call therefore gets 64 MiB (execGittakes an optionalmaxBuffer), and the git cache stores the count instead of the listing. The caching thatrunGitArgsdid now lives in a shared helper, so the count gets the same in-memory and on-disk cache.node_modulesuntracked next to one staged and one unstaged change:git status --porcelain -z25 ms (Git Status, Staged and Unstaged Files, unchanged),git ls-files --others146 ms. On a 20,000-file.venv: 24 ms and 46 ms. The listing runs only when Git Untracked Files is on the line and git status lists something untracked, so it never runs understatus.showUntrackedFiles = no. Git Untracked Files reads the cached git status the other git widgets share, so with Git Status or another file count on the line it adds nogit statuscall of its own.mainshows+*? S:1 M:1 ?:1and this branch shows+*? S:1 M:1 ?:36530.Unchanged: a session at the root, without
diff.relative, in a tree with no new untracked directories, sees the same numbers as before. Git Untracked Files still shows 0 understatus.showUntrackedFiles = noand whengit statusfails, as onmain. Agit ls-filespath is still printed relative to the session's directory, so the git cache stays per directory; its comment now says that instead of callingls-files --unmergedcwd-scoped.Overlap with open upstream PRs: #632, #797 and #798 change
git.tstoo and conflict with this one, and so does my #822 (git.tsand the git count widget). When either of #822 and this one merges, I'll mergemaininto the other. In that merge Git Untracked Files shows nothing whengit statusfails, as #822's other counts do.Demo
Piped renders of scratch repositories with fake data, run with
bun run src/ccstatusline.tsonmain(before) and on this branch (after).Git Conflicts, mid-merge, from the root,
packages/api/andpackages/web/Powerline
Before:
After:
Plain
Before:
After:
Git Untracked Files with a new folder of 4 files plus
notes.mdThe cases are the plain tree, the same tree under
status.showUntrackedFiles = no, and the tree aftergit add .. Before:?:2,?:0, thenS:5. After:?:5,?:0, thenS:5.Powerline
Before:
After:
Plain
Before:
After:
Git Changes / Insertions with
diff.relative = true, from the root,packages/web/anddocs/Powerline
Before:
After:
Plain
Before:
After:
Testing
mainas of 2.2.34 (0c94df7): it merges cleanly.bun run lintis clean, andbun teston Linux CI gives 4421 pass, 0 fail (4403 onmain). The built CLI under Bun 1.4.4 and Node 26.13.0 agrees across runtimes and matchesmainin plain and Powerline mode, and the TUI opens and exits cleanly under both. It adds no JavaScript or Node API newer than Node 14, the build's target.getGitFileStatusCountstest rewritten because that function no longer counts untracked files. Run againstmain's code, each alone, 13 of the 18 fail. (git.test.tsneeds a shim that exportsmain's untracked count under the new name to load there.) The other 5 pass onmainand guard what this branch adds: the untracked count's 0 when nothing is untracked and when git fails; its two cases that run no listing (git status lists nothing untracked, or fails), whichmainpasses because it only ever read git status; and conflicts withGIT_LITERAL_PATHSPECS=1, whichmaincounted at the root because it passed no pathspec.src/utils/__tests__/git-counts-exec.test.ts(7 tests). It runs real git with no mocks: it builds each repository state, calls the git helpers in a child process and, except understatus.showUntrackedFiles = no, checks that plain git run the same way counts less. The cases are line counts withdiff.relativefrom a subdirectory; conflicts outside the session's directory; conflicts withGIT_LITERAL_PATHSPECS=1; untracked files in a new directory; the same withGIT_LITERAL_PATHSPECS=1; no untracked files understatus.showUntrackedFiles = no, where git status lists none, and each one again once it is set tonormal; and staged, unstaged and untracked counts next to an untracked tree that lists in more than 1 MiB, where plain execFileSync throwsENOBUFS.git.test.ts(10 tests): the argumentsexecGitadds beforediffand beforels-files. For the untracked count: its arguments, a buffer above 1 MiB, no listing when git status lists nothing untracked or fails, 0 when nothing is untracked and when git fails, a listing that fails, and a persistent cache that keeps the count rather than the listing.?? a.tsand the untracked listing then throws. The count is 0 after both calls (the status, then thels-fileslisting), and the test fails if that fallback returns anything other than 0.GitConflicts.test.ts(1 test): the:/arguments.ls-fileslisting, the Git Untracked Files tests mock git status and then the listing, and the shared fake-git helper strips the argumentsexecGitnow adds.node node_modules/vitest/vitest.mjs runon Node 26.10.0. 300 of 316 pass on this branch, and 283 of 298 onmain(the same files without the new one). Every failure is ingit.test.ts, from spying onos.homedir, which Node's ESM forbids. That covers the same 15 tests on both, plus the new persistent-cache test, which uses the same temp-home helper. Every other new test passes under Node.runtime-check.shon Bun 1.4.2 and Node 26.10.0:main, with both runtimes agreeing.mainas intended, with both runtimes agreeing in plain and Powerline mode: conflicts fromdocs/, line counts from an unchanged subdirectory withdiff.relative = true, the file counts in thenode_modulestree, and conflicts plus untracked files at the root withGIT_LITERAL_PATHSPECS=1(theremainalso shows⚠2, and only the untracked count of a new directory differs).status.showUntrackedFiles = noit renders the same asmain, and without the setting it differs as intended, with both runtimes agreeing in plain and Powerline mode.dist/under Node 26.10.0 and Bun 1.4.2, the same on both:⚠2fromdocs/,(+3,-0)fromc/withdiff.relative,S:1 M:1 ?:36530in thenode_modulestree,⚠2 | ?:3at the root withGIT_LITERAL_PATHSPECS=1, and on a new 4-file folder plus one new file,?:0 | S:0understatus.showUntrackedFiles = noand? | ?:5 | S:0without it.