jj-stack

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

PR stack
local jj history
trunk() ← ABC

jj-stack submit

GitHub PR stack main ← PR 1PR 2PR 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:none
Each local change has one pull request. Reordering your changes and submitting again will update your PRs.

From local changes to merged PRs

01

Write with jj

Create, split, squash, and reorder changes with the tools you already use.

02

Submit for review with jj-stack

Run jj-stack submit to create or update the pull requests for your stack.

03

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.

Download the terminal recording

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.

Next steps