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}