Focus on your code. Your PRs move with you.
Stacked GitHub pull requests, native to Jujutsu.
jj-stack manages a series of jj changes as a stack of
GitHub pull requests. Split, squash, reorder, or revise as you
please; jj-stack keeps your PRs in sync.
Latest release: v0.1.5
local jj history
trunk() ← A ← B ← C
❯ jj-stack submit
GitHub PR stack
main ← PR 1 ← PR 2 ← PR 3
✓ edits keep the same pull requests
Your local history determines the stack
Choose the top change in your stack, and jj-stack follows its parents back to trunk to find
the rest. Submitting creates or updates one PR per change, in the same order. Each PR targets
the branch below it, so reviewers can consider one change at a time while you keep working on
the changes that depend on it.
Jujutsu makes rewriting history cheap, but keeping your PRs in sync on GitHub adds considerable
drag. jj-stack handles GitHub book-keeping for you. Rewrite your local history however you
like, run jj-stack submit, and it updates and reorganizes your existing GitHub PRs to match.
---
config:
block:
padding: 32
---
block-beta
columns 3
L["Local jj"] space G["GitHub"]
C["C: UI"] space P3["PR #3"]
B["B: API"] space P2["PR #2"]
A["A: model"] space P1["PR #1"]
T["trunk()"] space M["main"]
T --> A
A --> B
B --> C
M --> P1
P1 --> P2
P2 --> P3
A -.-> P1
B -.-> P2
C -.-> P3
style L fill:none,stroke:none
style G fill:none,stroke:noneFrom local changes to merged PRs
Write with jj
Create, split, squash, and reorder changes with the tools you already use.
Submit for review with jj-stack
Run jj-stack submit to create or update the pull requests for your stack.
Revise, resubmit, then merge
Submit again after making changes. Run jj-stack merge when the PRs at the
bottom are ready.
jj-stack merge waits for GitHub, showing progress through its merge queue, then updates your
local stack and remaining PRs automatically. Use --no-wait to return early, or press Ctrl-C
to stop waiting without cancelling the merge. After GitHub finishes, run
jj-stack sync <head-change-id>. Run the same sync after merging through GitHub’s UI.
Watch the workflow
Edit and reorder your work, resubmit to refresh GitHub, then merge the finished stack.
Scroll through the full transcript
# Three local changes: A → B → C
❯ jj log
@ sknwynsz C: add UI
○ rmrupltr B: add API
○ rxpopzrx A: refactor shared model
◆ nkvluzuu base
│
~
❯ jj-stack submit
Selected: C: add UI (sknwynsz)
Submitted changes:
@ sknwynsz C: add UI: PR #3
│
○ rmrupltr B: add API: PR #2
│
○ rxpopzrx A: refactor shared model: PR #1
│
◆ nkvluzuu base
│
Top of stack: PR #3
Created GitHub stack #1.
# Edit B, insert D between B and C
❯ jj edit rmrupltr
Working copy (@) now at: rmrupltr cbe81d50 B: add API
Parent commit (@-) : rxpopzrx ae21ee9b A: refactor shared model
Added 0 files, modified 0 files, removed 1 files
❯ printf 'typed response\n' >> api.py
❯ jj new -m 'D: validate API input'
Rebased 1 descendant commits onto updated working copy.
Working copy (@) now at: trymwsxs de4227bc (empty) D: validate API input
Parent commit (@-) : rmrupltr 1e892022 B: add API
❯ printf 'validate input\n' > validation.py
❯ jj rebase -r sknwynsz -d @
Rebased 1 commits to destination.
❯ jj edit sknwynsz
Working copy (@) now at: sknwynsz 15996403 C: add UI
Parent commit (@-) : trymwsxs 896893e2 D: validate API input
Added 1 files, modified 0 files, removed 0 files
# The local stack is now A → B → D → C
❯ jj log
@ sknwynsz C: add UI
○ trymwsxs D: validate API input
○ rmrupltr B: add API
○ rxpopzrx A: refactor shared model
◆ nkvluzuu base
│
~
❯ jj-stack submit
Selected: C: add UI (sknwynsz)
Submitted changes:
@ sknwynsz C: add UI: pushed, PR #3 updated
│
○ trymwsxs D: validate API input: PR #4
│
○ rmrupltr B: add API: pushed, PR #2 unchanged
│
○ rxpopzrx A: refactor shared model: already pushed, PR #1 unchanged
│
◆ nkvluzuu base
│
Top of stack: PR #3
Dissolved GitHub stack #1, created GitHub stack #2.
# After review, merge the stack
❯ jj-stack merge --method rebase
Waiting for GitHub to merge. Ctrl-C stops waiting without cancelling.
Trunk: main, observed at "base"
Merge completed:
✓ GitHub merge request: merged PRs #1, #2, #4, #3 into main through change sknwynsz (commit ID
15996403)
GitHub reported final trunk commit 067fd467.
Updating the local stack after the completed merge:
Removing merged changes from the bottom of the stack: rxpopzrx, rmrupltr, trymwsxs, sknwynsz
Cleanup:
✓ remote branch: delete jj-stack/a-refactor-shared-model-rxpopzrx@origin
✓ forget the saved link between PR #1 and rxpopzrx
✓ remote branch: delete jj-stack/b-add-api-rmrupltr@origin
✓ forget the saved link between PR #2 and rmrupltr
✓ remote branch: delete jj-stack/d-validate-api-input-trymwsxs@origin
✓ forget the saved link between PR #4 and trymwsxs
✓ remote branch: delete jj-stack/c-add-ui-sknwynsz@origin
✓ forget the saved link between PR #3 and sknwynsz
❯ jj log
@ wotlvxnu
◆ sknwynsz C: add UI
◆ trymwsxs D: validate API input
◆ rmrupltr B: add API
◆ rxpopzrx A: refactor shared model
◆ nkvluzuu base
│
~
Keep the same PRs as your work changes
jj-stack follows change IDs across rewrites, so edits keep their PRs, comments, and review
history. Your local history and the PRs on GitHub can change independently. Before updating a PR,
jj-stack checks that it still belongs to the intended local change and that its branch
hasn’t moved unexpectedly. If those checks fail, the command stops and tells you what to
inspect.