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    /// Pass-through args after `--` forwarded to `jj git push` verbatim.
163    #[arg(last = true)]
164    pub passthrough: Vec<String>,
165}
166
167/// Reconstruct the argv list for `jj git push` from our parsed flags.
168/// Known flags emit their canonical form; unknown flags arrive via
169/// `passthrough` and are appended at the end.
170pub fn push_argv(args: &PushArgs, dry_run: bool) -> Vec<String> {
171    let mut out = Vec::new();
172
173    for b in &args.bookmark {
174        out.push("-b".into());
175        out.push(b.clone());
176    }
177    for r in &args.revision {
178        out.push("-r".into());
179        out.push(r.clone());
180    }
181    for c in &args.change {
182        out.push("-c".into());
183        out.push(c.clone());
184    }
185    if let Some(remote) = &args.remote {
186        out.push("--remote".into());
187        out.push(remote.clone());
188    }
189    if args.all {
190        out.push("--all".into());
191    }
192    if args.tracked {
193        out.push("--tracked".into());
194    }
195    if args.deleted {
196        out.push("--deleted".into());
197    }
198    if dry_run {
199        out.push("--dry-run".into());
200    }
201    out.extend(args.passthrough.iter().cloned());
202
203    out
204}
205
206#[derive(clap::ValueEnum, Debug, Clone, Copy)]
207pub enum RunnerArg {
208    PreCommit,
209    Prek,
210    Lefthook,
211    Hk,
212}
213
214impl From<RunnerArg> for Runner {
215    fn from(value: RunnerArg) -> Self {
216        match value {
217            RunnerArg::PreCommit => Runner::PreCommit,
218            RunnerArg::Prek => Runner::Prek,
219            RunnerArg::Lefthook => Runner::Lefthook,
220            RunnerArg::Hk => Runner::Hk,
221        }
222    }
223}
224
225#[derive(clap::ValueEnum, Debug, Clone, Copy)]
226pub enum StageArg {
227    PreCommit,
228    PrePush,
229}
230
231impl From<StageArg> for Stage {
232    fn from(value: StageArg) -> Self {
233        match value {
234            StageArg::PreCommit => Stage::PreCommit,
235            StageArg::PrePush => Stage::PrePush,
236        }
237    }
238}