git_cliff/
args.rs

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/// Command-line arguments to parse.
52#[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	/// Increases the logging verbosity.
92	#[arg(short, long, action = ArgAction::Count, alias = "debug", help_heading = Some("FLAGS"))]
93	pub verbose:          u8,
94	/// Writes the default configuration file to cliff.toml
95	#[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	/// Sets the configuration file.
104	#[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	/// Sets the URL for the configuration file.
114	#[arg(long, env = "GIT_CLIFF_CONFIG_URL", value_name = "URL", hide = !cfg!(feature = "remote"))]
115	pub config_url:       Option<Url>,
116	/// Sets the working directory.
117	#[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	/// Sets the git repository.
126	#[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	/// Sets the path to include related commits.
136	#[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	/// Sets the path to exclude related commits.
144	#[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	/// Sets the regex for matching git tags.
152	#[arg(long, env = "GIT_CLIFF_TAG_PATTERN", value_name = "PATTERN")]
153	pub tag_pattern:      Option<Regex>,
154	/// Sets custom commit messages to include in the changelog.
155	#[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	/// Sets custom message for the latest release.
163	#[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	/// Sets the tags to ignore in the changelog.
171	#[arg(long, env = "GIT_CLIFF_IGNORE_TAGS", value_name = "PATTERN")]
172	pub ignore_tags:      Option<Regex>,
173	/// Sets the tags to count in the changelog.
174	#[arg(long, env = "GIT_CLIFF_COUNT_TAGS", value_name = "PATTERN")]
175	pub count_tags:       Option<Regex>,
176	/// Sets commits that will be skipped in the changelog.
177	#[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	/// Prepends entries to the given changelog file.
185	#[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	/// Writes output to the given file.
194	#[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	/// Sets the tag for the latest version.
205	#[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	/// Bumps the version for unreleased changes. Optionally with specified
214	/// version.
215	#[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	/// Prints bumped version for unreleased changes.
224	#[arg(long, help_heading = Some("FLAGS"))]
225	pub bumped_version:   bool,
226	/// Sets the template for the changelog body.
227	#[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	/// Processes the commits starting from the latest tag.
236	#[arg(short, long, help_heading = Some("FLAGS"))]
237	pub latest:           bool,
238	/// Processes the commits that belong to the current tag.
239	#[arg(long, help_heading = Some("FLAGS"))]
240	pub current:          bool,
241	/// Processes the commits that do not belong to a tag.
242	#[arg(short, long, help_heading = Some("FLAGS"))]
243	pub unreleased:       bool,
244	/// Sorts the tags topologically.
245	#[arg(long, help_heading = Some("FLAGS"))]
246	pub topo_order:       bool,
247	/// Include only the tags that belong to the current branch.
248	#[arg(long, help_heading = Some("FLAGS"))]
249	pub use_branch_tags:  bool,
250	/// Disables the external command execution.
251	#[arg(long, help_heading = Some("FLAGS"))]
252	pub no_exec:          bool,
253	/// Prints changelog context as JSON.
254	#[arg(short = 'x', long, help_heading = Some("FLAGS"))]
255	pub context:          bool,
256	/// Generates changelog from a JSON context.
257	#[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	/// Strips the given parts from the changelog.
265	#[arg(short, long, value_name = "PART", value_enum)]
266	pub strip:            Option<Strip>,
267	/// Sets sorting of the commits inside sections.
268	#[arg(
269		long,
270		value_enum,
271		default_value_t = Sort::Oldest
272	)]
273	pub sort:             Sort,
274	/// Sets the commit range to process.
275	#[arg(value_name = "RANGE", help_heading = Some("ARGS"))]
276	pub range:            Option<String>,
277	/// Sets the GitHub API token.
278	#[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	/// Sets the GitHub repository.
287	#[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	/// Sets the GitLab API token.
296	#[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	/// Sets the GitLab repository.
305	#[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	/// Sets the Gitea API token.
314	#[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	/// Sets the Gitea repository.
323	#[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	/// Sets the Bitbucket API token.
332	#[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	/// Sets the Bitbucket repository.
341	#[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	/// Load TLS certificates from the native certificate store.
350	#[arg(long, help_heading = Some("FLAGS"), hide = !cfg!(feature = "remote"))]
351	pub use_native_tls:   bool,
352}
353
354/// Custom type for the remote value.
355#[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/// Parser for the remote value.
366#[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/// Parser for bump type.
416#[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	/// Custom string parser for directories.
452	///
453	/// Expands the tilde (`~`) character in the beginning of the
454	/// input string into contents of the path returned by [`home_dir`].
455	///
456	/// [`home_dir`]: dirs::home_dir
457	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}