Skip to content

The git panel grows Zed's whole workflow: remotes, branches, a commit graph, and staging - #2

Merged
cesp99 merged 18 commits into
mainfrom
git
Aug 21, 2026
Merged

The git panel grows Zed's whole workflow: remotes, branches, a commit graph, and staging#2
cesp99 merged 18 commits into
mainfrom
git

Conversation

@cesp99

Copy link
Copy Markdown
Owner

Summary

This branch turns the git panel from "stage, unstage, discard, commit" into the real Zed git panel: everything you can do from Zed's git panel on desktop, reachable by touch, keyboard, and mouse, backed by the same Rust engine-side logic as the rest of the app.

Remotes (pull / push / publish / fetch)

  • A split action button at the top of the panel grows into fetch / pull / push / publish, with each remote command streaming git's stdout and stderr into a visible output view you can scroll while it runs (RemoteOutput, RemoteButton).
  • Push asks git for the upstream before publishing; force push exists and uses --force-with-lease; pull offers plain and rebase. Remote URLs are parsed and validated engine-side (GitRemoteUrl.kt), and pull/push commands run under a deadline so a dead network can never hang the panel.

The commit graph

  • The graph earns its sidebar: refs and lane dots from the engine's own git walking, per-commit file trees, paging, and a commit becoming a diff tab — unified diffs open as tabs and the branch picker opens the real diff of any branch ("Branch diff opens for real").
  • The footer names the last commit, and can take it back — uncommit, which pins its commit and only ever mutates the panel one operation at a time.

The branch picker

  • The branch name in the panel header (or title bar) opens Zed's branch picker: every local and remote branch with its last commit, filtered as you type, Enter checks out, a fresh name becomes Create Branch (off HEAD, or Ctrl+Enter off the default branch). Ctrl+Shift+Backspace deletes with warnings, Ctrl+Shift+I cycles the all/local/remote filter. docs/SHORTCUTS.md documents the whole thing, plus Ctrl+G as a leader for two-step chords (fetch, push, pull, force-push, pull-with-rebase, diff) — every chord is also a palette command, so it works with no keyboard at all.

Staging and commits

  • Amend via Ctrl+Shift+Enter, stage/unstage everything with Ctrl+Space / Ctrl+Shift+Space, Ctrl+1/Ctrl+2 switch Changes/History — and every mutation answers the panel's poll with its status.

Housekeeping, for review convenience

  • The last commit is a trim of the issue/PR templates only (the diff on .github/ is that, not part of the feature).
  • Removed two Kotlin entry points (CoreBridge.kt, GitSession.kt shrank or moved logic) and added Rust modules git_branches.rs, git_remotes.rs, git_patch.rs; JNI bridge extended in lockstep, editor logic stays in Rust, UI in Kotlin.
  • ~2,000 lines of new tests (engine Rust tests plus Compose tests for branch rows, remote output, chords, drafts, graph labels, staging) and an icon import script touch.

Testing

  • cd core && cargo test && cargo clippy
  • ./gradlew :app:testFullDebugUnitTest
  • Emerald/manual pass on the emulator with a real local repo: fetch/pull/push both streams, branch switch/create/delete via keyboard and touch, graph paging, per-commit diff tabs, uncommit, amend, and the Ctrl+G chord timeout.
  • Merge gates to run before this merges: ./gradlew assembleFullDebug assemblePlayDebug.

Checklist

  • Editor logic stays in Rust (core/), UI in Kotlin (app/); JNI boundary changes moved both sides together
  • Interactive features work by touch and keyboard and mouse; docs/SHORTCUTS.md updated
  • Nothing blocking on the main thread, no unrequested network or telemetry, no private info committed
  • Both editions considered — remote/push work is edition-agnostic; clone/apt gating unchanged

