A CLI tool that syncs Jujutsu (jj) bookmarks with GitHub Pull Requests.
Core philosophy:
- No hidden state — everything is derived at runtime from
jj,gh, and change description trailers likePR: #1234 - jj owns structure — the DAG of changes
- GitHub owns presentation — PR titles, descriptions, review state
- Invalid states are first class — ambiguities and inconsistencies are clearly displayed to you and don't halt your work — fix them when you feel like it
cargo install --git https://github.com/hydro-project/jj-pr --locked
jj-pr util install-aliasesThe second line installs jj pr as a subcommand alias into your user config.
Requires jj (of course) and gh (GitHub CLI) on your PATH.
To install jj:
cargo install jj-cli --bin jj --lockedjj git fetchNote: jj-pr does not fetch for you — fetch first to update the local repo to the latest remote state.
jj pr # or: jj-pr show○ PR #2804 Ready mingwei/fix-inline-tasks
│ fix(dfir_rs): defer `_counter` task spawning via `Context::request_task` buffer
│ ○ PR #2746 Ready mingwei/sqs-rebase
├─╯ feat(hydro_test): add example of AWS + SQS support
│ ◆ trunk()
├─╯
│ ○ PR #2812* Draft mingwei/delete-subgraph-id
│ ├─╮ refactor(dfir_rs): remove SubgraphId, simplify StateLifespan
│ │ ○ PR #2811* Draft mingwei/delete-schedule-subgraph-args
│ ○ │ PR #2810* Draft mingwei/delete-metrics-slotvec
│ ├─╯ refactor(dfir_rs): use slotmap SecondaryMap for metrics
│ ○ PR #2801* Draft mingwei/delete-state-api-usage
╭─┤ refactor(dfir_lang): operators capture local state
│ ○ PR #2798 Ready mingwei/delete-intermediate-subgraph
├─╯ refactor(dfir_lang): remove intermediate subgraphs
~ (elided revisions)PR numbers are clickable hyperlinks in supported terminals. A * after the PR number means there are pending actions
for that PR.
If there are ambiguous commits, they show up as warnings with hints.
○ PR #2798* Ready mingwei/delete-intermediate-subgraph
│ refactor(dfir_lang): double-buffer defer_tick handoffs, remove intermediate subgraphs [ci-bench]
│ ○ PR #2801* Draft mingwei/delete-state-api-usage
├─╯ refactor(dfir_lang): operators capture local state instead of using the state API [ci-bench]
⚠ ambiguous branches: PR #2798, PR #2801, trailers: PR #2798
│ (restructure PRs to resolve — stack one on the other)
◆ trunk()Use jj pr log to see individual commits and their PR associations.
jj pr log # commits associated with PRs
jj pr log --all # include commits not in any PRThis is mainly useful for diagnosing ambiguous commits.
○ lqxnymrprnpl a6f5cee01ecb PR #2801* Draft
│ refactor(dfir_lang): add write_tick_end to OperatorWriteOutput, convert fold to inline state
│ ○ wnkkzpnlkwzt ca43c533b6f6 mingwei/delete-intermediate-subgraph sandbox-d11a1381-9aa3-49f8-abf1-16585609c8a6 PR #2798* Ready
├─╯ Address PR #2798 review comments: extract helper methods, fix stale comments
⚠ prupozyltvpz cf49be1b8fc8 ambiguous [PR #2798, PR #2801]
│ refactor(dfir_lang): double-buffer defer_tick handoffs, remove intermediate subgraphs
⚠ xqxnxvxoyltz 7a71c32b196c ambiguous [PR #2798, PR #2801]
│ test: add test_mutual_defer_tick reproducing as_code topo sort cycle
◆ trunk()Fix ambiguities by restructuring with jj (e.g., jj rebase to stack one PR on the other, or jj describe to edit
"PR: #1234" trailers).
jj pr syncThis stamps missing trailers, rebases children of merged PRs onto trunk(), abandons merged commits, pushes affected
bookmarks, and updates GitHub base branches. Bookmarks with content-conflicted commits are skipped automatically.
You'll be prompted before any changes are applied.
stamp #15 trailer on knynvqkoypsy
stamp #14 trailer on olqtvyvtvzyq
push: #15 (my-feature-a)
Apply 3 action(s)? [Y/n]
Stamping PR #15 on knynvqkoypsy
Stamping PR #14 on olqtvyvtvzyq
Pushing 1 bookmark(s): my-feature-ajj bookmark create my-feature-b # first create the bookmark in jj
jj pr create my-feature-b # then create a draft PR for itThe base branch is auto-detected by walking the parent graph.
Pushing my-feature-b
Creating PR: feat: my new features (hydro-project:main ← my-feature-b) [draft]
Created PR #16PR membership is determined from the jj DAG structure: each commit belongs to the PR whose bookmark is its nearest
descendant. Commits shared between multiple PRs become ambiguous nodes. Trailers (PR: #1234) are appended to change
descriptions as a safety net. See the decide() function in pr_dag.rs for the full membership algorithm.
See DESIGN.md for the full design rationale.
If you push to a personal fork instead of the upstream repo:
- Add your fork as a named remote:
jj git remote add fork https://github.com/YOUR_USERNAME/REPO.git- Configure jj to push to your fork by default:
jj config set --repo git.push "fork"This tells jj pr create where to push bookmarks that aren't yet tracked on any remote.
- Create bookmarks and PRs as normal:
jj bookmark create my-branch
jj pr create my-branchjj pr create will automatically:
- Detect the fork by comparing the remote's GitHub owner against the upstream owner
- Push to the correct remote
- Pass
YOUR_USERNAME:branchas the head ref so GitHub creates a cross-repo PR
The display shows both owners:
create PR: "my feature" (upstream-org:main ← your-username:my-branch) [draft]
- No stacked PRs in fork workflows — GitHub requires the base branch to exist on the upstream repo. Since intermediate branches only exist on your fork, all fork PRs target the default branch (e.g.
main). Local DAG structure is preserved in jj but not reflected in GitHub's PR chain. - Multi-parent PRs target the default branch — GitHub PRs can only have a single base branch. If a PR has multiple parents in the DAG, it targets the default branch instead of stacking.
- One tracked remote per PR bookmark — if a bookmark tracks multiple non-git remotes,
jj pr createwill error. Untrack one withjj bookmark untrack. - Multiple PRs sharing the same bookmark — if you have e.g. an open and a closed/merged PR both using the same branch name, only one will be tracked.
- Some other things, probably