Skip to main content

jj_hooks/
cli.rs

1//! clap argument structs for the `jj-hooks` binary.
2
3use clap::{Parser, Subcommand};
4use clap_complete::Shell;
5
6use crate::runner::{Runner, Stage};
7
8#[derive(Parser, Debug)]
9#[command(
10    name = "jj-hooks",
11    about = "Run pre-commit / lefthook / hk hooks against jj bookmark pushes",
12    version,
13    propagate_version = true
14)]
15pub struct Cli {
16    /// Hook runner to use. Overrides autodetect.
17    #[arg(long, value_enum, global = true, env = "JJ_HOOKS_RUNNER")]
18    pub runner: Option<RunnerArg>,
19
20    /// Log level filter (e.g. `info`, `debug`, `warn`).
21    #[arg(long, global = true, env = "JJ_HOOKS_LOG", default_value = "warn")]
22    pub log_level: String,
23
24    #[command(subcommand)]
25    pub command: Command,
26}
27
28#[derive(Subcommand, Debug)]
29pub enum Command {
30    /// Run hooks then push. Mirrors the flags `jj git push` accepts; any
31    /// flags we don't model can be passed through after `--`.
32    Push {
33        /// Advance the local bookmark to the fixup commit when hooks modify
34        /// files. Reads `jj-hooks.advance-bookmarks` config when not given.
35        #[arg(long)]
36        advance_bookmarks: bool,
37
38        /// Hook stage to run. Defaults to `pre-push`.
39        #[arg(long, value_enum, default_value = "pre-push")]
40        stage: StageArg,
41
42        #[command(flatten)]
43        push: PushArgs,
44
45        /// Only display what will change on the remote.
46        #[arg(long)]
47        dry_run: bool,
48
49        /// Disable the post-fixup retry. By default, when hooks fail
50        /// AND produce a fixup commit, jj-hooks re-runs the hook
51        /// backend against the fixup; if the re-run is clean, the
52        /// push proceeds and a warning is printed. Pass this flag to
53        /// restore pre-0.3.0 behavior (any failure aborts immediately).
54        #[arg(long)]
55        no_retry_after_fixup: bool,
56    },
57
58    /// Run hooks against a revset without pushing.
59    Run {
60        /// Hook stage to run. Defaults to `pre-commit`.
61        #[arg(long, value_enum, default_value = "pre-commit")]
62        stage: StageArg,
63
64        /// Revset to check. Defaults to `@`.
65        #[arg(default_value = "@")]
66        revset: String,
67
68        /// Disable the post-fixup retry. See `push --no-retry-after-fixup`.
69        #[arg(long)]
70        no_retry_after_fixup: bool,
71
72        /// Run hooks against every tracked file, ignoring the revset's
73        /// diff range. Each runner's own equivalent flag is used:
74        /// `--all-files` for pre-commit/prek/lefthook, `--glob '*'` for
75        /// hk (its own `-a/--all` doesn't actually override stage-hook
76        /// ref bounds in v1.45.0). Useful when you want to lint
77        /// everything once (e.g. after a refactor that touched a lot)
78        /// without crafting a revset that happens to cover every gated
79        /// step. The setup pipeline still runs; the runner just sees no
80        /// `--from-ref`/`--to-ref`.
81        #[arg(long)]
82        all_files: bool,
83    },
84
85    /// Push jj tags to a git remote. jj has no native `jj git push --tag`,
86    /// so this exports refs to the colocated git repo and shells out to
87    /// `git push refs/tags/<tag>` for each requested tag.
88    PushTags {
89        /// Tag name(s) to push. Mutually exclusive with `--all`.
90        tags: Vec<String>,
91
92        /// Push every local tag.
93        #[arg(long, conflicts_with = "tags")]
94        all: bool,
95
96        /// Force-push the tag refs (overwrites the remote). Use sparingly.
97        #[arg(short = 'f', long)]
98        force: bool,
99
100        /// Print the commands without running them.
101        #[arg(short = 'n', long)]
102        dry_run: bool,
103
104        /// Git remote to push to.
105        #[arg(long, default_value = "origin")]
106        remote: String,
107    },
108
109    /// Interactive setup: install `jj push` alias and configure defaults.
110    Init,
111
112    /// Print a shell completion script. Pipe into your shell rc, e.g.
113    /// `eval "$(jj-hp completions zsh)"`.
114    Completions {
115        /// Shell to generate completions for.
116        #[arg(value_enum)]
117        shell: Shell,
118    },
119}
120
121/// All flags that `jj git push` accepts and that we model explicitly.
122/// Anything unrecognized passes through via [`PushArgs::passthrough`] (the
123/// trailing `--` escape hatch).
124#[derive(clap::Args, Debug, Clone, Default)]
125pub struct PushArgs {
126    /// Push only this bookmark (can be repeated).
127    #[arg(
128        short = 'b',
129        long,
130        action = clap::ArgAction::Append,
131        add = clap_complete::ArgValueCompleter::new(crate::completions::bookmark_value_completer),
132    )]
133    pub bookmark: Vec<String>,
134
135    /// Push bookmarks pointing to these commits (can be repeated).
136    #[arg(short = 'r', long, action = clap::ArgAction::Append)]
137    pub revision: Vec<String>,
138
139    /// Push these commits by creating a bookmark (can be repeated).
140    #[arg(short = 'c', long, action = clap::ArgAction::Append)]
141    pub change: Vec<String>,
142
143    /// The remote to push to.
144    #[arg(
145        long,
146        add = clap_complete::ArgValueCompleter::new(crate::completions::remote_value_completer),
147    )]
148    pub remote: Option<String>,
149
150    /// Push all bookmarks (including new bookmarks).
151    #[arg(long)]
152    pub all: bool,
153
154    /// Push all tracked bookmarks.
155    #[arg(long)]
156    pub tracked: bool,
157
158    /// Push all deleted bookmarks.
159    #[arg(long)]
160    pub deleted: bool,
161
162    /// Allow pushing new bookmarks (i.e. bookmarks not yet on the remote).
163    #[arg(long)]
164    pub allow_new: bool,
165
166    /// Pass-through args after `--` forwarded to `jj git push` verbatim.
167    #[arg(last = true)]
168    pub passthrough: Vec<String>,
169}
170
171/// Reconstruct the argv list for `jj git push` from our parsed flags.
172/// Known flags emit their canonical form; unknown flags arrive via
173/// `passthrough` and are appended at the end.
174pub fn push_argv(args: &PushArgs, dry_run: bool) -> Vec<String> {
175    let mut out = Vec::new();
176
177    for b in &args.bookmark {
178        out.push("-b".into());
179        out.push(b.clone());
180    }
181    for r in &args.revision {
182        out.push("-r".into());
183        out.push(r.clone());
184    }
185    for c in &args.change {
186        out.push("-c".into());
187        out.push(c.clone());
188    }
189    if let Some(remote) = &args.remote {
190        out.push("--remote".into());
191        out.push(remote.clone());
192    }
193    if args.all {
194        out.push("--all".into());
195    }
196    if args.tracked {
197        out.push("--tracked".into());
198    }
199    if args.deleted {
200        out.push("--deleted".into());
201    }
202    if args.allow_new {
203        out.push("--allow-new".into());
204    }
205    if dry_run {
206        out.push("--dry-run".into());
207    }
208    out.extend(args.passthrough.iter().cloned());
209
210    out
211}
212
213#[derive(clap::ValueEnum, Debug, Clone, Copy)]
214pub enum RunnerArg {
215    PreCommit,
216    Prek,
217    Lefthook,
218    Hk,
219}
220
221impl From<RunnerArg> for Runner {
222    fn from(value: RunnerArg) -> Self {
223        match value {
224            RunnerArg::PreCommit => Runner::PreCommit,
225            RunnerArg::Prek => Runner::Prek,
226            RunnerArg::Lefthook => Runner::Lefthook,
227            RunnerArg::Hk => Runner::Hk,
228        }
229    }
230}
231
232#[derive(clap::ValueEnum, Debug, Clone, Copy)]
233pub enum StageArg {
234    PreCommit,
235    PrePush,
236}
237
238impl From<StageArg> for Stage {
239    fn from(value: StageArg) -> Self {
240        match value {
241            StageArg::PreCommit => Stage::PreCommit,
242            StageArg::PrePush => Stage::PrePush,
243        }
244    }
245}