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_SKIP_TAGS", value_name = "PATTERN")]
154 pub skip_tags: Option<Regex>,
155 #[arg(long, env = "GIT_CLIFF_IGNORE_TAGS", value_name = "PATTERN")]
157 pub ignore_tags: Option<Regex>,
158 #[arg(long, env = "GIT_CLIFF_COUNT_TAGS", value_name = "PATTERN")]
160 pub count_tags: Option<Regex>,
161 #[arg(
163 long,
164 env = "GIT_CLIFF_SKIP_COMMIT",
165 value_name = "SHA1",
166 num_args(1..)
167 )]
168 pub skip_commit: Option<Vec<String>>,
169 #[arg(
171 short,
172 long,
173 env = "GIT_CLIFF_PREPEND",
174 value_name = "PATH",
175 value_parser = Opt::parse_dir
176 )]
177 pub prepend: Option<PathBuf>,
178 #[arg(
180 short,
181 long,
182 env = "GIT_CLIFF_OUTPUT",
183 value_name = "PATH",
184 value_parser = Opt::parse_dir,
185 num_args = 0..=1,
186 default_missing_value = DEFAULT_OUTPUT
187 )]
188 pub output: Option<PathBuf>,
189 #[arg(
191 short,
192 long,
193 env = "GIT_CLIFF_TAG",
194 value_name = "TAG",
195 allow_hyphen_values = true
196 )]
197 pub tag: Option<String>,
198 #[arg(
201 long,
202 value_name = "BUMP",
203 value_enum,
204 num_args = 0..=1,
205 default_missing_value = "auto",
206 value_parser = clap::value_parser!(BumpOption))]
207 pub bump: Option<BumpOption>,
208 #[arg(long, help_heading = Some("FLAGS"))]
210 pub bumped_version: bool,
211 #[arg(
213 short,
214 long,
215 env = "GIT_CLIFF_TEMPLATE",
216 value_name = "TEMPLATE",
217 allow_hyphen_values = true
218 )]
219 pub body: Option<String>,
220 #[arg(short, long, help_heading = Some("FLAGS"))]
222 pub latest: bool,
223 #[arg(long, help_heading = Some("FLAGS"))]
225 pub current: bool,
226 #[arg(short, long, help_heading = Some("FLAGS"))]
228 pub unreleased: bool,
229 #[arg(long, help_heading = Some("FLAGS"))]
231 pub topo_order: bool,
232 #[arg(long, help_heading = Some("FLAGS"))]
234 pub use_branch_tags: bool,
235 #[arg(long, help_heading = Some("FLAGS"))]
237 pub no_exec: bool,
238 #[arg(short = 'x', long, help_heading = Some("FLAGS"))]
240 pub context: bool,
241 #[arg(
243 long,
244 value_name = "PATH",
245 value_parser = Opt::parse_dir,
246 env = "GIT_CLIFF_CONTEXT",
247 )]
248 pub from_context: Option<PathBuf>,
249 #[arg(short, long, value_name = "PART", value_enum)]
251 pub strip: Option<Strip>,
252 #[arg(
254 long,
255 value_enum,
256 default_value_t = Sort::Oldest
257 )]
258 pub sort: Sort,
259 #[arg(
261 long,
262 help_heading = "REMOTE OPTIONS",
263 env = "GITHUB_TOKEN",
264 value_name = "TOKEN",
265 hide_env_values = true,
266 hide = !cfg!(feature = "github"),
267 )]
268 pub github_token: Option<SecretString>,
269 #[arg(
271 long,
272 help_heading = "REMOTE OPTIONS",
273 env = "GITHUB_REPO",
274 value_parser = clap::value_parser!(RemoteValue),
275 value_name = "OWNER/REPO",
276 hide = !cfg!(feature = "github"),
277 )]
278 pub github_repo: Option<RemoteValue>,
279 #[arg(
281 long,
282 help_heading = "REMOTE OPTIONS",
283 env = "GITLAB_TOKEN",
284 value_name = "TOKEN",
285 hide_env_values = true,
286 hide = !cfg!(feature = "gitlab"),
287 )]
288 pub gitlab_token: Option<SecretString>,
289 #[arg(
291 long,
292 help_heading = "REMOTE OPTIONS",
293 env = "GITLAB_REPO",
294 value_parser = clap::value_parser!(RemoteValue),
295 value_name = "OWNER/REPO",
296 hide = !cfg!(feature = "gitlab"),
297 )]
298 pub gitlab_repo: Option<RemoteValue>,
299 #[arg(
301 long,
302 help_heading = "REMOTE OPTIONS",
303 env = "GITEA_TOKEN",
304 value_name = "TOKEN",
305 hide_env_values = true,
306 hide = !cfg!(feature = "gitea"),
307 )]
308 pub gitea_token: Option<SecretString>,
309 #[arg(
311 long,
312 help_heading = "REMOTE OPTIONS",
313 env = "GITEA_REPO",
314 value_parser = clap::value_parser!(RemoteValue),
315 value_name = "OWNER/REPO",
316 hide = !cfg!(feature = "gitea"),
317 )]
318 pub gitea_repo: Option<RemoteValue>,
319 #[arg(
321 long,
322 help_heading = "REMOTE OPTIONS",
323 env = "BITBUCKET_TOKEN",
324 value_name = "TOKEN",
325 hide_env_values = true,
326 hide = !cfg!(feature = "bitbucket"),
327 )]
328 pub bitbucket_token: Option<SecretString>,
329 #[arg(
331 long,
332 help_heading = "REMOTE OPTIONS",
333 env = "BITBUCKET_REPO",
334 value_parser = clap::value_parser!(RemoteValue),
335 value_name = "OWNER/REPO",
336 hide = !cfg!(feature = "bitbucket"),
337 )]
338 pub bitbucket_repo: Option<RemoteValue>,
339 #[arg(
341 long,
342 help_heading = "REMOTE OPTIONS",
343 env = "AZURE_DEVOPS_TOKEN",
344 value_name = "TOKEN",
345 hide_env_values = true,
346 hide = !cfg!(feature = "azure_devops"),
347 )]
348 pub azure_devops_token: Option<SecretString>,
349 #[arg(
351 long,
352 help_heading = "REMOTE OPTIONS",
353 env = "AZURE_DEVOPS_REPO",
354 value_parser = clap::value_parser!(RemoteValue),
355 value_name = "OWNER/REPO",
356 hide = !cfg!(feature = "azure_devops"),
357 )]
358 pub azure_devops_repo: Option<RemoteValue>,
359 #[arg(value_name = "RANGE", help_heading = Some("ARGS"))]
361 pub range: Option<String>,
362 #[arg(long, help_heading = Some("FLAGS"), hide = !cfg!(feature = "remote"))]
364 pub use_native_tls: bool,
365 #[arg(long, help_heading = Some("REMOTE OPTIONS"), hide = !cfg!(feature = "remote"))]
367 pub offline: bool,
368}
369
370#[derive(Clone, Debug, PartialEq)]
372pub struct RemoteValue(pub Remote);
373
374impl ValueParserFactory for RemoteValue {
375 type Parser = RemoteValueParser;
376 fn value_parser() -> Self::Parser {
377 RemoteValueParser
378 }
379}
380
381#[derive(Clone, Debug)]
383pub struct RemoteValueParser;
384
385impl TypedValueParser for RemoteValueParser {
386 type Value = RemoteValue;
387 fn parse_ref(
388 &self,
389 cmd: &clap::Command,
390 arg: Option<&clap::Arg>,
391 value: &std::ffi::OsStr,
392 ) -> Result<Self::Value, clap::Error> {
393 let inner = clap::builder::StringValueParser::new();
394 let mut value = inner.parse_ref(cmd, arg, value)?;
395 if let Ok(url) = Url::parse(&value) {
396 value = url.path().trim_start_matches('/').to_string();
397 }
398 let parts = value.rsplit_once('/');
399 if let Some((owner, repo)) = parts {
400 Ok(RemoteValue(Remote::new(
401 owner.to_string(),
402 repo.to_string(),
403 )))
404 } else {
405 let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
406 if let Some(arg) = arg {
407 err.insert(
408 ContextKind::InvalidArg,
409 ContextValue::String(arg.to_string()),
410 );
411 }
412 err.insert(ContextKind::InvalidValue, ContextValue::String(value));
413 Err(err)
414 }
415 }
416}
417
418#[derive(Debug, Clone, Eq, PartialEq)]
419pub enum BumpOption {
420 Auto,
421 Specific(BumpType),
422}
423
424impl ValueParserFactory for BumpOption {
425 type Parser = BumpOptionParser;
426 fn value_parser() -> Self::Parser {
427 BumpOptionParser
428 }
429}
430
431#[derive(Clone, Debug)]
433pub struct BumpOptionParser;
434
435impl TypedValueParser for BumpOptionParser {
436 type Value = BumpOption;
437 fn parse_ref(
438 &self,
439 cmd: &clap::Command,
440 arg: Option<&clap::Arg>,
441 value: &std::ffi::OsStr,
442 ) -> Result<Self::Value, clap::Error> {
443 let inner = clap::builder::StringValueParser::new();
444 let value = inner.parse_ref(cmd, arg, value)?;
445 match value.as_str() {
446 "auto" => Ok(BumpOption::Auto),
447 "major" => Ok(BumpOption::Specific(BumpType::Major)),
448 "minor" => Ok(BumpOption::Specific(BumpType::Minor)),
449 "patch" => Ok(BumpOption::Specific(BumpType::Patch)),
450 _ => {
451 let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
452 if let Some(arg) = arg {
453 err.insert(
454 ContextKind::InvalidArg,
455 ContextValue::String(arg.to_string()),
456 );
457 }
458 err.insert(ContextKind::InvalidValue, ContextValue::String(value));
459 Err(err)
460 }
461 }
462 }
463}
464
465impl Opt {
466 fn parse_dir(dir: &str) -> Result<PathBuf, String> {
473 Ok(PathBuf::from(shellexpand::tilde(dir).to_string()))
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use std::ffi::OsStr;
480
481 use clap::CommandFactory;
482
483 use super::*;
484
485 #[test]
486 fn verify_cli() {
487 Opt::command().debug_assert();
488 }
489
490 #[test]
491 fn path_tilde_expansion() {
492 let home_dir = dirs::home_dir().expect("cannot retrieve home directory");
493 let dir = Opt::parse_dir("~/").expect("cannot expand tilde");
494 assert_eq!(home_dir, dir);
495 }
496
497 #[test]
498 fn remote_value_parser() -> Result<(), clap::Error> {
499 let remote_value_parser = RemoteValueParser;
500 assert_eq!(
501 RemoteValue(Remote::new("test", "repo")),
502 remote_value_parser.parse_ref(&Opt::command(), None, OsStr::new("test/repo"))?
503 );
504 assert_eq!(
505 RemoteValue(Remote::new("gitlab/group/test", "repo")),
506 remote_value_parser.parse_ref(
507 &Opt::command(),
508 None,
509 OsStr::new("gitlab/group/test/repo")
510 )?
511 );
512 assert_eq!(
513 RemoteValue(Remote::new("test", "testrepo")),
514 remote_value_parser.parse_ref(
515 &Opt::command(),
516 None,
517 OsStr::new("https://github.com/test/testrepo")
518 )?
519 );
520 assert_eq!(
521 RemoteValue(Remote::new(
522 "archlinux/packaging/packages",
523 "arch-repro-status"
524 )),
525 remote_value_parser.parse_ref(
526 &Opt::command(),
527 None,
528 OsStr::new(
529 "https://gitlab.archlinux.org/archlinux/packaging/packages/arch-repro-status"
530 )
531 )?
532 );
533 assert!(
534 remote_value_parser
535 .parse_ref(&Opt::command(), None, OsStr::new("test"))
536 .is_err()
537 );
538 assert!(
539 remote_value_parser
540 .parse_ref(&Opt::command(), None, OsStr::new(""))
541 .is_err()
542 );
543 Ok(())
544 }
545
546 #[test]
547 fn bump_option_parser() -> Result<(), clap::Error> {
548 let bump_option_parser = BumpOptionParser;
549 assert_eq!(
550 BumpOption::Auto,
551 bump_option_parser.parse_ref(&Opt::command(), None, OsStr::new("auto"))?
552 );
553 assert!(
554 bump_option_parser
555 .parse_ref(&Opt::command(), None, OsStr::new("test"))
556 .is_err()
557 );
558 assert_eq!(
559 BumpOption::Specific(BumpType::Major),
560 bump_option_parser.parse_ref(&Opt::command(), None, OsStr::new("major"))?
561 );
562 Ok(())
563 }
564}