cesp99and others added 18 commits August 21, 2026 01:24
Co-Authored-By: Spettro <spettro@eyed.to>
Co-Authored-By: Spettro <spettro@eyed.to>
Co-Authored-By: Spettro <spettro@eyed.to>
Co-Authored-By: Spettro <spettro@eyed.to>
Co-Authored-By: Spettro <spettro@eyed.to>
Co-Authored-By: Spettro <spettro@eyed.to>
Co-Authored-By: Spettro <spettro@eyed.to>
Co-Authored-By: Spettro <spettro@eyed.to>
Co-Authored-By: Spettro <spettro@eyed.to>
Batch G of the review fixes — graph, hosting, avatars, diff parsing:
- G1: the graph's pager moves into GraphPaging, whose generation guard
strands any in-flight page when the version poll reloads the list —
no more scrambled lane order, no stale error killing the fresh
list's paging, and one page in flight at a time.
- G2: a detached HEAD's bare 'HEAD' chip dresses like any other chip,
as Zed's is_head_ref renders it — plain wash, no check icon.
- G3: one shared GitRemoteUrl parse (scheme, scp and bare spellings)
behind both githubRepoSlug and GitRemote.isGithub, so an '@' buried
in a path or a lookalike host can no longer pass for github.com and
the picker's glyph agrees with the sidebar's permalink gate.
- G4: avatar loads are single-flight per author (latecomers join the
first fetch instead of racing it to the same .part file), downloads
write under unique temp names, and the cache directory is pruned to
a 5MB budget — oldest first — once per session.
- G5: the patch parser reads 'new file mode'/'deleted file mode', so
an empty added or deleted file — and a pure rename — say what they
are instead of "Only the file's mode changed."
Co-Authored-By: Spettro <spettro@eyed.to>
The Changes tab kept its LazyColumn composed even with zero rows, as a
later sibling of the empty-state message inside the same Box. Compose
hit-tests later siblings first, and an empty list's scrollable modifier
still claims the pointer across its full fillMaxSize bounds — so every
tap on 'View Branch Diff' and 'Initialize Repository' landed on an
invisible, empty list and the buttons never fired, leaving both flows
dead on device even after their actions were wired correctly.
The list now leaves the composition entirely when there are no rows,
making the empty state the topmost hit target. Both buttons work end to
end on the emulator: View Branch Diff opens the 'Changes since master'
merge-base tab with content, and Initialize Repository runs git init
and redraws the panel as a fresh repository with its untracked files.
Co-Authored-By: Spettro <spettro@eyed.to>
Remote ops through run_git_split came back 'git exited with 2' with
both streams empty, while the identical command in the app's own
terminal printed two 'fatal:' lines and exited 128. The split wrapper
parked stderr under \${TMPDIR:-/tmp} — and inside the app process
TMPDIR names the host cache directory, set by Android's runtime after
fork (invisible in /proc/<pid>/environ, which only shows the initial
environment) and nonexistent inside the fake root. dash failed the
redirection before git ever ran, carried on to print both markers, and
its own 'cannot create' complaint sat on a stderr pipe the capture
discards on success. Exit 2 is dash's redirection-failure status, not
git's.
Two independent guards, either sufficient: the fixed guest environment
now pins TMPDIR=/tmp alongside PATH and HOME — the inherited value is a
host path for the same reason the inherited PATH is — so git's own
temp-file use is safe too; and the split wrapper parks under /tmp
literally, refusing to trust whatever TMPDIR says by the time it runs.
A successful capture now also logs any stderr the wrapper's shell left
behind, so this class of silent failure shows its sentence in the log.
Regression tests run the real wrapper under a hostile TMPDIR: through
the host's /bin/sh directly, and through the whole run_git_split chain
with a real git fetch failing against a nonexistent remote — both must
keep git's words and git's exit status. On the emulator the panel strip
now reads "git fetch failed: fatal: '/nonexistent-repo' does not appear
to be a git repository", exit 128 in the log, matching the terminal.
Co-Authored-By: Spettro <spettro@eyed.to>
A mutating command drops the cache's `scanned` flag, and whoever reads
the state right then renders "Asking git…". When the rescan's snapshot
came back identical — a failed push, a refused commit — the version
counter never moved, the panel's poll never re-read, and that transient
was the last thing it ever showed: tab switches and reopen included.
The version now bumps for a flag flip too (scanned coming back, `ran`
changing), while the diff gutter's generation stays content-only.
Co-Authored-By: Spettro <spettro@eyed.to>
Drop redundant markdown intros and duplicate fields, merge device/version
info into single inputs, and move the PR testing gates into a comment.
Co-Authored-By: Spettro <spettro@eyed.to>
@cesp99
cesp99 merged commit 0f699b2 into mainAug 21, 2026
2 checks passed
@cesp99
cesp99 deleted the git branch August 21, 2026 09:03
Sign up for freeto 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

@cesp99