Skip to content

git queue tui — interactive queue/history editor (TUI) #1

Description

@freshtonic

Problem Statement

Editing a queue today means either git queue edit — a text file of [branch]
sections where the commit sequence is frozen and all you can do is reassign
commits to branches by moving headers — or dropping to raw git rebase -i and
hand-managing the stack. Neither lets a user shape the history itself — reorder
commits, squash adjacent ones, split one commit into several, reword, or delete —
while staying inside git-queue's model of a queue line, branches,
and Stable-Commit-Ids. The text-file edit is deliberately safe
(no commit is rewritten) but therefore can't do history surgery;
rebase -i can, but is blind to the queue, its branch boundaries, and change
identity. A user who wants to tidy a stack before submitting has no single,
guided, interactive place to do it.

Solution

A new git queue tui subcommand: an interactive, keyboard-driven terminal UI for
editing the current queue line — both its branch
boundaries (as edit already allows, ref-only and safe) and the
commits themselves (reorder, squash, split, reword, delete). Edits mutate real git
state immediately (ADR-0002), so the panes always show true, materialised
history; a per-operation undo/redo stack and a suspend-to-shell conflict
escape make immediate mutation safe. A three-pane layout shows the queue on the
left, an editable commit message top-right, and the selected commit's read-only
diff bottom-right. git queue edit is untouched and remains the safe, ref-only,
non-TTY/scriptable path.

User Stories

  1. As a stack author, I want to open the whole current queue line in an interactive UI, so that I can shape it without hand-editing a text file or juggling rebase -i.
  2. As a stack author, I want the queue rendered front (oldest, merges first) at the top down to the tip, so that the ordering matches how the queue merges.
  3. As a stack author, I want each branch shown as a section with its commits listed beneath, so that I can see which change belongs to which branch at a glance.
  4. As a stack author, I want to reorder a commit up or down within the line, so that I can put changes in the order I want them to merge.
  5. As a stack author, I want a reorder to happen atomically and immediately, so that the displayed history is always the real history.
  6. As a stack author, when a reorder produces conflicts, I want to be prompted "resolve now or undo?", so that I decide whether to fix it or back out cleanly.
  7. As a stack author, when I choose to resolve, I want to be dropped into the shell in the standard mid-rebase state, so that I resolve the conflict with my normal git workflow and tools.
  8. As a stack author, after resolving in the shell, I want to re-enter the TUI and have it resume where it left off, so that I don't lose my place.
  9. As a stack author, when I choose to undo a conflicting operation, I want it reverted with nothing left applied, so that a bad move costs me nothing.
  10. As a stack author, I want to squash a commit into its adjacent (older) neighbour, so that I can fold a fixup into the change it belongs to.
  11. As a stack author, I want squash restricted to adjacent commits, so that the operation stays predictable.
  12. As a stack author, when I squash, I want the older commit to keep its Stable-Commit-Id and the newer to be absorbed, so that change identity follows the jj convention we adopted.
  13. As a stack author, when I squash two commits that both have messages, I want to be prompted with a combined description to trim, so that no message text is silently lost.
  14. As a stack author, when I squash across a branch boundary, I want the result to land in the older branch, so that the merge order is preserved.
  15. As a stack author, when a cross-boundary squash empties the newer branch, I want to be prompted to dissolve it, so that I don't leave a zero-commit branch behind.
  16. As a stack author, I want to split one commit into several, so that I can separate concerns that were committed together.
  17. As a stack author, I want to select what goes into each piece at the line level, so that I can divide a commit even when two changes share a hunk.
  18. As a stack author, I want split to be a two-way operation I can repeat, so that I can produce as many pieces as I need.
  19. As a stack author, when I split, I want the older piece to keep the original Stable-Commit-Id and message and each peeled piece to get a fresh id and a message I write, so that identity follows the jj convention.
  20. As a stack author, I want to reword a commit's message in an editable pane, so that I can improve messages without leaving the tool.
  21. As a stack author, I want a reword to preserve the commit's Stable-Commit-Id, so that the change keeps its identity across the rewrite.
  22. As a stack author, I want reword to apply only on an explicit action (not per keystroke), so that I'm not triggering a rebase on every character.
  23. As a stack author, when I navigate away from an edited-but-unapplied message, I want an apply/discard prompt, so that I neither lose my edit nor apply half-typed text.
  24. As a stack author, I want to delete a commit, so that I can drop a change I no longer want.
  25. As a stack author, when deleting a branch's only commit empties it, I want to be prompted to dissolve the branch, so that no zero-commit branch is left.
  26. As a stack author, I want to rename a branch, so that its name reflects what it now contains.
  27. As a stack author, I want to add a branch boundary to split one branch in two, so that I can divide a branch's commits.
  28. As a stack author, I want to remove a boundary to dissolve a branch into its neighbour, so that I can merge two branches' commits.
  29. As a stack author, I want to shift a boundary to move commits between adjacent branches, so that I can rebalance the stack.
  30. As a stack author, I want boundary edits to move refs only and never rewrite a commit, so that reassigning branches stays as safe as it is in edit.
  31. As a stack author, when I reorder a commit across a boundary, I want it reassigned to the branch whose run it lands in, so that ordering and branch membership stay consistent.
  32. As a stack author, I want unlimited undo within a session, so that I can back out any sequence of edits.
  33. As a stack author, I want redo, so that I can reapply an edit I undid.
  34. As a stack author, I want undo to restore branch refs and queue metadata exactly, so that undoing genuinely returns me to the prior state.
  35. As a stack author, I want a quit-guard when I exit with operations on the stack, so that I don't discard a session by a stray keystroke.
  36. As a stack author, I want every ref rewrite to land in git's reflog, so that even a closed session is recoverable by hand.
  37. As a stack author, I want the selected commit's own diff shown read-only, so that I can see exactly what that commit changes.
  38. As a stack author, I want +/-/hunk lines coloured, so that the diff is readable, even without syntax highlighting.
  39. As a stack author, I want binary files shown as a placeholder in the diff pane, so that the pane isn't flooded with unreadable bytes.
  40. As a stack author, I want to scroll the diff and message panes, so that I can read long content.
  41. As a stack author, I want vim-style keybindings, so that navigation feels natural in a terminal git tool.
  42. As a stack author, I want to click to select a commit, click to focus a pane, and use the wheel to scroll, so that basic navigation works with the mouse.
  43. As a stack author, during a split I want to click lines to toggle which piece they go in, so that selection is quick.
  44. As a stack author, when an operation leaves a commit empty and description-less, I want it auto-dropped, so that I don't accumulate empty commits.
  45. As a stack author, when an empty commit has a description, I want it kept and shown as (empty), so that an intentional marker commit isn't eaten.
  46. As a stack author, I want explicit delete to always remove a commit whether or not it's empty, so that delete is unambiguous.
  47. As a stack author on an untracked branch, I want it to open as one provisional section over trunk, so that I can divide its commits into a queue.
  48. As a stack author, I want the TUI to require a clean worktree, so that constant rebasing has a safe starting point.
  49. As a stack author, I want the TUI to refuse an empty queue with a clear message, so that I'm not shown an empty editor.
  50. As a stack author, when I quit, I want HEAD returned to the branch I launched from at its new tip (or its nearest surviving neighbour if it was dissolved), so that I end up somewhere sensible.
  51. As a stack author, when I quit, I want the new branch layout printed with a reminder to run git queue sync, so that I know how to update the PRs.
  52. As a stack author, when a branch I dissolved has an open PR, I want to be warned, so that I can close or repurpose it.
  53. As a stack author on a forked queue line, I want the TUI to refuse with a clear message, so that I'm never at risk of orphaning a sibling line by rewriting shared history.
  54. As a user without a TTY (pipe, CI, dumb terminal), I want git queue tui to error cleanly, so that I know to use git queue edit instead.
  55. As a user, I want git queue edit to keep working exactly as before, so that the safe, ref-only, scriptable path is still available.
  56. As a stack author, I want the queue never left with untracked commits, so that any commit the TUI creates (split pieces) is stamped with a Stable-Commit-Id automatically.
  57. As a stack author, I want a help view listing keybindings, so that I can discover what the TUI can do.

