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