git_cliff/
args.rs

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