Implementation Decisions

  • New subcommand git queue tui. Added to the clap CLI in lib.rs and dispatched in commands.rs. edit is unchanged and remains the ref-only / non-TTY / scriptable counterpart. tui errors cleanly when stdout is not a TTY.
  • Scope = the current queue line. Reuse edit's scoping (Queue::load, line_through, base/branch resolution, untracked-branch-as-one-section). Refuse forked lines in v1 (a line whose editing range contains a fork) with a clear message; rewriting shared history is deferred. Require a clean worktree; bail on an empty queue.
  • Immediate mutation (ADR-0002). Every operation performs its git rewrite the moment it's made; no batched rebase-todo. The panes render true, materialised history.
  • Headless engine as a deep module, behind the view. A new engine module owns the editing state — the live commit sequence, the branch boundaries, and the undo/redo stack — and exposes operations. It drives git/meta (reusing git.rs, meta.rs, ident.rs, requeue.rs) for rebasing, ref moves, id stamping, and parent-pointer wiring. It is exposed from lib.rs so it is callable in-process by tests.
  • Operations are a data type. The view produces an Operation value from input; the engine applies it. Operations at minimum: reorder, squash, split, reword, delete, add-boundary, remove-boundary, move-boundary, rename-branch, undo, redo. This makes undo a snapshot↔operation pair, makes the engine directly testable, and leaves a door open to a future scripted mode (not built now).
  • Undo/redo. Before each operation, snapshot all queue-line refs plus queue metadata (parent pointers, cached PR numbers). Undo restores the snapshot; rewritten commit objects survive in git until GC. Unlimited within a session, session-scoped, redo mirror, git reflog as the durable cross-session backstop. Quit-guard on exit with a non-empty stack.
  • Conflict handling. An operation that conflicts prompts resolve or undo. Resolve suspends to the shell, leaving the user in the standard mid-rebase state (consistent with sync's resume-by-re-run); re-entry resumes the session. Undo backs the operation out. No predictive pre-conflict trial.
  • Stable-Commit-Id semantics (follow jj; see CONTEXT.md and ADR: no divergence).
    • Reorder / reword: id preserved.
    • Delete: id gone with the commit.
    • Squash (adjacent): older commit keeps its id; newer absorbed; descriptions combined (both non-empty → prompt; else the non-empty one). Cross-boundary squash lands in the older branch; dissolve-prompt if it empties the newer.
    • Split: line-level, two-way repeated for N pieces; older piece keeps original id + message; each peeled piece gets a freshly minted id + a written message.
    • Any commit the TUI creates is auto-stamped, so the queue never leaves with untracked commits. Ids stay globally unique — no jj-style divergence.
  • Empty commits (follow jj). Auto-drop a commit that becomes empty and has no description; keep and render (empty) if it has one; explicit delete always removes.
  • UI on ratatui + crossterm (ADR-0001). Three panes: queue (left), editable message (top-right, explicit-apply via Ctrl-S/confirm with an apply/discard prompt on navigate-away), read-only diff (bottom-right — the selected commit's own diff, our +/-/hunk colouring, no syntax highlighting, binary → placeholder). During a split, the diff pane flips to an interactive line-selection mode.
  • Keyboard: vim idiom. Mouse (v1): navigation/selection/scroll + split-toggle only — no drag-to-mutate.
  • Boundary edits stay ref-only. Rename / add / remove / shift boundaries move refs and rewrite no commit — edit's existing safety, generalised into engine operations.
  • Exit behaviour. Reuse edit's landing logic (return to the launch branch or nearest surviving neighbour), print the new layout, remind to run git queue sync, and warn about dissolved branches with open PRs (reuse edit's PR/remote-branch messaging).

Testing Decisions

  • What makes a good test here: it asserts external behaviour of the engine — given a starting repo and a scripted Vec<Operation>, the resulting branch tips, parent pointers, Stable-Commit-Ids, HEAD, and file contents — not the engine's internal state or the view's rendering internals.
  • Primary seam — the engine, in-process. Add engine integration tests that build the engine over a throwaway tempdir repo and apply operations-as-data, asserting git state. Prior art: tests/integration.rs (tempdir repos, git/git_out/commit/new_repo/is_ancestor helpers, gpgsign disabled for hermeticity). These tests call the engine as a library (via lib.rs) rather than spawning the binary, because the interactive view can't be driven through assert_cmd. Cover: reorder (clean and conflicting), squash (adjacent and cross-boundary, description combining, dissolve-prompt path), split (line-level, id/message assignment, N via repetition), reword (id preserved, descendants rebased), delete (and empties-branch dissolve), boundary edits (ref-only, no id change), reorder-across-boundary reassignment, undo/redo restoring exact refs and metadata, empty-commit auto-drop vs keep, forked-line refusal, clean-worktree and empty-queue guards, auto-stamping of new commits.
  • Pure helpers — existing render.rs seam. Unit-test diff +/-/hunk colouring, (empty) rendering, front→tip ordering, and pane layout math, like the existing render unit tests.
  • The ratatui view is deliberately not seam-tested beyond a smoke "it launches and quits." TUI end-to-end testing is expensive and low-value; the engine seam is what makes that acceptable.

Out of Scope

  • Forked-line editing. v1 refuses forked lines. Fast-follows: read-only-below-the-fork, then sibling-rebase.
  • Drag-to-mutate mouse (drag a commit to reorder, drag a boundary). v1 mouse is navigation/selection/scroll/split-toggle only.
  • In-diff syntax highlighting (no syntect); we colour +/-/hunks only.
  • Editing queue and branch descriptions (distinct from commit messages). Noted as a future feature; storage, status/submit surfacing, and UX all undecided.
  • A non-interactive scripted mode (feeding operations from a file). The operations-as-data design leaves the door open, but it is not built here.
  • Changes to git queue edit, sync, or submit. edit is untouched; sync stays explicit (not auto-run on quit).

Further Notes

  • The design paper trail is committed on branch tui-editor-design: glossary in CONTEXT.md; ADR-0001 (ratatui + crossterm, the deliberate departure from the tiny-dependency stance); ADR-0002 (immediate mutation with undo, over a batched rebase-todo model).
  • The existing edit command already separates "produce the assignment" from "apply it"; the engine generalises the apply half and the view replaces the interactive half, so this feature extends an existing shape rather than inventing a new one.
  • This spec is sized for splitting into tracer-bullet tickets (/to-tickets): a natural spine is engine scaffolding + operations-as-data → the ref-only boundary operations (safe, no rebase) → the rewriting operations (reorder/reword/delete) → squash → split with the line-level selector → undo/redo + conflict suspend/resume → the ratatui view and panes → mouse.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentSpec is ready to be picked up by an implementing agent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions