You found a bug in an open-source project and want to submit a fix.
# Fork the repo on GitHub, then clone your fork
jj git clone git@github.com:you/project.git
cd project
# Add the upstream remote
jj git remote add upstream git@github.com:owner/project.git
# Make your fix
jj new main
# ... edit files ...
jj commit -m "fix: handle nil pointer in user lookup"
# Send one PR against the upstream repo ("s" is an alias for "send")
jip s --upstream upstreamThat's it. jip creates a bookmark, pushes it to your fork, and opens a single PR against the upstream repo. No stack involved.
Your jj log now looks like:
@ lxmvutkn alice@example.com 2026-02-26 21:17:48 2f3e3a16
│ (empty) (no description set)
○ ntwlropr alice@example.com 2026-02-26 21:17:48 f6b29d43
│ fix: handle nil pointer in user lookup
○ szzvpuxl alice@example.com 2026-02-26 21:17:20 main 602123cc
│ initial commit
◆ zzzzzzzz root() 00000000
If the reviewer requests changes:
# You're already on the working copy child of your fix
# ... edit files ...
jj squash
# Update the PR (jip posts a comment showing what changed)
jip s --upstream upstreamYou need to make four changes to a project. Three are related (data model → store → API endpoint), one is unrelated (docs). You decide to split them into two stacks.
# Stack 1: data model → store → API endpoint
jj new main
# ... create user type ...
jj commit -m "feat: add user data model"
# ... create store ...
jj commit -m "feat: add user store"
# ... create handler ...
jj commit -m "feat: add user API endpoint"
# Stack 2: unrelated docs change (branching from main, not from the stack above)
jj new main
# ... update docs ...
jj commit -m "docs: update README with getting started section"Your jj log now shows two independent stacks branching from main:
# Send both stacks at once (tips of each stack, jip resolves ancestors)
jip s qnv ppxjip creates 4 PRs total: 3 for stack 1 (each building on the previous), 1 for the docs change.
The reviewer requests changes to the user store (PR #2) — they want a List
method added.
# Fix the user store commit
jj new pkn
# ... add List method ...
jj squash
# Update all PRs (jip posts comments showing what changed)
jip s qnv ppx
# Or combine rebase + send in one step
jip s --rebase qnv ppxThe user store PR (#2) gets a comment showing the added List method. The API
endpoint PR (#3) is rebased but its content didn't change. The data model PR
(#1) is unaffected. Reviewers see comments showing exactly what changed.
The reviewer approves and merges the data model PR (#1):
jj git fetch
jj rebase -o main
# Update all existing (-x/--existing) PRs for changes that are descendants of main
jip s --existing main::
# Or combine rebase + send in one step
jip s -x --rebase main::Now the user store PR targets main directly. The stack shortened itself one
PR at a time.
Meanwhile the docs PR was reviewed and merged independently — it was never part of the same stack.
Your team maintains a release branch. It's time to merge the 35 commits that
have landed on main since the last release.
# Fetch latest state
jj git fetch
# Create a merge commit
jj new release main -m "chore: merge main into release"
jj new
# Send as a single PR (all 35 commits bundled, not stacked)
jip s --base release --no-stackThe --no-stack flag tells jip to send a single PR for the tip of the stack
rather than one PR per commit. The reviewer sees one PR with the full diff from
release to main.
You're contributing to a project and have several independent fixes and
improvements. Instead of sending them one by one, you create all changes
branching from main and send them in one go.
# Fix 1
jj new main
# ... edit files ...
jj commit -m "fix: set default config path in Dockerfiles"
# Fix 2
jj new main
# ... edit files ...
jj commit -m "feat: add health check endpoint"
# Fix 3: a small stack (two related changes)
jj new main
# ... edit files ...
jj commit -m "docs: add getting started section to README"
# ... edit more files ...
jj commit -m "nit: fix typos in README"
# Fix 4
jj new main
# ... edit files ...
jj commit -m "feat: support environment variable overrides"Now merge all branches together so you have a single working copy on top:
# See the "jj log" below to identify the change-ids
jj new zyx ab mno wx -m "private: local merge"The private: prefix is key — if configured, jip skips private changes, so the
merge commit won't get its own PR. To make this work, configure private commits
in ~/.config/jj/config.toml:
[git]
# Ensure commit messages prefixed with "wip:" or "private:" are not pushed
private-commits = "description(glob-i:'wip:*') | description(glob-i:'private:*')"Your jj log now looks something like:
@ kkvmxlpw alice@example.com 2026-03-10 21:07:13 a1b2c3d4
│ (empty) (no description set)
○ ppnrqxyz alice@example.com 2026-03-10 21:06:32 e5f6a7b8
├─┬─┬─╮ private: local merge
│ │ │ ○ wxrstuvq alice@example.com 2026-03-10 10:13:07 c9d0e1f2
│ │ │ │ feat: support environment variable overrides
│ │ ○ │ mnopqrst alice@example.com 2026-03-10 10:39:57 3a4b5c6d
│ │ │ │ nit: fix typos in README
│ │ ○ │ ghijklmn alice@example.com 2026-03-10 10:38:12 7e8f9a0b
│ │ ├─╯ docs: add getting started section to README
│ ○ │ abcdefgh alice@example.com 2026-03-10 10:20:00 1c2d3e4f
│ ├─╯ feat: add health check endpoint
○ │ zyxwvuts alice@example.com 2026-03-10 10:05:00 5a6b7c8d
├─╯ fix: set default config path in Dockerfiles
◆ tzmypqws maintainer@example.com 2026-03-10 06:21:07 main 49ea0086
│ Fix lint errors (#591)
jip sendjip walks all ancestors of @, skips the private merge commit, and creates a
PR for each change (or a stacked PR for the README chain). One command,
multiple PRs.
# Fix the config path commit
jj new zyx
# ... make changes ...
jj squash
# Update all PRs
jj rebase -b ppn -o main
jip send ppn
# Or rebase and send in one step
jip send --rebase ppnIf you only want to update PRs that already exist on GitHub (without creating
new ones), use --existing:
jip send --existingCreate a new working copy on top of the commit you want to change, make your edits, then squash them into it. jip posts diff comments on all affected PRs.
jj new <change-id>
# ... make changes ...
jj squash
jip sjip posts a comment on the updated PR showing exactly what changed:
Use jj new to insert a commit at the right position:
# Insert a commit after <parent-change-id>
jj new <parent-change-id>
# ... make changes ...
jj commit -m "fix: add input validation"
# jj automatically rebases descendants
jip sUse jj rebase:
# Move commit B to come after commit C instead of before it
jj rebase -r <B-change-id> -A <C-change-id>
jip sThe squashed-away commit's PR becomes orphaned. You should close it manually on GitHub. The remaining commit's PR is updated normally.
Yes. If your revset resolves to a single commit, jip creates a single PR with no stack navigation in the description.


