worktree: Collapse untracked directories in Status. Fixes #181 - #2282
Open
cedric-appdirect wants to merge 1 commit into
Open
worktree: Collapse untracked directories in Status. Fixes #181#2282cedric-appdirect wants to merge 1 commit into
cedric-appdirect wants to merge 1 commit into
Conversation
cedric-appdirect
force-pushed
the
status-collapse-untracked-dirs
branch
from
July 28, 2026 22:51
2450124 to
a16f93c
Compare
cedric-appdirect
marked this pull request as draft
July 28, 2026 22:52
…#181 Worktree.Status() listed every untracked file individually, while "git status" reports a fully untracked directory as a single entry by default (status.showUntrackedFiles=normal). On worktrees with many untracked files, such as dotfiles repositories opened over a home directory, Status() enumerated, lstat'ed and allocated a node, a change and a status entry per untracked file, taking minutes where git takes seconds. Add StatusOptions.UntrackedFiles mirroring git's status.showUntrackedFiles: UntrackedFilesNormal (default) collapses untracked directories into a single entry, UntrackedFilesAll restores the historical per-file listing, and UntrackedFilesNo omits untracked entries. The collapse is implemented via noder.Collapser and a CollapseUntrackedDirs option on the filesystem noder, which validates a directory with an early-exit scan instead of a full enumeration. Empty untracked directories are not reported, matching git. Internal callers that enumerate untracked files (Add, AddGlob, Clean and the reset conflict check) are pinned to UntrackedFilesAll. Benchmark on a 20k-untracked-file tree (BenchmarkStatusUntrackedDir): UntrackedFilesAll 1476ms/op vs UntrackedFilesNormal 903ms/op. Assisted-by: Cursor with Kimi k3 <noreply@moonshot.ai> Signed-off-by: Cedric BAIL <cedric.bail@appdirect.com>
cedric-appdirect
force-pushed
the
status-collapse-untracked-dirs
branch
from
July 28, 2026 23:06
a16f93c to
6daf5ee
Compare
5 tasks
cedric-appdirect
marked this pull request as ready for review
July 29, 2026 17:14
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.
Why
Worktree.Status()lists every untracked file individually, while referencegit statusreports a fully untracked directory as a single entry by default (status.showUntrackedFiles=normal, see git-status docs and status.showUntrackedFiles).On worktrees with many untracked, non-ignored files — e.g. dotfiles repositories opened over a home directory as reported in #181 —
Status()enumerates,lstats and allocates a merkletrie node, aChangeand aFileStatusper untracked file. In a reproduction with a 30k-untracked-file worktreeStatus()takes ~2s wheregit statustakes ~0.1s; the reporter measured 326s on their home directory.What
StatusOptions.UntrackedFilesmirroring git'sstatus.showUntrackedFiles:UntrackedFilesNormal(zero value, new default): untracked directories are reported as a single entry, likegit status(?? dir/).UntrackedFilesAll: every untracked file listed individually — go-git's historical behavior.UntrackedFilesNo: untracked entries omitted, likegit status -uno.noder.Collapserinterface lets a directory noder declare its subtree representable by one change;Changes.addRecursivethen emits a single change for the directory instead of descending.CollapseUntrackedDirsoption (requiresIndex, same contract asIgnoreMatcher). A directory collapses only when it has no tracked descendants and contains at least one visible (non-ignored) file, validated with an early-exit scan — so fully untracked directories are usually validated with oneReadDirper nesting level instead of a full enumeration. Empty untracked directories produce no entry, matching git.Add,AddGlob,Clean, and the KeepReset conflict check) are pinned toUntrackedFilesAlland behave exactly as before.The design mirrors upstream git's traversal (
dir.c):UntrackedFilesNormalmaps toDIR_SHOW_OTHER_DIRECTORIES | DIR_HIDE_EMPTY_DIRECTORIES, the tracked-descendant check todirectory_exists_in_index(), and the early-exit visibility scan toread_directory_recursive()'scheck_onlymode.Behavior change (v6)
Status()output for untracked files changes from per-file listing to collapsed directory entries (git parity). Consumers needing the old listing can opt intoStatusOptions{UntrackedFiles: UntrackedFilesAll}. Verified against referencegit status --porcelainon equivalent fixtures, including nested.gitignoreun-ignore rules and tracked/untracked mixes.Benchmarks
A/B comparison of the
main-branch binary vs this branch on an identical fixture (5,000 untracked files across 550 directories, 100 tracked files; Apple M4 Pro; 7 runs each,-benchtime=5x), viabenchstat:The included
BenchmarkStatusUntrackedDirreproduces this on demand (20k files / 2,200 dirs:All1476ms/op vsNormal903ms/op) and the status map holds 1 entry instead of 20,000. Note: the remaining cost is dominated by thegitignore.ReadPatternspre-walk, which still scans the whole worktree; making that walk lazy/incremental (as upstream'sprep_excludedoes) is a natural follow-up PR.Test plan
utils/merkletrie(collapse insert/delete/non-collapse),utils/merkletrie/filesystem(collapse truth table: empty dirs, tracked descendants, no-index no-op, ignored contents, file→dir replacement, symlinks)UntrackedFilesAll,UntrackedFilesNo, and anAdd("dir")regression testTestStatusIgnoredupdated to the git-parity output (verified againstgit status --porcelain)go test .: 758 passed, 0 failedgo test -race ./...: 4659 passed; 4 failures inplumbing/transport/httpbackend E2E tests that also fail on pristinemain(local git backend infrastructure limitation)golangci-lint runclean on changed packagesAI disclosure
This contribution was produced with AI assistance (Kimi k3), including profiling analysis of #181, implementation and tests. Commits carry an
Assisted-bytrailer per AI_POLICY.md. The behavior was cross-checked against referencegiton equivalent fixtures.