1use clap::{
2 ArgAction,
3 Parser,
4 ValueEnum,
5 builder::{
6 Styles,
7 TypedValueParser,
8 ValueParserFactory,
9 styling::{
10 Ansi256Color,
11 AnsiColor,
12 },
13 },
14 error::{
15 ContextKind,
16 ContextValue,
17 ErrorKind,
18 },
19};
20use git_cliff_core::{
21 DEFAULT_CONFIG,
22 DEFAULT_OUTPUT,
23 config::BumpType,
24 config::Remote,
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(long, env = "GIT_CLIFF_CONFIG_URL", value_name = "URL", hide = !cfg!(feature = "remote"))]
115 pub config_url: Option<Url>,
116 #[arg(
118 short,
119 long,
120 env = "GIT_CLIFF_WORKDIR",
121 value_name = "PATH",
122 value_parser = Opt::parse_dir
123 )]
124 pub workdir: Option<PathBuf>,
125 #[arg(
127 short,
128 long,
129 env = "GIT_CLIFF_REPOSITORY",
130 value_name = "PATH",
131 num_args(1..),
132 value_parser = Opt::parse_dir
133 )]
134 pub repository: Option<Vec<PathBuf>>,
135 #[arg(
137 long,
138 env = "GIT_CLIFF_INCLUDE_PATH",
139 value_name = "PATTERN",
140 num_args(1..)
141 )]
142 pub include_path: Option<Vec<Pattern>>,
143 #[arg(
145 long,
146 env = "GIT_CLIFF_EXCLUDE_PATH",
147 value_name = "PATTERN",
148 num_args(1..)
149 )]
150 pub exclude_path: Option<Vec<Pattern>>,
151 #[arg(long, env = "GIT_CLIFF_TAG_PATTERN", value_name = "PATTERN")]
153 pub tag_pattern: Option<Regex>,
154 #[arg(
156 long,
157 env = "GIT_CLIFF_WITH_COMMIT",
158 value_name = "MSG",
159 num_args(1..)
160 )]
161 pub with_commit: Option<Vec<String>>,
162 #[arg(
164 long,
165 env = "GIT_CLIFF_WITH_TAG_MESSAGE",
166 value_name = "MSG",
167 num_args = 0..=1,
168 )]
169 pub with_tag_message: Option<String>,
170 #[arg(long, env = "GIT_CLIFF_IGNORE_TAGS", value_name = "PATTERN")]
172 pub ignore_tags: Option<Regex>,
173 #[arg(long, env = "GIT_CLIFF_COUNT_TAGS", value_name = "PATTERN")]
175 pub count_tags: Option<Regex>,
176 #[arg(
178 long,
179 env = "GIT_CLIFF_SKIP_COMMIT",
180 value_name = "SHA1",
181 num_args(1..)
182 )]
183 pub skip_commit: Option<Vec<String>>,
184 #[arg(
186 short,
187 long,
188 env = "GIT_CLIFF_PREPEND",
189 value_name = "PATH",
190 value_parser = Opt::parse_dir
191 )]
192 pub prepend: Option<PathBuf>,
193 #[arg(
195 short,
196 long,
197 env = "GIT_CLIFF_OUTPUT",
198 value_name = "PATH",
199 value_parser = Opt::parse_dir,
200 num_args = 0..=1,
201 default_missing_value = DEFAULT_OUTPUT
202 )]
203 pub output: Option<PathBuf>,
204 #[arg(
206 short,
207 long,
208 env = "GIT_CLIFF_TAG",
209 value_name = "TAG",
210 allow_hyphen_values = true
211 )]
212 pub tag: Option<String>,
213 #[arg(
216 long,
217 value_name = "BUMP",
218 value_enum,
219 num_args = 0..=1,
220 default_missing_value = "auto",
221 value_parser = clap::value_parser!(BumpOption))]
222 pub bump: Option<BumpOption>,
223 #[arg(long, help_heading = Some("FLAGS"))]
225 pub bumped_version: bool,
226 #[arg(
228 short,
229 long,
230 env = "GIT_CLIFF_TEMPLATE",
231 value_name = "TEMPLATE",
232 allow_hyphen_values = true
233 )]
234 pub body: Option<String>,
235 #[arg(short, long, help_heading = Some("FLAGS"))]
237 pub latest: bool,
238 #[arg(long, help_heading = Some("FLAGS"))]
240 pub current: bool,
241 #[arg(short, long, help_heading = Some("FLAGS"))]
243 pub unreleased: bool,
244 #[arg(long, help_heading = Some("FLAGS"))]
246 pub topo_order: bool,
247 #[arg(long, help_heading = Some("FLAGS"))]
249 pub use_branch_tags: bool,
250 #[arg(long, help_heading = Some("FLAGS"))]
252 pub no_exec: bool,
253 #[arg(short = 'x', long, help_heading = Some("FLAGS"))]
255 pub context: bool,
256 #[arg(
258 long,
259 value_name = "PATH",
260 value_parser = Opt::parse_dir,
261 env = "GIT_CLIFF_CONTEXT",
262 )]
263 pub from_context: Option<PathBuf>,
264 #[arg(short, long, value_name = "PART", value_enum)]
266 pub strip: Option<Strip>,
267 #[arg(
269 long,
270 value_enum,
271 default_value_t = Sort::Oldest
272 )]
273 pub sort: Sort,
274 #[arg(value_name = "RANGE", help_heading = Some("ARGS"))]
276 pub range: Option<String>,
277 #[arg(
279 long,
280 env = "GITHUB_TOKEN",
281 value_name = "TOKEN",
282 hide_env_values = true,
283 hide = !cfg!(feature = "github"),
284 )]
285 pub github_token: Option<SecretString>,
286 #[arg(
288 long,
289 env = "GITHUB_REPO",
290 value_parser = clap::value_parser!(RemoteValue),
291 value_name = "OWNER/REPO",
292 hide = !cfg!(feature = "github"),
293 )]
294 pub github_repo: Option<RemoteValue>,
295 #[arg(
297 long,
298 env = "GITLAB_TOKEN",
299 value_name = "TOKEN",
300 hide_env_values = true,
301 hide = !cfg!(feature = "gitlab"),
302 )]
303 pub gitlab_token: Option<SecretString>,
304 #[arg(
306 long,
307 env = "GITLAB_REPO",
308 value_parser = clap::value_parser!(RemoteValue),
309 value_name = "OWNER/REPO",
310 hide = !cfg!(feature = "gitlab"),
311 )]
312 pub gitlab_repo: Option<RemoteValue>,
313 #[arg(
315 long,
316 env = "GITEA_TOKEN",
317 value_name = "TOKEN",
318 hide_env_values = true,
319 hide = !cfg!(feature = "gitea"),
320 )]
321 pub gitea_token: Option<SecretString>,
322 #[arg(
324 long,
325 env = "GITEA_REPO",
326 value_parser = clap::value_parser!(RemoteValue),
327 value_name = "OWNER/REPO",
328 hide = !cfg!(feature = "gitea"),
329 )]
330 pub gitea_repo: Option<RemoteValue>,
331 #[arg(
333 long,
334 env = "BITBUCKET_TOKEN",
335 value_name = "TOKEN",
336 hide_env_values = true,
337 hide = !cfg!(feature = "bitbucket"),
338 )]
339 pub bitbucket_token: Option<SecretString>,
340 #[arg(
342 long,
343 env = "BITBUCKET_REPO",
344 value_parser = clap::value_parser!(RemoteValue),
345 value_name = "OWNER/REPO",
346 hide = !cfg!(feature = "bitbucket"),
347 )]
348 pub bitbucket_repo: Option<RemoteValue>,
349 #[arg(long, help_heading = Some("FLAGS"), hide = !cfg!(feature = "remote"))]
351 pub use_native_tls: bool,
352}
353
354#[derive(Clone, Debug, PartialEq)]
356pub struct RemoteValue(pub Remote);
357
358impl ValueParserFactory for RemoteValue {
359 type Parser = RemoteValueParser;
360 fn value_parser() -> Self::Parser {
361 RemoteValueParser
362 }
363}
364
365#[derive(Clone, Debug)]
367pub struct RemoteValueParser;
368
369impl TypedValueParser for RemoteValueParser {
370 type Value = RemoteValue;
371 fn parse_ref(
372 &self,
373 cmd: &clap::Command,
374 arg: Option<&clap::Arg>,
375 value: &std::ffi::OsStr,
376 ) -> Result<Self::Value, clap::Error> {
377 let inner = clap::builder::StringValueParser::new();
378 let mut value = inner.parse_ref(cmd, arg, value)?;
379 if let Ok(url) = Url::parse(&value) {
380 value = url.path().trim_start_matches('/').to_string();
381 }
382 let parts = value.rsplit_once('/');
383 if let Some((owner, repo)) = parts {
384 Ok(RemoteValue(Remote::new(
385 owner.to_string(),
386 repo.to_string(),
387 )))
388 } else {
389 let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
390 if let Some(arg) = arg {
391 err.insert(
392 ContextKind::InvalidArg,
393 ContextValue::String(arg.to_string()),
394 );
395 }
396 err.insert(ContextKind::InvalidValue, ContextValue::String(value));
397 Err(err)
398 }
399 }
400}
401
402#[derive(Debug, Clone, Eq, PartialEq)]
403pub enum BumpOption {
404 Auto,
405 Specific(BumpType),
406}
407
408impl ValueParserFactory for BumpOption {
409 type Parser = BumpOptionParser;
410 fn value_parser() -> Self::Parser {
411 BumpOptionParser
412 }
413}
414
415#[derive(Clone, Debug)]
417pub struct BumpOptionParser;
418
419impl TypedValueParser for BumpOptionParser {
420 type Value = BumpOption;
421 fn parse_ref(
422 &self,
423 cmd: &clap::Command,
424 arg: Option<&clap::Arg>,
425 value: &std::ffi::OsStr,
426 ) -> Result<Self::Value, clap::Error> {
427 let inner = clap::builder::StringValueParser::new();
428 let value = inner.parse_ref(cmd, arg, value)?;
429 match value.as_str() {
430 "auto" => Ok(BumpOption::Auto),
431 "major" => Ok(BumpOption::Specific(BumpType::Major)),
432 "minor" => Ok(BumpOption::Specific(BumpType::Minor)),
433 "patch" => Ok(BumpOption::Specific(BumpType::Patch)),
434 _ => {
435 let mut err =
436 clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
437 if let Some(arg) = arg {
438 err.insert(
439 ContextKind::InvalidArg,
440 ContextValue::String(arg.to_string()),
441 );
442 }
443 err.insert(ContextKind::InvalidValue, ContextValue::String(value));
444 Err(err)
445 }
446 }
447 }
448}
449
450impl Opt {
451 fn parse_dir(dir: &str) -> Result<PathBuf, String> {
458 Ok(PathBuf::from(shellexpand::tilde(dir).to_string()))
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use clap::CommandFactory;
466 use std::ffi::OsStr;
467
468 #[test]
469 fn verify_cli() {
470 Opt::command().debug_assert();
471 }
472
473 #[test]
474 fn path_tilde_expansion() {
475 let home_dir = dirs::home_dir().expect("cannot retrieve home directory");
476 let dir = Opt::parse_dir("~/").expect("cannot expand tilde");
477 assert_eq!(home_dir, dir);
478 }
479
480 #[test]
481 fn remote_value_parser() -> Result<(), clap::Error> {
482 let remote_value_parser = RemoteValueParser;
483 assert_eq!(
484 RemoteValue(Remote::new("test", "repo")),
485 remote_value_parser.parse_ref(
486 &Opt::command(),
487 None,
488 OsStr::new("test/repo")
489 )?
490 );
491 assert_eq!(
492 RemoteValue(Remote::new("gitlab/group/test", "repo")),
493 remote_value_parser.parse_ref(
494 &Opt::command(),
495 None,
496 OsStr::new("gitlab/group/test/repo")
497 )?
498 );
499 assert_eq!(
500 RemoteValue(Remote::new("test", "testrepo")),
501 remote_value_parser.parse_ref(
502 &Opt::command(),
503 None,
504 OsStr::new("https://github.com/test/testrepo")
505 )?
506 );
507 assert_eq!(
508 RemoteValue(Remote::new("archlinux/packaging/packages", "arch-repro-status")),
509 remote_value_parser.parse_ref(
510 &Opt::command(),
511 None,
512 OsStr::new("https://gitlab.archlinux.org/archlinux/packaging/packages/arch-repro-status")
513 )?
514 );
515 assert!(
516 remote_value_parser
517 .parse_ref(&Opt::command(), None, OsStr::new("test"))
518 .is_err()
519 );
520 assert!(
521 remote_value_parser
522 .parse_ref(&Opt::command(), None, OsStr::new(""))
523 .is_err()
524 );
525 Ok(())
526 }
527
528 #[test]
529 fn bump_option_parser() -> Result<(), clap::Error> {
530 let bump_option_parser = BumpOptionParser;
531 assert_eq!(
532 BumpOption::Auto,
533 bump_option_parser.parse_ref(
534 &Opt::command(),
535 None,
536 OsStr::new("auto")
537 )?
538 );
539 assert!(
540 bump_option_parser
541 .parse_ref(&Opt::command(), None, OsStr::new("test"))
542 .is_err()
543 );
544 assert_eq!(
545 BumpOption::Specific(BumpType::Major),
546 bump_option_parser.parse_ref(
547 &Opt::command(),
548 None,
549 OsStr::new("major")
550 )?
551 );
552 Ok(())
553 }
554}