stackmap is a local-first terminal map for Git branches and Graphite stacks.
It keeps the full local branch topology open in a compact, searchable TUI,
shows parent-relative branch diffstats and named-stack base-to-tip totals, marks worktree safety state, refreshes after
external Git changes, and optionally adds matching GitHub pull requests.
The current release is the unsigned, not-notarized 0.1.0-alpha.1 preview for
macOS 15+ on Apple Silicon and Intel. See the feature guide,
support policy, and release verification guide.
Requirements:
- Rust 1.88 or newer
- Git
- Graphite CLI metadata (optional)
- GitHub CLI authenticated for the repository (optional)
Download the matching arm64 or x86_64 archive from
GitHub Releases, verify it
using the release guide, or build from a tagged checkout:
cargo test --locked
cargo install --path . --lockedStackmap is not published to crates.io, so cargo install stackmap is not a
supported installation path during the alpha.
Then open any local repository:
cd /path/to/repository
stackmapYou can also pass the repository path directly:
stackmap /path/to/repository
stackmap --current /path/to/repository--current opens only the current branch's stack plus its shared ancestry and
trunk. Without it, every configured Graphite trunk and its stacks are visible.
stackmap /repo live
[staging]
│ ○ child-stack-B 11m +6 -1
└─┐
│ ○ child-stack-A 35m +18 -3
└─┐
● › │ ○ feature/stack-root 2h +120 -14 ⎇ repo
└─┐
○ staging 1d +? -?
3/4 recent All pitch:auto a View Archive x archive v range X delete ? help
Each section grows upward from its trunk at the bottom. The first child stays
on the direct lane; additional children split one lane from their exact parent.
All branches in one linear stack share a lane and label column, while sibling
stacks move over by one lane. The trunk connector touches the trunk circle, and
trunk names are bold with a reserved color. Every configured local Graphite
trunk gets a section; invalid, unknown-parent, and Git-only branches remain
visible in a final Untrunked section.
Recent ordering is the default: recently active stacks sit nearest the trunk, with progressively older stacks above them. Stacks still precede one-off branches. The order picker also provides Alphabetical and Graphite ordering.
Markers are independent of color:
›selected branch; its accent spans the full terminal row○topology node●checked-out branch in the fixed left status column◉checked-out trunk (○for any other trunk)*dirty current worktree⎇ namechecked out in the named current or linked worktree■included in the current archive/restore range
Stack colors are deterministic. Yellow is reserved for PR data, while green
and red represent insertions and deletions. Set NO_COLOR=1 for non-color
output. At 120 columns and wider, press d to show or hide a detail pane for
the selected branch's exact local commit time and PR title. It starts hidden.
Visual feature sections split a real stack into repository-local presentation
ranges without changing Git or Graphite. Press i on a branch to start or
remove a section. Successive sections indent branch-name text while leaving
the real circles, rails, and connectors fixed. Custom stack and section titles
remain white while branch names, dividers, and topology retain identity colors.
A named stack gets its own title row, followed by one blank hierarchy row. That title shows the net diff from the validated parent of the stack's bottom branch to its real displayed tip; it is not a sum of branch rows. Named visual sections remain directly above the first visible branch they own.
| Key | Action |
|---|---|
Up / Down, j / k |
Move through visible branches |
Shift+Up / Shift+Down, Command+Up / Command+Down, J / K |
Move to the adjacent stack head; move 10 branches when outside a true stack |
Option+Up / Option+Down, g / G |
Move to the top / bottom branch of the current trunk section |
t |
Toggle Recent / Graphite stack order |
T |
Open the Recent / Alphabetical / Graphite order picker |
h |
Focus the selected stack and shared ancestry; repeat to show all |
H |
Focus the selected trunk (or Untrunked); repeat to show all |
s |
Toggle blank rows between adjacent stacks (on by default) |
d |
Show/hide the wide branch-detail sidebar (off by default) |
+ / - / 0 |
Increase / decrease lane pitch; reset to automatic width |
/ |
Filter by branch name; ancestors remain as dimmed context |
Enter, Enter |
Arm and confirm switching the selected branch; one Enter edits a selected label |
i |
Add/remove a purely visual section boundary on the selected branch |
c / C |
Cycle/open color for the selected section boundary/label, otherwise its stack |
n |
Create a missing stack/section label and edit it inline |
x |
Archive/restore the selected branch and move focus to the nearest branch above |
v, arrows, Enter |
Preview and apply a contiguous archive/restore range |
a |
Toggle Active / Archive view |
X, then y / n |
Confirm or cancel guarded deletion of one exact local branch |
r |
Force repository reconciliation |
o / y |
Open or copy the selected PR URL |
? |
Show help and provider status |
Esc |
Close help/message or cancel a filter edit |
q, Ctrl-C |
Quit |
Checkout is intentionally conservative. The first Enter arms the selected branch and the
second confirms it; Esc or navigation cancels. stackmap then runs an exact git switch -- <branch> after re-reading live repository state. It never stashes, resets,
cleans, deletes, or forces. Git's normal overwrite and worktree protections are
preserved, and errors leave the current tree untouched.
Deletion is local-only, exact-target, confirmed, and non-force. Stackmap never
deletes remotes or closes pull requests. It refuses the current branch,
configured trunks, worktree-owned branches, unsafe repository states, stale
tips, degraded Graphite topology, and tracked branches with children.
Definitely untracked branches must already be merged into the current HEAD
and are deleted with an atomic expected-object-ID ref transaction. Eligible
Graphite-tracked leaves use only the installed CLI's characterized
noninteractive single-branch command; Graphite provides no expected-OID
transaction, so Stackmap performs immediate preflight and post-read checks and
blocks further deletion when the observed result is inconsistent.
Archiving is the normal cleanup operation and never changes Git. Archived names
persist in <git-common-dir>/stackmap/config.toml, are hidden from Active view,
and can always be restored from Archive view. The current branch and trunks
cannot be archived. Active and Archive rows show compact configured-upstream
and local remote-ref state in the right-side metadata: ✓ pushed, ↑n ahead,
↓n behind, ↕n/n div, × gone, ○ no remote, or ? remote. Dim,
nonselectable ancestry keeps each archived branch oriented in its stack. This
evidence never fetches: ○ no remote means there is no configured upstream and
no current local remote-tracking ref contains the commit, not that the commit
is absent from the server. Unknown evidence is intentionally not treated as
safe deletion evidence.
- Git refs are authoritative; Graphite only supplies validated parent edges.
- Filesystem events and a 30-second reconciliation tick feed a one-slot refresh queue. There is at most one active refresh and one pending request.
- Structural snapshots publish immediately while an independent latest-state diff coordinator uses at most four workers and a 2,048-entry object-pair cache shared by branch and stack-title diffs. Older enriched snapshots cannot replace newer structure. Branch/shared results publish before aggregate-only stack work, so title summaries cannot delay ordinary branch evidence.
- All inter-thread queues, subprocess output, and caches are bounded.
- Immutable snapshots are replaced as a unit; stale diff snapshots are rejected by generation, while delayed PR results require a matching branch and object ID.
- Rendering visits only visible rows. The full branch model remains internally scrollable and searchable.
- Structural topology indexes and lane intervals remain linear in branches and
edges.
t,h,H,s, search, and navigation project entirely in memory without Git, SQLite, Graphite, or GitHub work. - The implementation contains no application
unsafeblocks. Rust ownership, bounded queues/caches, subprocess timeouts, and snapshot-release tests protect the long-running process from retained generations and unbounded growth. - Visible-row remote-ref checks use one active/latest-pending coordinator with bounded targets, output, result queue, deadline, and cache in both Active and Archive views. View and scroll changes replace obsolete work.
Run the deterministic 500-branch projection benchmark with:
cargo bench --bench responsiveness --lockedColor overrides are stored outside tracked files at:
<git-common-dir>/stackmap/config.toml
Writes use a bounded cross-process lock, synced temporary file, and atomic rename. Before saving, stackmap reloads the latest configuration so separate worktrees changing different stack roots do not overwrite each other. Invalid on-disk edits leave the last valid in-memory configuration active.
Graphite's SQLite database is private and versioned outside this project.
stackmap opens it read-only and requires the branch_metadata.branch_name
and parent_branch_name columns plus readable ordered trunks configuration.
The optional children column supplies default Graphite order when compatible.
Unsupported,
missing, busy, or corrupt metadata produces an explicit topology-unavailable
state; every Git-local branch remains visible as an independent root. The tool
never writes or migrates Graphite files.
GitHub enrichment is optional. A single-flight, TTL-limited bounded gh pr list
request retrieves PRs in all states, and results attach only when branch name and tip
object ID still match. Missing auth, offline operation, timeout, or malformed
JSON is shown as provider state and does not affect local navigation.
| Symptom | Meaning / action |
|---|---|
topology unavailable |
Graphite metadata is missing or incompatible; Git branches are still complete |
| PR details are blank | Run gh auth status; local behavior does not require GitHub |
| Checkout is disabled | The branch is current, another worktree owns it, or Git has an operation in progress |
| Git blocks checkout | Commit/move the conflicting work yourself; stackmap deliberately performs no cleanup |
| Shift-arrow does not jump | This is terminal encoding, not Vim. Use J / K, or configure the terminal to send ESC [ 1 ; 2 A / ESC [ 1 ; 2 B for Shift+Up / Shift+Down. Stackmap enables complete modifier reporting when the terminal supports the enhanced keyboard protocol. |
| Option-arrow does not jump | Use the terminal-compatible g / G fallback |
| A branch vanished from Active view | Press a, find it in Archive, then press x to restore it |
Archive says local only |
This is local no-fetch evidence; fetch manually if you need current server evidence |
| Footer still shows a completed checkout | Press r; reconciliation is target-aware and returns normal controls after success or a bounded timeout |
needs at least 40 columns |
Widen the pane; no branches were removed from the model |
cargo fmt --all -- --check
cargo clippy --locked --offline --all-targets --all-features -- -D warnings
cargo test --locked --offline --all-targets --all-features
cargo build --release --locked
scripts/check-package-contents.shThe integration suite uses real temporary Git repositories for inventory, refresh, and checkout protection. Other tests cover cycles and duplicate branches, Graphite schema degradation, fixed responsive fields, 500-branch reachability, bounded command output/cache size, stale GitHub responses, and release of obsolete snapshots across 100 refresh generations.