Skip to main content

jj-stack: manage stacked GitHub PRs with jj

jj-stack turns a linear series of local jj changes into a stack of GitHub pull requests. Rewrite, split, squash, or reorder the changes with jj, then run jj-stack submit to update GitHub. Existing PRs follow their change IDs, keeping comments and review history together.

Quick start

You need Python 3.14 or newer, jj 0.45.1 or newer, and a repo on github.com where you can push branches and open pull requests. jj-stack uses GITHUB_TOKEN, then GH_TOKEN, then your GitHub CLI login for authentication.

Install with uv:

uv tool install jj-stack

Submit your first stack

Start with a linear series of local jj changes on top of trunk(). Authenticate with gh auth login, or supply a token in GITHUB_TOKEN or GH_TOKEN. Check the repo setup and apply the safe local fixes:

jj-stack doctor --fix

Inspect the stack that ends at your working copy:

jj-stack

Create one GitHub PR per local change:

jj-stack submit

Revise the changes locally with jj and rerun jj-stack submit whenever the stack is ready to refresh. Use jj-stack list to see every tracked stack in the repo.

Mental model

Your local jj history determines which changes form a stack and their order. On GitHub, each change gets a stable PR branch and a PR. The bottom PR targets trunk by default, and each PR above it targets the PR branch below:

Local changes:  trunk() <- A     <- B     <- C
GitHub PRs:     main    <- PR #1 <- PR #2 <- PR #3

Each PR's diff shows only the changes it adds on top of its base, so reviewers can consider one change at a time. jj-stack manages the PR branches for you, and they normally stay out of your local bookmark view.

When you rewrite a change, its change ID still connects it to the same PR. Submitting again updates that PR and the PRs for any dependent changes, preserving their discussions.

To select a stack, pass the change ID of its head (the top change). jj-stack follows the head's parents back to trunk to find the rest. If you don't name a head, it uses your working copy when it has both a description and changes, or its parent otherwise. After editing a lower change, pass the head's ID to jj-stack submit so the update includes the changes above your edit.

Everyday workflow

  1. Write code as a series of local jj changes.
  2. Run jj-stack submit.
  3. Revise, add, remove, or reorder the changes locally as reviews come in.
  4. Run jj-stack submit again to refresh GitHub.
  5. Run jj-stack merge when the PRs at the bottom are ready. Pass --pull-request <pr> to stop at an earlier PR, and --method to choose a merge method your repo allows. The command waits for GitHub, including its merge queue, then updates your local stack and remaining PRs.
  6. If you used --no-wait, interrupted the wait, or merged through GitHub, run jj-stack sync <head-change-id> after GitHub finishes.

view, submit, merge, and sync accept a change ID when you need to select a stack other than the one ending at the working copy.

See the user guide for drafts, descriptions, merge queues, cleanup, and working with multiple stacks.

Optional setup

Invoke it as jj stack

Add a command alias to your user configuration with jj config edit --user:

[aliases]
stack = ["util", "exec", "--", "jj-stack"]

For tab completion of both jj-stack and jj stack, add the output of jj-stack completion to your shell startup file:

eval "$(jj-stack completion zsh --jj-alias stack)"

bash and fish work the same way. See Configuration for more setup options.

Other installation options

pipx provides another isolated installation:

pipx install jj-stack

You can also use pip inside an activated virtual environment:

python -m pip install jj-stack

To upgrade an installation made with uv, run uv tool upgrade jj-stack. If the command is not on your shell PATH, run uv tool update-shell.

The repository also provides a Nix flake. You can run it directly:

nix run github:bos/jj-stack -- --help

Or add it to your configs:

{
   # add the input
   inputs.jj-stack.url = "github:bos/jj-stack";
}
{
   environment.systemPackages = [
     inputs.jj-stack.packages.${pkgs.stdenv.hostPlatform.system}.jj-stack
   ];
}

From a checkout, nix build builds the package and nix develop enters a development shell with uv, jj, and just.

Learn more

For all flags and aliases, use the built-in help:

jj-stack --help
jj-stack <command> --help
jj-stack help --all

Coding agent integration

Install the bundled skill to give coding agents instructions for working with jj-stack:

gh skill install bos/jj-stack jj-stack

The skill source and evaluation notes are included in this repo.

Development

With uv, jj, and just installed, or in a nix develop shell that provides them, run just to list the development workflows. See CONTRIBUTING.md for setup and validation instructions.

Metadata

Release files for jj-stack 0.1.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jj-stack 0.1.8
File Size Uploaded Provenance
jj_stack-0.1.8.tar.gz 508.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for jj-stack 0.1.8
File Interpreter ABI Platform
jj_stack-0.1.8-py3-none-any.whl Python 3 none any Details

Total release size: 760.2 kB

Release files / jj_stack-0.1.8.tar.gz

Download URL jj_stack-0.1.8.tar.gz
Size 508.9 kB
Tags Source
SHA-256 checksum
How to use checksums
291ab154d75ee76d7f1a0680c6bbd26f6b6b5875b38909bfe7588b4a0451843e
BLAKE2b-256 checksum
How to use checksums
75e74eb8629b57c52e593cdbd8430513ab005060fb1373a1e906970c60d96f3e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / jj_stack-0.1.8-py3-none-any.whl

Download URL jj_stack-0.1.8-py3-none-any.whl
Size 251.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c1e6c8a8e64be0938b26b8c9a6fc290dbf9912970aea687a7a3c41661779229b
BLAKE2b-256 checksum
How to use checksums
ab19ce0acec6cd2a476c06f89ebd4d41f84d0805a645e5bcd79d155f82230e97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for jj-stack 0.1.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page