You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- New process-global `RwLock<HashMap<RepoRoot, CachedStatus>>` in `status.rs`. One full-repo `git status --porcelain=v2 -z --untracked-files=normal` per `.git/index` mtime change. Subsequent calls slice the cached map by `dir_in_worktree` in memory.
- Watcher invalidates the snapshot in `recompute_and_emit` (any `.git/*` mutation drops the cache; the next call repopulates) and on the last unsubscribe so unwatched repos don't pin a snapshot forever.
- Always run with `--untracked-files=normal`. Decision noted in `CLAUDE.md`: with the cache, the untracked walk is amortized; the earlier "skip outside root" sketch buys nothing.
- Tests: 4 slice tests (root, descendants, lookalikes, self-dir), 4 cache tests (hit, mtime invalidate, explicit invalidate, slice-from-cache).
- Bench split into cold + warm paths. On the 50k-file fixture: cold p50=84 ms / p95=99 ms, warm p50=15 µs / p95=18 µs (~5500× faster on cache hits).
Copy file name to clipboardExpand all lines: apps/desktop/src-tauri/src/file_system/git/CLAUDE.md
+24-1Lines changed: 24 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,7 +29,7 @@ three new error variants (`ShallowBoundary`, `MissingObject`,
29
29
|`submodules.rs`|`list_submodules` – gix `Repository::submodules()`. Each entry sets `redirect_to_path` to `<repo_root>/<rel-path>`|
30
30
|`tree.rs`|`list_tree`, `get_tree_entry`, `lookup_blob_id`, `read_blob` – gix tree walks. Permissions reflect `EntryKind::BlobExecutable` so cross-volume copy preserves the executable bit |
31
31
|`read_blob.rs`|`GitBlobReadStream` – owns the full `Vec<u8>` and yields 256 KB chunks. See *Honest blob streaming* below |
32
-
|`status.rs`|`list_status(repo, dir)`shells out to`git status --porcelain=v2 -z`. Parses the output into a `Vec<EntryStatus>`|
32
+
|`status.rs`|`list_status(repo, dir)`runs a full-repo`git status --porcelain=v2 -z` once per `.git/index` mtime, caches the result in a process-global `RwLock<HashMap<RepoRoot, CachedStatus>>`, and slices it by `dir`. The watcher invalidates the snapshot whenever `.git/*` changes. Parses porcelain v2 in `parse_porcelain_v2`.|
33
33
|`watcher.rs`|`GitWatcherRegistry` – per-repo notify-rs debouncer. `subscribe(app, root)` returns the current `RepoInfo` synchronously and emits `git-state-changed` on relevant `.git/*` mutations. 200 ms debounce. M2: also calls `notify_directory_changed(.., FullRefresh)` for any cached `.git/{branches,tags}/` listings on the local volume |
34
34
|`friendly.rs`|`FriendlyGitError`, `FriendlyGitErrorKind` – ten variants (M1's six, `BlobTooLarge` from M2, plus M4's `ShallowBoundary`, `MissingObject`, `GitDirPermissionDenied`). Active-voice copy, no "error" / "failed". `to_friendly_error()` builds a `volume::FriendlyError` for `ErrorPane`; `encode_for_volume_error()` + `try_decode_git_friendly()` carry the structured payload through `VolumeError::IoError` so the streaming pipeline rebuilds it on the way out |
35
35
|`column_meta.rs`| Per-row column-population helpers shared across `virtual_listing`, `log`, `tree`, etc. — `pluralize`, `ahead_behind_for_branch`, `commit_meta`, `files_changed_count`, `recursive_tree_size`, plus newest-of-set helpers for category-level Modified dates |
@@ -146,6 +146,29 @@ The frontend reads `display_size` / `display_size_tooltip` from `FileEntry`; the
146
146
147
147
## Decisions
148
148
149
+
**Decision (M4 follow-up)**: Cache `list_status` results keyed by `.git/index` mtime
150
+
**Why**: Status used to walk the worktree on every `listing-complete` (every nav,
151
+
every diff). On a 50k-file repo that's ~75 ms per nav. We now run one full-repo
152
+
walk per index change, store the result in a process-global
153
+
`RwLock<HashMap<RepoRoot, CachedStatus>>`, and slice by `dir_in_worktree` on
154
+
each call. Cached calls land sub-millisecond on the same fixture (warm p95 in
155
+
the bench is bounded by an arbitrary 5 ms ceiling so a busy CI doesn't flake).
156
+
The watcher (`watcher.rs::recompute_and_emit`) drops the cache entry on every
157
+
`.git/*` mutation it observes, so the next call repopulates. The
158
+
`unsubscribe`-on-last-pane path also drops the entry so an unwatched repo
159
+
doesn't pin a full-repo-sized snapshot.
160
+
161
+
**Decision (M4 follow-up)**: Always run with `--untracked-files=normal`, no
162
+
"skip untracked outside the worktree root" trick
163
+
**Why**: An earlier sketch had us pass `--untracked-files=no` when the caller
164
+
scoped to a sub-path inside the worktree, on the theory that listing a deep
165
+
subdir doesn't need the full untracked walk. With the cache above, the
166
+
untracked walk runs once per index change anyway and the cost is amortized
167
+
across every subsequent listing — the extra complexity (two code paths,
168
+
mismatched cache keys for the same repo) buys nothing measurable. We always
169
+
walk the full worktree with `--untracked-files=normal` and let the cache do
170
+
the work.
171
+
149
172
**Decision (M4)**: Live-toggleable portal via a process-global `AtomicBool`
0 commit comments