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