git_cliff/
args.rs

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/// Command-line arguments to parse.
34#[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    /// Increases the logging verbosity.
74    #[arg(short, long, action = ArgAction::Count, alias = "debug", help_heading = Some("FLAGS"))]
75    pub verbose: u8,
76    /// Writes the default configuration file to cliff.toml
77    #[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    /// Sets the configuration file.
86    #[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    /// Sets the URL for the configuration file.
96    #[arg(long, env = "GIT_CLIFF_CONFIG_URL", value_name = "URL", hide = !cfg!(feature = "remote"))]
97    pub config_url: Option<Url>,
98    /// Sets the working directory.
99    #[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    /// Sets the git repository.
108    #[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    /// Sets the path to include related commits.
118    #[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    /// Sets the path to exclude related commits.
126    #[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    /// Sets the regex for matching git tags.
134    #[arg(long, env = "GIT_CLIFF_TAG_PATTERN", value_name = "PATTERN")]
135    pub tag_pattern: Option<Regex>,
136    /// Sets custom commit messages to include in the changelog.
137    #[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    /// Sets custom message for the latest release.
145    #[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    /// Sets the tags to ignore in the changelog.
153    #[arg(long, env = "GIT_CLIFF_IGNORE_TAGS", value_name = "PATTERN")]
154    pub ignore_tags: Option<Regex>,
155    /// Sets the tags to count in the changelog.
156    #[arg(long, env = "GIT_CLIFF_COUNT_TAGS", value_name = "PATTERN")]
157    pub count_tags: Option<Regex>,
158    /// Sets commits that will be skipped in the changelog.
159    #[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    /// Prepends entries to the given changelog file.
167    #[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    /// Writes output to the given file.
176    #[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    /// Sets the tag for the latest version.
187    #[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    /// Bumps the version for unreleased changes. Optionally with specified
196    /// version.
197    #[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    /// Prints bumped version for unreleased changes.
206    #[arg(long, help_heading = Some("FLAGS"))]
207    pub bumped_version: bool,
208    /// Sets the template for the changelog body.
209    #[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    /// Processes the commits starting from the latest tag.
218    #[arg(short, long, help_heading = Some("FLAGS"))]
219    pub latest: bool,
220    /// Processes the commits that belong to the current tag.
221    #[arg(long, help_heading = Some("FLAGS"))]
222    pub current: bool,
223    /// Processes the commits that do not belong to a tag.
224    #[arg(short, long, help_heading = Some("FLAGS"))]
225    pub unreleased: bool,
226    /// Sorts the tags topologically.
227    #[arg(long, help_heading = Some("FLAGS"))]
228    pub topo_order: bool,
229    /// Include only the tags that belong to the current branch.
230    #[arg(long, help_heading = Some("FLAGS"))]
231    pub use_branch_tags: bool,
232    /// Disables the external command execution.
233    #[arg(long, help_heading = Some("FLAGS"))]
234    pub no_exec: bool,
235    /// Prints changelog context as JSON.
236    #[arg(short = 'x', long, help_heading = Some("FLAGS"))]
237    pub context: bool,
238    /// Generates changelog from a JSON context.
239    #[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    /// Strips the given parts from the changelog.
247    #[arg(short, long, value_name = "PART", value_enum)]
248    pub strip: Option<Strip>,
249    /// Sets sorting of the commits inside sections.
250    #[arg(
251		long,
252		value_enum,
253		default_value_t = Sort::Oldest
254	)]
255    pub sort: Sort,
256    /// Sets the commit range to process.
257    #[arg(value_name = "RANGE", help_heading = Some("ARGS"))]
258    pub range: Option<String>,
259    /// Sets the GitHub API token.
260    #[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    /// Sets the GitHub repository.
269    #[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    /// Sets the GitLab API token.
278    #[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    /// Sets the GitLab repository.
287    #[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    /// Sets the Gitea API token.
296    #[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    /// Sets the Gitea repository.
305    #[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    /// Sets the Bitbucket API token.
314    #[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    /// Sets the Bitbucket repository.
323    #[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    /// Load TLS certificates from the native certificate store.
332    #[arg(long, help_heading = Some("FLAGS"), hide = !cfg!(feature = "remote"))]
333    pub use_native_tls: bool,
334}
335
336/// Custom type for the remote value.
337#[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/// Parser for the remote value.
348#[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/// Parser for bump type.
398#[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    /// Custom string parser for directories.
433    ///
434    /// Expands the tilde (`~`) character in the beginning of the
435    /// input string into contents of the path returned by [`home_dir`].
436    ///
437    /// [`home_dir`]: dirs::home_dir
438    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}