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)]
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(value_name = "RANGE", help_heading = Some("ARGS"))]
258 pub range: Option<String>,
259 #[arg(
261 long,
262 env = "GITHUB_TOKEN",
263 value_name = "TOKEN",
264 hide_env_values = true,
265 hide = !cfg!(feature = "github"),
266 )]
267 pub github_token: Option<SecretString>,
268 #[arg(
270 long,
271 env = "GITHUB_REPO",
272 value_parser = clap::value_parser!(RemoteValue),
273 value_name = "OWNER/REPO",
274 hide = !cfg!(feature = "github"),
275 )]
276 pub github_repo: Option<RemoteValue>,
277 #[arg(
279 long,
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 env = "GITLAB_REPO",
290 value_parser = clap::value_parser!(RemoteValue),
291 value_name = "OWNER/REPO",
292 hide = !cfg!(feature = "gitlab"),
293 )]
294 pub gitlab_repo: Option<RemoteValue>,
295 #[arg(
297 long,
298 env = "GITEA_TOKEN",
299 value_name = "TOKEN",
300 hide_env_values = true,
301 hide = !cfg!(feature = "gitea"),
302 )]
303 pub gitea_token: Option<SecretString>,
304 #[arg(
306 long,
307 env = "GITEA_REPO",
308 value_parser = clap::value_parser!(RemoteValue),
309 value_name = "OWNER/REPO",
310 hide = !cfg!(feature = "gitea"),
311 )]
312 pub gitea_repo: Option<RemoteValue>,
313 #[arg(
315 long,
316 env = "BITBUCKET_TOKEN",
317 value_name = "TOKEN",
318 hide_env_values = true,
319 hide = !cfg!(feature = "bitbucket"),
320 )]
321 pub bitbucket_token: Option<SecretString>,
322 #[arg(
324 long,
325 env = "BITBUCKET_REPO",
326 value_parser = clap::value_parser!(RemoteValue),
327 value_name = "OWNER/REPO",
328 hide = !cfg!(feature = "bitbucket"),
329 )]
330 pub bitbucket_repo: Option<RemoteValue>,
331 #[arg(long, help_heading = Some("FLAGS"), hide = !cfg!(feature = "remote"))]
333 pub use_native_tls: bool,
334}
335
336#[derive(Clone, Debug, PartialEq)]
338pub struct RemoteValue(pub Remote);
339
340impl ValueParserFactory for RemoteValue {
341 type Parser = RemoteValueParser;
342 fn value_parser() -> Self::Parser {
343 RemoteValueParser
344 }
345}
346
347#[derive(Clone, Debug)]
349pub struct RemoteValueParser;
350
351impl TypedValueParser for RemoteValueParser {
352 type Value = RemoteValue;
353 fn parse_ref(
354 &self,
355 cmd: &clap::Command,
356 arg: Option<&clap::Arg>,
357 value: &std::ffi::OsStr,
358 ) -> Result<Self::Value, clap::Error> {
359 let inner = clap::builder::StringValueParser::new();
360 let mut value = inner.parse_ref(cmd, arg, value)?;
361 if let Ok(url) = Url::parse(&value) {
362 value = url.path().trim_start_matches('/').to_string();
363 }
364 let parts = value.rsplit_once('/');
365 if let Some((owner, repo)) = parts {
366 Ok(RemoteValue(Remote::new(
367 owner.to_string(),
368 repo.to_string(),
369 )))
370 } else {
371 let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
372 if let Some(arg) = arg {
373 err.insert(
374 ContextKind::InvalidArg,
375 ContextValue::String(arg.to_string()),
376 );
377 }
378 err.insert(ContextKind::InvalidValue, ContextValue::String(value));
379 Err(err)
380 }
381 }
382}
383
384#[derive(Debug, Clone, Eq, PartialEq)]
385pub enum BumpOption {
386 Auto,
387 Specific(BumpType),
388}
389
390impl ValueParserFactory for BumpOption {
391 type Parser = BumpOptionParser;
392 fn value_parser() -> Self::Parser {
393 BumpOptionParser
394 }
395}
396
397#[derive(Clone, Debug)]
399pub struct BumpOptionParser;
400
401impl TypedValueParser for BumpOptionParser {
402 type Value = BumpOption;
403 fn parse_ref(
404 &self,
405 cmd: &clap::Command,
406 arg: Option<&clap::Arg>,
407 value: &std::ffi::OsStr,
408 ) -> Result<Self::Value, clap::Error> {
409 let inner = clap::builder::StringValueParser::new();
410 let value = inner.parse_ref(cmd, arg, value)?;
411 match value.as_str() {
412 "auto" => Ok(BumpOption::Auto),
413 "major" => Ok(BumpOption::Specific(BumpType::Major)),
414 "minor" => Ok(BumpOption::Specific(BumpType::Minor)),
415 "patch" => Ok(BumpOption::Specific(BumpType::Patch)),
416 _ => {
417 let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
418 if let Some(arg) = arg {
419 err.insert(
420 ContextKind::InvalidArg,
421 ContextValue::String(arg.to_string()),
422 );
423 }
424 err.insert(ContextKind::InvalidValue, ContextValue::String(value));
425 Err(err)
426 }
427 }
428 }
429}
430
431impl Opt {
432 fn parse_dir(dir: &str) -> Result<PathBuf, String> {
439 Ok(PathBuf::from(shellexpand::tilde(dir).to_string()))
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use std::ffi::OsStr;
446
447 use clap::CommandFactory;
448
449 use super::*;
450
451 #[test]
452 fn verify_cli() {
453 Opt::command().debug_assert();
454 }
455
456 #[test]
457 fn path_tilde_expansion() {
458 let home_dir = dirs::home_dir().expect("cannot retrieve home directory");
459 let dir = Opt::parse_dir("~/").expect("cannot expand tilde");
460 assert_eq!(home_dir, dir);
461 }
462
463 #[test]
464 fn remote_value_parser() -> Result<(), clap::Error> {
465 let remote_value_parser = RemoteValueParser;
466 assert_eq!(
467 RemoteValue(Remote::new("test", "repo")),
468 remote_value_parser.parse_ref(&Opt::command(), None, OsStr::new("test/repo"))?
469 );
470 assert_eq!(
471 RemoteValue(Remote::new("gitlab/group/test", "repo")),
472 remote_value_parser.parse_ref(
473 &Opt::command(),
474 None,
475 OsStr::new("gitlab/group/test/repo")
476 )?
477 );
478 assert_eq!(
479 RemoteValue(Remote::new("test", "testrepo")),
480 remote_value_parser.parse_ref(
481 &Opt::command(),
482 None,
483 OsStr::new("https://github.com/test/testrepo")
484 )?
485 );
486 assert_eq!(
487 RemoteValue(Remote::new(
488 "archlinux/packaging/packages",
489 "arch-repro-status"
490 )),
491 remote_value_parser.parse_ref(
492 &Opt::command(),
493 None,
494 OsStr::new(
495 "https://gitlab.archlinux.org/archlinux/packaging/packages/arch-repro-status"
496 )
497 )?
498 );
499 assert!(
500 remote_value_parser
501 .parse_ref(&Opt::command(), None, OsStr::new("test"))
502 .is_err()
503 );
504 assert!(
505 remote_value_parser
506 .parse_ref(&Opt::command(), None, OsStr::new(""))
507 .is_err()
508 );
509 Ok(())
510 }
511
512 #[test]
513 fn bump_option_parser() -> Result<(), clap::Error> {
514 let bump_option_parser = BumpOptionParser;
515 assert_eq!(
516 BumpOption::Auto,
517 bump_option_parser.parse_ref(&Opt::command(), None, OsStr::new("auto"))?
518 );
519 assert!(
520 bump_option_parser
521 .parse_ref(&Opt::command(), None, OsStr::new("test"))
522 .is_err()
523 );
524 assert_eq!(
525 BumpOption::Specific(BumpType::Major),
526 bump_option_parser.parse_ref(&Opt::command(), None, OsStr::new("major"))?
527 );
528 Ok(())
529 }
530}