1use clap::{
2 builder::{
3 styling::{
4 Ansi256Color,
5 AnsiColor,
6 },
7 Styles,
8 TypedValueParser,
9 ValueParserFactory,
10 },
11 error::{
12 ContextKind,
13 ContextValue,
14 ErrorKind,
15 },
16 ArgAction,
17 Parser,
18 ValueEnum,
19};
20use git_cliff_core::{
21 config::BumpType,
22 config::Remote,
23 DEFAULT_CONFIG,
24 DEFAULT_OUTPUT,
25};
26use glob::Pattern;
27use regex::Regex;
28use secrecy::SecretString;
29use std::path::PathBuf;
30use url::Url;
31
32#[derive(Debug, Clone, Copy, ValueEnum)]
33pub enum Strip {
34 Header,
35 Footer,
36 All,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
40pub enum Sort {
41 Oldest,
42 Newest,
43}
44
45const STYLES: Styles = Styles::styled()
46 .header(Ansi256Color(208).on_default().bold())
47 .usage(Ansi256Color(208).on_default().bold())
48 .literal(AnsiColor::White.on_default())
49 .placeholder(AnsiColor::Green.on_default());
50
51#[derive(Debug, Parser)]
53#[command(
54 version,
55 author = clap::crate_authors!("\n"),
56 about,
57 rename_all_env = "screaming-snake",
58 help_template = "\
59{before-help}{name} {version}
60{author-with-newline}{about-with-newline}
61{usage-heading}
62 {usage}
63
64{all-args}{after-help}
65",
66 override_usage = "git-cliff [FLAGS] [OPTIONS] [--] [RANGE]",
67 next_help_heading = Some("OPTIONS"),
68 disable_help_flag = true,
69 disable_version_flag = true,
70 styles(STYLES),
71)]
72pub struct Opt {
73 #[arg(
74 short,
75 long,
76 action = ArgAction::Help,
77 global = true,
78 help = "Prints help information",
79 help_heading = "FLAGS"
80 )]
81 pub help: Option<bool>,
82 #[arg(
83 short = 'V',
84 long,
85 action = ArgAction::Version,
86 global = true,
87 help = "Prints version information",
88 help_heading = "FLAGS"
89 )]
90 pub version: Option<bool>,
91 #[arg(short, long, action = ArgAction::Count, alias = "debug", help_heading = Some("FLAGS"))]
93 pub verbose: u8,
94 #[arg(
96 short,
97 long,
98 value_name = "CONFIG",
99 num_args = 0..=1,
100 required = false
101 )]
102 pub init: Option<Option<String>>,
103 #[arg(
105 short,
106 long,
107 env = "GIT_CLIFF_CONFIG",
108 value_name = "PATH",
109 default_value = DEFAULT_CONFIG,
110 value_parser = Opt::parse_dir
111 )]
112 pub config: PathBuf,
113 #[arg(
115 short,
116 long,
117 env = "GIT_CLIFF_WORKDIR",
118 value_name = "PATH",
119 value_parser = Opt::parse_dir
120 )]
121 pub workdir: Option<PathBuf>,
122 #[arg(
124 short,
125 long,
126 env = "GIT_CLIFF_REPOSITORY",
127 value_name = "PATH",
128 num_args(1..),
129 value_parser = Opt::parse_dir
130 )]
131 pub repository: Option<Vec<PathBuf>>,
132 #[arg(
134 long,
135 env = "GIT_CLIFF_INCLUDE_PATH",
136 value_name = "PATTERN",
137 num_args(1..)
138 )]
139 pub include_path: Option<Vec<Pattern>>,
140 #[arg(
142 long,
143 env = "GIT_CLIFF_EXCLUDE_PATH",
144 value_name = "PATTERN",
145 num_args(1..)
146 )]
147 pub exclude_path: Option<Vec<Pattern>>,
148 #[arg(long, env = "GIT_CLIFF_TAG_PATTERN", value_name = "PATTERN")]
150 pub tag_pattern: Option<Regex>,
151 #[arg(
153 long,
154 env = "GIT_CLIFF_WITH_COMMIT",
155 value_name = "MSG",
156 num_args(1..)
157 )]
158 pub with_commit: Option<Vec<String>>,
159 #[arg(
161 long,
162 env = "GIT_CLIFF_WITH_TAG_MESSAGE",
163 value_name = "MSG",
164 num_args = 0..=1,
165 )]
166 pub with_tag_message: Option<String>,
167 #[arg(long, env = "GIT_CLIFF_IGNORE_TAGS", value_name = "PATTERN")]
169 pub ignore_tags: Option<Regex>,
170 #[arg(long, env = "GIT_CLIFF_COUNT_TAGS", value_name = "PATTERN")]
172 pub count_tags: Option<Regex>,
173 #[arg(
175 long,
176 env = "GIT_CLIFF_SKIP_COMMIT",
177 value_name = "SHA1",
178 num_args(1..)
179 )]
180 pub skip_commit: Option<Vec<String>>,
181 #[arg(
183 short,
184 long,
185 env = "GIT_CLIFF_PREPEND",
186 value_name = "PATH",
187 value_parser = Opt::parse_dir
188 )]
189 pub prepend: Option<PathBuf>,
190 #[arg(
192 short,
193 long,
194 env = "GIT_CLIFF_OUTPUT",
195 value_name = "PATH",
196 value_parser = Opt::parse_dir,
197 num_args = 0..=1,
198 default_missing_value = DEFAULT_OUTPUT
199 )]
200 pub output: Option<PathBuf>,
201 #[arg(
203 short,
204 long,
205 env = "GIT_CLIFF_TAG",
206 value_name = "TAG",
207 allow_hyphen_values = true
208 )]
209 pub tag: Option<String>,
210 #[arg(
213 long,
214 value_name = "BUMP",
215 value_enum,
216 num_args = 0..=1,
217 default_missing_value = "auto",
218 value_parser = clap::value_parser!(BumpOption))]
219 pub bump: Option<BumpOption>,
220 #[arg(long, help_heading = Some("FLAGS"))]
222 pub bumped_version: bool,
223 #[arg(
225 short,
226 long,
227 env = "GIT_CLIFF_TEMPLATE",
228 value_name = "TEMPLATE",
229 allow_hyphen_values = true
230 )]
231 pub body: Option<String>,
232 #[arg(short, long, help_heading = Some("FLAGS"))]
234 pub latest: bool,
235 #[arg(long, help_heading = Some("FLAGS"))]
237 pub current: bool,
238 #[arg(short, long, help_heading = Some("FLAGS"))]
240 pub unreleased: bool,
241 #[arg(long, help_heading = Some("FLAGS"))]
243 pub topo_order: bool,
244 #[arg(long, help_heading = Some("FLAGS"))]
246 pub use_branch_tags: bool,
247 #[arg(long, help_heading = Some("FLAGS"))]
249 pub no_exec: bool,
250 #[arg(short = 'x', long, help_heading = Some("FLAGS"))]
252 pub context: bool,
253 #[arg(
255 long,
256 value_name = "PATH",
257 value_parser = Opt::parse_dir,
258 env = "GIT_CLIFF_CONTEXT",
259 )]
260 pub from_context: Option<PathBuf>,
261 #[arg(short, long, value_name = "PART", value_enum)]
263 pub strip: Option<Strip>,
264 #[arg(
266 long,
267 value_enum,
268 default_value_t = Sort::Oldest
269 )]
270 pub sort: Sort,
271 #[arg(value_name = "RANGE", help_heading = Some("ARGS"))]
273 pub range: Option<String>,
274 #[arg(
276 long,
277 env = "GITHUB_TOKEN",
278 value_name = "TOKEN",
279 hide_env_values = true,
280 hide = !cfg!(feature = "github"),
281 )]
282 pub github_token: Option<SecretString>,
283 #[arg(
285 long,
286 env = "GITHUB_REPO",
287 value_parser = clap::value_parser!(RemoteValue),
288 value_name = "OWNER/REPO",
289 hide = !cfg!(feature = "github"),
290 )]
291 pub github_repo: Option<RemoteValue>,
292 #[arg(
294 long,
295 env = "GITLAB_TOKEN",
296 value_name = "TOKEN",
297 hide_env_values = true,
298 hide = !cfg!(feature = "gitlab"),
299 )]
300 pub gitlab_token: Option<SecretString>,
301 #[arg(
303 long,
304 env = "GITLAB_REPO",
305 value_parser = clap::value_parser!(RemoteValue),
306 value_name = "OWNER/REPO",
307 hide = !cfg!(feature = "gitlab"),
308 )]
309 pub gitlab_repo: Option<RemoteValue>,
310 #[arg(
312 long,
313 env = "GITEA_TOKEN",
314 value_name = "TOKEN",
315 hide_env_values = true,
316 hide = !cfg!(feature = "gitea"),
317 )]
318 pub gitea_token: Option<SecretString>,
319 #[arg(
321 long,
322 env = "GITEA_REPO",
323 value_parser = clap::value_parser!(RemoteValue),
324 value_name = "OWNER/REPO",
325 hide = !cfg!(feature = "gitea"),
326 )]
327 pub gitea_repo: Option<RemoteValue>,
328 #[arg(
330 long,
331 env = "BITBUCKET_TOKEN",
332 value_name = "TOKEN",
333 hide_env_values = true,
334 hide = !cfg!(feature = "bitbucket"),
335 )]
336 pub bitbucket_token: Option<SecretString>,
337 #[arg(
339 long,
340 env = "BITBUCKET_REPO",
341 value_parser = clap::value_parser!(RemoteValue),
342 value_name = "OWNER/REPO",
343 hide = !cfg!(feature = "bitbucket"),
344 )]
345 pub bitbucket_repo: Option<RemoteValue>,
346 #[arg(long, help_heading = Some("FLAGS"), hide = !cfg!(feature = "remote"))]
348 pub use_native_tls: bool,
349}
350
351#[derive(Clone, Debug, PartialEq)]
353pub struct RemoteValue(pub Remote);
354
355impl ValueParserFactory for RemoteValue {
356 type Parser = RemoteValueParser;
357 fn value_parser() -> Self::Parser {
358 RemoteValueParser
359 }
360}
361
362#[derive(Clone, Debug)]
364pub struct RemoteValueParser;
365
366impl TypedValueParser for RemoteValueParser {
367 type Value = RemoteValue;
368 fn parse_ref(
369 &self,
370 cmd: &clap::Command,
371 arg: Option<&clap::Arg>,
372 value: &std::ffi::OsStr,
373 ) -> Result<Self::Value, clap::Error> {
374 let inner = clap::builder::StringValueParser::new();
375 let mut value = inner.parse_ref(cmd, arg, value)?;
376 if let Ok(url) = Url::parse(&value) {
377 value = url.path().trim_start_matches('/').to_string();
378 }
379 let parts = value.rsplit_once('/');
380 if let Some((owner, repo)) = parts {
381 Ok(RemoteValue(Remote::new(
382 owner.to_string(),
383 repo.to_string(),
384 )))
385 } else {
386 let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
387 if let Some(arg) = arg {
388 err.insert(
389 ContextKind::InvalidArg,
390 ContextValue::String(arg.to_string()),
391 );
392 }
393 err.insert(ContextKind::InvalidValue, ContextValue::String(value));
394 Err(err)
395 }
396 }
397}
398
399#[derive(Debug, Clone, Eq, PartialEq)]
400pub enum BumpOption {
401 Auto,
402 Specific(BumpType),
403}
404
405impl ValueParserFactory for BumpOption {
406 type Parser = BumpOptionParser;
407 fn value_parser() -> Self::Parser {
408 BumpOptionParser
409 }
410}
411
412#[derive(Clone, Debug)]
414pub struct BumpOptionParser;
415
416impl TypedValueParser for BumpOptionParser {
417 type Value = BumpOption;
418 fn parse_ref(
419 &self,
420 cmd: &clap::Command,
421 arg: Option<&clap::Arg>,
422 value: &std::ffi::OsStr,
423 ) -> Result<Self::Value, clap::Error> {
424 let inner = clap::builder::StringValueParser::new();
425 let value = inner.parse_ref(cmd, arg, value)?;
426 match value.as_str() {
427 "auto" => Ok(BumpOption::Auto),
428 "major" => Ok(BumpOption::Specific(BumpType::Major)),
429 "minor" => Ok(BumpOption::Specific(BumpType::Minor)),
430 "patch" => Ok(BumpOption::Specific(BumpType::Patch)),
431 _ => {
432 let mut err =
433 clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
434 if let Some(arg) = arg {
435 err.insert(
436 ContextKind::InvalidArg,
437 ContextValue::String(arg.to_string()),
438 );
439 }
440 err.insert(ContextKind::InvalidValue, ContextValue::String(value));
441 Err(err)
442 }
443 }
444 }
445}
446
447impl Opt {
448 fn parse_dir(dir: &str) -> Result<PathBuf, String> {
455 Ok(PathBuf::from(shellexpand::tilde(dir).to_string()))
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use clap::CommandFactory;
463 use std::ffi::OsStr;
464
465 #[test]
466 fn verify_cli() {
467 Opt::command().debug_assert();
468 }
469
470 #[test]
471 fn path_tilde_expansion() {
472 let home_dir = dirs::home_dir().expect("cannot retrieve home directory");
473 let dir = Opt::parse_dir("~/").expect("cannot expand tilde");
474 assert_eq!(home_dir, dir);
475 }
476
477 #[test]
478 fn remote_value_parser() -> Result<(), clap::Error> {
479 let remote_value_parser = RemoteValueParser;
480 assert_eq!(
481 RemoteValue(Remote::new("test", "repo")),
482 remote_value_parser.parse_ref(
483 &Opt::command(),
484 None,
485 OsStr::new("test/repo")
486 )?
487 );
488 assert_eq!(
489 RemoteValue(Remote::new("gitlab/group/test", "repo")),
490 remote_value_parser.parse_ref(
491 &Opt::command(),
492 None,
493 OsStr::new("gitlab/group/test/repo")
494 )?
495 );
496 assert_eq!(
497 RemoteValue(Remote::new("test", "testrepo")),
498 remote_value_parser.parse_ref(
499 &Opt::command(),
500 None,
501 OsStr::new("https://github.com/test/testrepo")
502 )?
503 );
504 assert_eq!(
505 RemoteValue(Remote::new("archlinux/packaging/packages", "arch-repro-status")),
506 remote_value_parser.parse_ref(
507 &Opt::command(),
508 None,
509 OsStr::new("https://gitlab.archlinux.org/archlinux/packaging/packages/arch-repro-status")
510 )?
511 );
512 assert!(remote_value_parser
513 .parse_ref(&Opt::command(), None, OsStr::new("test"))
514 .is_err());
515 assert!(remote_value_parser
516 .parse_ref(&Opt::command(), None, OsStr::new(""))
517 .is_err());
518 Ok(())
519 }
520
521 #[test]
522 fn bump_option_parser() -> Result<(), clap::Error> {
523 let bump_option_parser = BumpOptionParser;
524 assert_eq!(
525 BumpOption::Auto,
526 bump_option_parser.parse_ref(
527 &Opt::command(),
528 None,
529 OsStr::new("auto")
530 )?
531 );
532 assert!(bump_option_parser
533 .parse_ref(&Opt::command(), None, OsStr::new("test"))
534 .is_err());
535 assert_eq!(
536 BumpOption::Specific(BumpType::Major),
537 bump_option_parser.parse_ref(
538 &Opt::command(),
539 None,
540 OsStr::new("major")
541 )?
542 );
543 Ok(())
544 }
545}