jj tools for working with GitHub from your terminal.
- Create PRs locally from your preferred editor, for any arbitrary revision ID
- Intelligently supports stacked PRs by choosing the correct base if the revision has an ancestor bookmark for which an open PR exists
- Enable auto-merge for a PR by its revision ID, without having to know/find its PR number (e.g.
jj pr auto-merge zqxy) - Create local bookmarks for PRs, including across forks (e.g.
jj pr fetch 1234 && jj new pr-1234/..., useful for testing PRs to OSS repos) - Show PR metadata like number and CI status in commit graph (e.g.
jj pr log)
all from the comfort of your terminal, without touching GitHub's clunky web UI. Works great when combined with the jj megamerge workflow!
See DOCS.md for all commands, flags, and features. PRs welcome and encouraged!
| Writing up a PR in Neovim | PR number and GitHub Actions status in commit log graph |
|---|---|
![]() |
![]() |
jj must be on PATH. pr fetch additionally requires a colocated git repo
and git on PATH (jj cannot yet fetch arbitrary refs like
refs/pull/123/head, so the fetch step shells out to git).
With Nix
Add the flake input:
{
inputs.jj-gh.url = "github:mrjones2014/jj-gh";
outputs =
{
self,
nixpkgs,
jj-gh,
...
}:
{
# use jj-gh.packages.${system}.default
};
}You can either use the overlay directly, or use the home-manager module.
{ jj-gh, pkgs, ... }:
{
# overlay, not needed if using home-manager module
nixpkgs.overlays = [ jj-gh.overlays.default ];
home.packages = [ pkgs.jj-gh ];
# home-manager
imports = [ jj-gh.homeManagerModules.default ];
programs.jujutsu.gh = {
enable = true;
# Map of `jj` alias name -> `jj-gh` subcommand. Each entry installs the
# alias *and* drops a completion overlay for `jj <name> <tab>` into any
# shell home-manager has enabled (fish/bash/zsh).
# aliases = { pr = "pr"; };
settings = {
gh_askpass = [
"op"
"read"
"op://Private/GitHub/token"
];
};
};
}Not required, but you may also opt-in to using our Cachix binary cache:
Public Key: jj-gh.cachix.org-1:N1uFBMDd9znlhDa68BRqLSXYzXXJ2+WHVuwxpGxCtDo=
From crates.io
Requires a Rust toolchain.
cargo install jj-ghFrom source
Requires a Rust toolchain. Clone this repository, then from the repo root:
cargo install --path .Set up pr as a built-in jj subcommand so you can write jj pr create <rev>. If you use the home-manager module this is already done for you.
# ~/.config/jj/config.toml
[aliases]
pr = ["util", "exec", "--", "jj-gh", "pr"]Now jj pr create <rev> (and the alias jj pr c <rev>) and jj pr fetch <pr-num> (alias jj pr f <pr-num>) work like any other jj
subcommand.
jj-gh completions <shell> prints a standard completion script for the jj-gh binary. For the more common case where you invoke jj-gh through a jj alias (e.g. jj pr <tab>), pass --jj-alias <NAME> --subcommand <NAME> (both required together) to emit an overlay that adds completions for the alias on top of jj's own completion script.
# fish
jj util completion fish | source
jj-gh completions fish --jj-alias pr --subcommand pr | source
# bash
eval "$(jj util completion bash)"
eval "$(jj-gh completions bash --jj-alias pr --subcommand pr)"
# zsh (after compinit)
source <(jj util completion zsh)
source <(jj-gh completions zsh --jj-alias pr --subcommand pr)The overlay must be sourced after jj util completion <shell> so it can chain to jj's completer when the alias is not the one being completed.
If you use the home-manager module with programs.fish.enable / programs.bash.enable / programs.zsh.enable set, the matching overlay is wired up automatically for every entry in programs.jujutsu.gh.aliases. You only need the manual steps above when installing via cargo install or from source.
Add a [jj-gh] table to any jj config layer (global ~/.config/jj/config.toml or repo-local config via jj config edit --repo).
Options related to PR metadata may also be overridden via the markdown frontmatter when your editor opens.
[jj-gh]
# Auth (one source required; see "Token source precedence" below for env vars and CLI flag)
gh_askpass = ["op", "read", "op://Personal/github/token"] # preferred
gh_token = "ghp_..." # plain token, less safe
askpass_timeout_secs = 20 # default 20
# Behavior
default_base_branch = "main" # default "master"
draft = false # default false
auto_merge = false # default false; enable auto-merge on PR after creation
auto_merge_method = "merge" # default "merge"; one of "merge", "squash", "rebase"
default_remote = "origin" # default remote to use
upstream_remote = "upstream" # default remote to use for cross-fork PR fetching
# PR body template. `pr_create_template` is a jj template string, evaluated
# against the revset being PR'd in chronological order. `pr_create_template_file`
# is a markdown file path. See "PR body template resolution" below for the full
# precedence list and "Template aliases" for what's available inside
# `pr_create_template`.
# Example: emit each commit's full description, separated by blank lines.
pr_create_template = 'description ++ "\n"'
# if not set, by default this will look for the following candidates:
# .github/PULL_REQUEST_TEMPLATE.md
# .github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md
# .github/pull_request_template.md
# .github/PULL_REQUEST_TEMPLATE/pull_request_template.md
pr_create_template_file = ".github/PULL_REQUEST_TEMPLATE.md"
# Bookmark name template for `pr fetch`. A jj template string, evaluated once
# against `root()` with `pr_*` aliases pre-populated from the PR's metadata.
# Default: '"pr-" ++ pr_number ++ "/" ++ pr_branch'. See "Template aliases" for
# the full list.
pr_fetch_bookmark_template = '"pr-" ++ pr_number ++ "/" ++ pr_branch'
# Editor command, shell-words split. Falls back to $VISUAL, then $EDITOR.
editor = [
"nvim",
"+10", # +10 jumps your cursor past the frontmatter
]
# enable or disable the use of nerdfont icons
# (e.g. in the `pr log` default template)
# NOTE: if you have issues with nerdfont icons, its most likely your `$PAGER`,
# you can fix it by either using something like `bat` (https://github.com/sharkdp/bat)
# as your pager, or setting
# ~/.config/jj/config.toml
# [ui]
# pager = { command = ["less", "-FRX"], env = { LESSCHARSET = "utf-8", LESSUTFCHARDEF = "E000-F8FF:p,F0000-FFFFD:p,100000-10FFFD:p" } }
nerdfonts = trueConfig precedence (high to low):
- CLI flags
- env (
GH_ASKPASS,JJ_GH_TEMPLATE,JJ_GH_TEMPLATE_FILE) $JJ_GH_EXTRA_CONFIGfilejjrepo-local config filejjglobal config file- built-in defaults
JJ_GH_TEMPLATE maps to pr_create_template (jj template string).
JJ_GH_TEMPLATE_FILE maps to pr_create_template_file (path to a markdown
template).
Token source precedence (high to low):
--gh-askpassCLI flaggh_askpassfrom merged config$JJ_GH_TOKENenvironment variable$GH_TOKENenvironment variable (matches theghCLI convention)gh_tokenfrom merged config (plain text, less safe)- Attempting to run
gh auth token
Env vars override gh_token from config, but a configured gh_askpass still
wins. Use $JJ_GH_TOKEN when you need a different token for jj-gh than for
the gh CLI itself. You may also run gh auth login before running jj-gh
to use the GitHub CLI's authentication.
jj-gh renders three different template surfaces through jj's template engine,
each with its own set of injected aliases. Aliases are pre-quoted strings, so
use them directly without wrapping in "...".
Evaluated against the revset being PR'd, in chronological order (--reversed),
so a multi-commit stack renders bottom-up. All standard jj template builtins
work (description, commit_id, author, etc.). Injected aliases:
pr_title: default title (first-line description of the oldest commit on the stack).pr_base: resolved base branch.pr_head_branch: existing local bookmark on the rev, or empty if the rev is unpushed.pr_oldest_rev_id: 40-char hex commit SHA of the oldest commit in the revset. Because the template runs once per commit, static content like a fixed PR header would otherwise be duplicated N times for an N-commit stack. Comparingcommit_id.short(40) == pr_oldest_rev_idlets the template emit such content exactly once, at the bottom-most commit (which lands at the top of the output thanks to--reversed). Example:
if(commit_id.short(40) == pr_oldest_rev_id,
"Fixes \n\n",
""
) ++ "- `" ++ description.first_line() ++ "`\n"
The rendered output seeds the buffer your editor opens; you can still edit the body and frontmatter before the PR is submitted.
Evaluated once against root() (no commit context). Injected aliases:
pr_number: PR number as a decimal string.pr_title: PR title.pr_branch: head ref name (the source branch on the PR's fork).pr_url: PR'shtml_url.pr_head_sha: 40-char hex commit SHA of the PR's head.pr_head_user: PR's head fork owner login, or empty if the fork was deleted.pr_head_repo: PR's head fork repository name, or empty if the fork was deleted.pr_slug: sanitized lowercase ASCII slug of the title (max 50 chars), suitable for embedding in a bookmark name.
Per-commit aliases, each keyed on commit_id and empty for commits without
a matching open PR:
pr_number: PR number as a string.pr_url: PR URL.pr_ci_status:SUCCESS,FAILED, orPENDING.pr_merge_status: merged / in-merge-queue / auto-merge label.pr_meta: pre-formatted hyperlinked PR number, colored CI icon, and merge status.
pr create picks the body template from the following sources, highest first:
--no-templateflag (skip templating entirely).-T/--templateCLI (jj template string).--template-fileCLI (path).- Repo-layer
pr_create_template(jj template string from repo, workspace, or$JJ_GH_EXTRA_CONFIGconfig). - Repo-layer
pr_create_template_file(path). - Auto-detected
.github/PULL_REQUEST_TEMPLATE.md(case variants included). - User-layer
pr_create_template(from your global jj config). - User-layer
pr_create_template_file(path from your global jj config).
The split between repo and user layers lets you set a global default jj
template while still picking up per-repo .github/PULL_REQUEST_TEMPLATE.md
files when contributing to OSS.
title: "" # required, non-empty
base: "main" # required; pre-filled with the resolved base branch
labels: [] # list of strings, applied via a follow-up API call after creation
draft: false # bool
auto_merge: false # bool; enable GitHub auto-merge once required checks pass
# this value is not present by default but may be set here as well
auto_merge_method: "merge" # one of "merge", "squash", "rebase"The token supplied via gh_askpass, gh_token, $JJ_GH_TOKEN, or $GH_TOKEN needs a few permissions to function.
Fine-grained personal access token (preferred), with access to the target repositories:
| Permission | Level | Used by |
|---|---|---|
| Metadata | Read | every API call (always required) |
| Commit Statuses | Read | Used to show GitHub Actions status for PRs in jj pr log |
| Contents | Read | pr create (resolving the base branch ref), pr fetch (fetching refs/pull/<n>/head via git) |
| Pull requests | Read and write | pr create (list + create, enable auto-merge), pr fetch (get) |
| Issues | Read and write | pr create when applying labels (GitHub labels go through the Issues API) |
Classic personal access token:
- Private repos:
repo(full control). - Public repos only:
public_repois sufficient forpr createandpr fetch.
If you don't apply labels, you can drop the Issues permission. PRs are treated as Issues for the purposes of applying labels in GitHub's API.
All log output goes to STDERR; the final PR URL (or any value the command prints) goes to STDOUT. Pipe-friendly:
URL=$(jj pr create zxi)
echo "Opened $URL"- TTY on
STDOUT: default log level isINFO. - Piped
STDOUT: default log level drops toERROR, so only failures appear onSTDERR. - Override with
-v/-vv,-q,--log-level <level>, or$JJ_GH_LOG.

