Skip to content

Commit 19d5b07

Browse files
committed
Git browser: M4 polish
- Add **Settings > General > Git** section (`GitSection.svelte`) with three live toggles for repo chip, status column, and virtual portal. Refresh the helper text per the M4 spec; the existing chip/column descriptions get clearer wording too. - Wire `fileExplorer.git.showVirtualGitPortal` end-to-end: new Tauri command `set_show_virtual_git_portal` flips a process-global `AtomicBool` consulted on every git volume hook, so toggling makes `cd .git` switch instantly between virtual portal and raw on-disk listing. Seeded at startup from `Settings::show_virtual_git_portal`. - Plumb `FriendlyGitError` end-to-end. Three new variants (`ShallowBoundary`, `MissingObject`, `GitDirPermissionDenied`) plus the existing seven each map to the right `ErrorCategory` and carry warm copy. New `to_friendly_error()` builds a `volume::FriendlyError`; `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, and `ErrorPane` renders it directly. - Extend the existing `error_messages_never_contain_error_or_failed` rule to every git variant. - Add a Playwright spec (`git-portal.spec.ts`) that synthesizes a tiny git repo at test time and exercises the four key scenarios from the plan: `.git` shows virtual entries, `branches/main/` browses tree at HEAD, history-pane file preserves executable bit on disk, and toggling the portal off reveals raw `.git` contents. - Refactor `settings-applier.ts` to keep the cyclomatic complexity under the 15 cap by table-driving the passthrough handlers. - Update `CHANGELOG.md`: replace the M1-M3 stubs with a single user-facing block under `[Unreleased]` describing the five new things users can do. - Update every git-touching `CLAUDE.md` (`file_system/git/`, `file_system/volume/`, `file-explorer/git/`) and mark the git module complete in `docs/architecture.md`. `./scripts/check.sh` clean (Rust + Svelte + scripts + website + API). 1545 Svelte tests + 1465 Rust unit tests + 26 Rust integration tests pass; the new portal-toggle test and friendly-error sentinel round-trip test pass too.
1 parent 1ebcfa1 commit 19d5b07

20 files changed

Lines changed: 892 additions & 198 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,28 @@ The format is based on [keep a changelog](https://keepachangelog.com/en/1.1.0/),
99

1010
### Added
1111

12-
- **Git repo chip in the breadcrumb.** Open a folder inside a git clone and the breadcrumb shows a chip with the current
13-
branch, ahead/behind counts vs the upstream, and a "dirty" indicator when the worktree has uncommitted changes.
14-
Detached HEAD shows the short SHA; freshly-initialized repos show "no commits yet". Updates live as you commit, fetch,
15-
or branch from the terminal — no polling, no refresh button. Toggle via **Settings > General > Git > Show repo chip**.
16-
Foundation for the upcoming `.git` virtual portal (next milestone).
17-
- **Optional Git status column in Full mode.** Adds a single-glyph column (`M`, `A`, `D`, `?`, `!`) showing each file's
18-
git status. Hidden by default; enable in **Settings > General > Git > Show git status column**.
19-
- **Virtual `.git` portal.** Step into the `.git` folder of any clone and instead of seeing libgit internals, you'll see
20-
`branches/`, `tags/`, and a `raw/` escape hatch. Drill into a ref like `branches/main/` and you're browsing the
21-
working tree at that commit — preview files, sort, copy. Cross-volume copy works too: drag a file from
22-
`.git/branches/feature-x/src/foo.rs` into the working tree to pluck a single file from another branch, no
23-
`git checkout` needed. Breadcrumb segments inside the portal pick up a dedicated git-portal color so it's clear you're
24-
in history-land. Refs with slashes (like `feature/foo`) render as one entry, not nested folders. Read-only — writes
25-
are blocked at the volume layer.
26-
- **Commits, stash, worktrees, and submodules in the `.git` portal.** The portal now also exposes `commits/`, `stash/`,
27-
`worktrees/`, and `submodules/`. Open `commits/` to browse HEAD-reachable history (newest first, capped at 5000 with a
28-
"Load more" entry); each entry's name shows the short SHA plus the subject, and dates drive the date-sort. You can
29-
also type or paste any commit SHA directly — `.git/commits/<sha>/...` resolves even for unreachable commits in shallow
30-
clones. Stash entries appear as `stash/0/`, `stash/1/`, … browsing the working-tree state at stash time. Linked
31-
worktrees and submodules show as redirect entries: opening one navigates straight to its working dir (which is itself
32-
a git portal — turtles all the way down). The chip and listings stay live as you commit, stash, or `git worktree add`
33-
from the terminal.
12+
- **Git browser.** Cmdr now treats every git repo as first-class. Five things you can do that you couldn't before:
13+
- **See branch + dirty state at a glance.** Open a folder inside a git clone and the breadcrumb shows a pill with the
14+
current branch, ahead/behind counts vs upstream, and a dirty indicator. Detached HEAD shows the short SHA; brand-new
15+
repos show "no commits yet". The pill updates live as you commit, fetch, or branch from a terminal — no polling, no
16+
refresh button.
17+
- **Browse history as folders.** Step into `.git` and see `branches/`, `tags/`, `commits/`, `stash/`, `worktrees/`,
18+
`submodules/`, plus a `raw/` escape hatch. Drill into `branches/main/` (or `commits/<sha>/`) and you're browsing the
19+
working tree at that point in history. Refs with slashes like `feature/foo` render as one entry, not nested folders.
20+
Breadcrumb segments inside the portal use a dedicated git-portal color so it's clear you're in history-land.
21+
- **Pluck a single file from another branch or commit.** Drag a file out of the history pane into the working tree.
22+
Cmdr preserves the bytes and the executable bit. No `git checkout`, no risk of touching anything else. Type or paste
23+
any SHA directly to reach unreachable commits, even in shallow clones.
24+
- **Open linked worktrees and submodules with one keypress.** Each shows as a redirect entry — opening it jumps
25+
straight to its working dir, which is itself a git portal. Turtles all the way down.
26+
- **Show per-file git status in Full mode.** Optional column with single-glyph codes (`M`, `A`, `D`, `?`, `!`) and
27+
long-form `aria-label`s for screen readers. Off by default; enable in **Settings > General > Git**.
28+
- **Friendly errors for the git browser.** Whatever goes wrong — the repo's damaged, the worktree's orphaned, the
29+
commit's beyond a shallow boundary, the `.git` folder isn't readable, the file's too big to load from history, another
30+
git command holds the index — Cmdr shows a warm, plain-language explanation in the error pane plus a concrete next
31+
step, not a raw stack trace.
32+
- **Toggle each piece independently.** Three switches in **Settings > General > Git**: show the repo chip, show the
33+
status column, and show the virtual portal. Disabling the portal lets `.git` browse like any other folder.
3434

3535
## [0.14.0] - 2026-04-26
3636

apps/desktop/src-tauri/src/commands/settings.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ pub fn set_error_reports_enabled(value: bool) {
108108
crate::error_reporter::auto_dispatcher::set_enabled(value);
109109
}
110110

111+
/// Enable or disable the virtual `.git` portal. When off, navigating into
112+
/// `.git` shows the raw on-disk contents instead of the branches/tags/commits
113+
/// virtual folders. Pushed live from the frontend whenever
114+
/// `fileExplorer.git.showVirtualGitPortal` changes.
115+
#[tauri::command]
116+
pub fn set_show_virtual_git_portal(enabled: bool) {
117+
crate::file_system::git::set_virtual_portal_enabled(enabled);
118+
}
119+
111120
/// Update menu accelerator for a command.
112121
/// Called from frontend when keyboard shortcuts are changed.
113122
#[tauri::command]

apps/desktop/src-tauri/src/file_system/git/CLAUDE.md

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
# File system › git (M1 foundation + M2 virtual portal + M3 history)
2-
3-
Backend module for the git browser. M1 ships repo discovery, repo info,
4-
status, the watcher, and friendly errors. M2 adds the virtual `.git`
5-
portal — `branches/`, `tags/`, `raw/` browsable as virtual trees, with
6-
cross-volume copy "for free" because git blobs flow through the existing
7-
`VolumeReadStream` abstraction. **M3 (this milestone) fills in commits,
8-
stash, worktrees, and submodules. The first two browse a commit tree
9-
just like branches/tags. The latter two surface `redirectToPath` so the
10-
frontend opens the worktree's / submodule's working dir directly — those
11-
working dirs are themselves git portals, so the experience cascades for
12-
free.**
1+
# File system › git (complete: M1 + M2 + M3 + M4)
2+
3+
Backend module for the git browser. M1 shipped repo discovery, repo
4+
info, status, the watcher, and the friendly-error skeleton. M2 added
5+
the virtual `.git` portal — `branches/`, `tags/`, `raw/` browsable as
6+
virtual trees, with cross-volume copy "for free" because git blobs flow
7+
through the existing `VolumeReadStream` abstraction. M3 filled in
8+
commits, stash, worktrees, and submodules: the first two browse a
9+
commit tree just like branches/tags; the latter two surface
10+
`redirectToPath` so the frontend opens the worktree's / submodule's
11+
working dir directly. **M4 (this milestone) adds three things: a live
12+
toggle for the portal so `cd .git` can fall through to raw on-disk
13+
contents, FriendlyError integration end-to-end so every git failure
14+
reaches `ErrorPane` with a warm title + explanation + suggestion, and
15+
three new error variants (`ShallowBoundary`, `MissingObject`,
16+
`GitDirPermissionDenied`).**
1317

1418
## File map
1519

@@ -27,7 +31,7 @@ free.**
2731
| `read_blob.rs` | `GitBlobReadStream` — owns the full `Vec<u8>` and yields 256 KB chunks. See *Honest blob streaming* below |
2832
| `status.rs` | `list_status(repo, dir)` shells out to `git status --porcelain=v2 -z`. Parses the output into a `Vec<EntryStatus>` |
2933
| `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 |
30-
| `friendly.rs` | `FriendlyGitError`, `FriendlyGitErrorKind`seven variants (M1's six + `BlobTooLarge`). Active-voice copy, no "error" / "failed" |
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 |
3135
| `tests.rs` | M1 tests: discover, repo_info, status, friendly errors |
3236
| `m2_tests.rs` | M2 tests: classify, list_branches/tags/root, list_tree, blob-read parity with `git show`, cross-volume copy round-trip |
3337
| `m3_tests.rs` | M3 tests: list_commits + sha browsing + cancellation + 1000-commit walk (`#[ignore]`), list_stashes, list_worktrees + redirect, list_submodules + redirect, watcher invalidation for `commits/` |
@@ -41,6 +45,7 @@ Wired from `commands/file_system/git.rs`:
4145
- `subscribe_git_state(repo_root) -> RepoInfo` — registers a subscriber, returns current `RepoInfo` synchronously, then emits `git-state-changed` events
4246
- `unsubscribe_git_state(repo_root) -> ()` — drops one subscriber; tears down the watcher when refcount hits zero
4347
- `get_git_status_for_paths(repo_root, dir) -> TimedOut<Vec<EntryStatus>>` — porcelain v2 walk, 5 s timeout
48+
- `set_show_virtual_git_portal(enabled)` (in `commands::settings`) — flips the live portal toggle. Pushed by `settings-applier.ts` whenever `fileExplorer.git.showVirtualGitPortal` changes
4449

4550
## Watcher path set
4651

@@ -110,6 +115,31 @@ Branches like `feature/foo` show as a single entry called `feature/foo`, not nes
110115

111116
## Decisions
112117

118+
**Decision (M4)**: Live-toggleable portal via a process-global `AtomicBool`
119+
**Why**: `try_route_listing` / `try_route_metadata` / `try_open_blob_stream`
120+
each early-return `None` when the toggle is off, falling through to the
121+
real-FS path. This keeps the toggle a no-op cost (one atomic load per
122+
hook call) and makes "show me the raw `.git`" instant — no listing
123+
cache invalidation, no IPC dance. The setter is wired live from the
124+
frontend (`set_show_virtual_git_portal`) and seeded at startup from
125+
`Settings::show_virtual_git_portal`. Mutation guards (`is_virtual` in
126+
`local_posix`) intentionally don't consult the toggle: even with the
127+
portal off we don't want Cmdr to write to `.git/HEAD` from a copy
128+
dialog. Power users who really want to mutate `.git` use a terminal.
129+
130+
**Decision (M4)**: Carry git-friendly payloads through `VolumeError::IoError`
131+
**Why**: `volume_hooks` return `Result<_, VolumeError>` (the contract is
132+
fixed), but the streaming pipeline calls `friendly_error_from_volume_error`
133+
to compute the `ErrorPane` payload — and that function previously knew
134+
nothing about git. Adding a `Friendly(FriendlyError)` variant to
135+
`VolumeError` would ripple through ~12 call sites. Instead, we serialize
136+
`FriendlyGitError` into the `IoError::message` field with a sentinel
137+
prefix (`__GIT_FRIENDLY__:<token>:<path>:<title>: <explanation>`) and
138+
have `friendly_error_from_volume_error` recognize and decode it
139+
up-front. Round-trip tested in `friendly::tests`. The encoded form
140+
also reads naturally in logs (`grep "__GIT_FRIENDLY__"` finds every
141+
git failure that bubbled to the user).
142+
113143
**Decision (M3)**: Shell out to `git stash list` rather than driving gix
114144
**Why**: gix 0.81 doesn't expose a public stash-list API. We could parse
115145
the `refs/stash` reflog by hand, but `git stash list -z --format=%H%x09%gd%x09%s%x09%ct`

0 commit comments

Comments
 (0)