use clap::{Parser, Subcommand};
use crate::forge::types::MergeMethod;
#[derive(Parser)]
#[command(name = "jjpr")]
#[command(about = "Manage stacked pull requests in Jujutsu repositories")]
#[command(version, disable_version_flag = true)]
#[command(long_about = "\
Manage stacked pull requests in Jujutsu repositories.
Each jj bookmark becomes one pull request. A \"stack\" is a chain of bookmarks \
that jjpr discovers by walking parent commits from your bookmarks toward trunk. \
Commits without bookmarks are folded into the nearest bookmarked ancestor's PR.
Run with no arguments to see your stacks and their PR/MR status (read-only):
$ jjpr
auth (1 change, #42 open, needs push)
profile (2 changes, #41 draft, synced)
Use `jjpr submit` to push bookmarks, create/update PRs, and add stack \
navigation comments. Use `jjpr merge` to land them from the bottom up.")]
pub struct Cli {
#[arg(short = 'v', short_alias = 'V', long = "version", action = clap::ArgAction::Version)]
pub version: (),
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(long, global = true)]
pub dry_run: bool,
#[arg(long, global = true)]
pub no_fetch: bool,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(long_about = "\
Push bookmarks and create/update pull requests for a stack.
Each bookmark in the stack gets its own PR. Commits between two bookmarks \
are grouped into the upper bookmark's PR. If you have 6 commits but only \
one bookmark, you get one PR containing all 6 commits.
When no bookmark is specified, jjpr infers the target from your working \
copy — it finds which stack overlaps with `trunk()..@` and submits up to \
the topmost bookmark. Your working copy must be at or below a bookmarked \
commit (an empty commit above the stack won't match any bookmark).
Each PR receives a stack navigation comment showing its position:
This PR is part of a stack:
1. `profile` <-- this PR
2. `auth`
Submit is idempotent — run it after rebasing, editing commits, or \
restacking to push updates, fix PR base branches, and sync descriptions.
Foreign base detection: if your stack builds on a coworker's remote \
branch, jjpr targets your bottom PR at their branch instead of main. \
Use --base to override this when the coworker hasn't pushed yet.
Examples:
jjpr submit # submit the stack under your working copy
jjpr submit auth # submit the stack ending at bookmark 'auth'
jjpr submit --draft # create new PRs as drafts
jjpr submit --dry-run # preview what would happen")]
Submit {
bookmark: Option<String>,
#[arg(long, value_delimiter = ',')]
reviewer: Vec<String>,
#[arg(long)]
remote: Option<String>,
#[arg(long)]
draft: bool,
#[arg(long, conflicts_with = "draft")]
ready: bool,
#[arg(long)]
base: Option<String>,
},
#[command(long_about = "\
Show your stacks with detailed CI, review, and mergeability status.
This is the same output as running `jjpr` with no arguments. The `status` \
subcommand exists for discoverability — both forms are identical.
Example output:
$ jjpr status
auth (1 change, #42 open, synced)
✓ mergeable ✓ CI passing ✓ 1 approval
profile (2 changes, #43 open, needs push)
✗ CI pending ✗ 0/1 approvals")]
Status {},
#[command(long_about = "\
Merge a stack of PRs from the bottom up.
Merges the bottommost mergeable PR, fetches the updated default branch, \
rebases the remaining stack onto it, pushes, retargets the next PR's base, \
and repeats until blocked or done.
Before merging each PR, jjpr checks:
- PR is not a draft
- CI checks pass (skip with --no-ci-check)
- Required approvals met (override with --required-approvals)
- No changes requested
- No merge conflicts
Idempotent — re-run after CI passes or reviews are approved to continue.
Examples:
jjpr merge # merge from the bottom up
jjpr merge --merge-method rebase # use rebase instead of squash
jjpr merge --no-ci-check # merge even if CI is pending
jjpr merge --dry-run # preview what would happen")]
Merge {
bookmark: Option<String>,
#[arg(long, value_enum)]
merge_method: Option<MergeMethod>,
#[arg(long)]
required_approvals: Option<u32>,
#[arg(long)]
no_ci_check: bool,
#[arg(long)]
remote: Option<String>,
#[arg(long)]
base: Option<String>,
#[arg(long)]
watch: bool,
},
#[command(long_about = "\
Manage forge authentication.
jjpr authenticates via token environment variables or CLI credential stores:
GitHub: GITHUB_TOKEN or GH_TOKEN (fallback: `gh auth login`)
GitLab: GITLAB_TOKEN (fallback: `glab auth login`)
Forgejo/Codeberg: FORGEJO_TOKEN
Use `jjpr auth test` to verify credentials, `jjpr auth setup` for full \
setup instructions.")]
Auth {
#[command(subcommand)]
command: AuthCommands,
},
#[command(long_about = "\
Manage jjpr configuration.
jjpr uses an optional TOML config file for merge settings. Global config \
lives at ~/.config/jjpr/config.toml (or $XDG_CONFIG_HOME/jjpr/config.toml).
A repo-local config at .jj/jjpr.toml overrides global settings — useful \
for setting forge type and token env var for self-hosted instances.
Use `jjpr config init` to create the global config with defaults, or \
`jjpr config init --repo` for repo-local config. CLI flags always override \
config file values.")]
Config {
#[command(subcommand)]
command: ConfigCommands,
},
}
#[derive(Subcommand)]
pub enum AuthCommands {
Test,
Setup,
}
#[derive(Subcommand)]
pub enum ConfigCommands {
Init {
#[arg(long)]
repo: bool,
},
}