Skip to main content

git_stats/
cli.rs

1use clap::Parser;
2
3use crate::model::{Options, SortBy};
4
5#[derive(Parser)]
6#[command(author, version, about, long_about = None)]
7pub struct Cli {
8    #[arg(name = "revision-range", default_value = "HEAD")]
9    /// Show only commits in the specified revision range.
10    ///
11    /// When no <revision-range> is specified, it defaults to HEAD (i.e. the whole history leading
12    /// to the current commit). origin..HEAD specifies all the commits reachable from the current
13    /// commit (i.e. HEAD), but not from origin. For a complete list of ways to spell
14    /// [revision-range], see the "Specifying Ranges" section of gitrevisions(7).
15    rev_range: String,
16    #[arg(short, long)]
17    /// Show the email address of each author.
18    email: bool,
19    #[arg(short, long)]
20    /// Show who reviewed/tested commits based on `Acked-by`, `Tested-by`, and
21    /// `Reviewed-by` git trailers.
22    reviews: bool,
23    /// What column to sort by
24    #[arg(short, long, value_enum, default_value_t = SortBy::Commits)]
25    sort: SortBy,
26    /// Whether to reverse the sorting from descending to ascending
27    #[arg(long)]
28    reverse: bool,
29    /// Limit the commits output to ones with author header lines that match the specified pattern (regular expression).
30    ///
31    /// With more than one --author=<pattern>, commits whose author matches any of the given patterns are chosen.
32    #[arg(short, long)]
33    author: Option<Vec<String>>,
34    /// Limit the commits output to ones more recent than a specific date.
35    #[arg(long)]
36    since: Option<String>,
37    /// Limit the commits output to ones older than a specific date.
38    #[arg(long)]
39    until: Option<String>,
40}
41
42impl Cli {
43    /// Lower the parsed CLI into the application's [`Options`].
44    #[must_use]
45    pub fn into_options(self) -> Options {
46        Options {
47            range: self.rev_range,
48            email: self.email,
49            reviews: self.reviews,
50            sort: self.sort,
51            reverse: self.reverse,
52            authors: self.author.unwrap_or_default(),
53            since: self.since,
54            until: self.until,
55        }
56    }
57}
58
59#[cfg(test)]
60mod test {
61    use super::Cli;
62    use crate::model::{Options, SortBy};
63    use clap::Parser;
64
65    #[test]
66    fn verify_app() {
67        use clap::CommandFactory;
68        Cli::command().debug_assert();
69    }
70
71    #[test]
72    fn into_options_maps_parsed_flags() {
73        let cli = Cli::try_parse_from([
74            "git-stats",
75            "--email",
76            "--reviews",
77            "--reverse",
78            "--sort",
79            "net",
80            "--author",
81            "alice",
82            "--since",
83            "2020-01-01",
84            "--until",
85            "2021-01-01",
86            "origin..HEAD",
87        ])
88        .unwrap();
89
90        let expected = Options {
91            range: "origin..HEAD".to_string(),
92            email: true,
93            reviews: true,
94            sort: SortBy::Net,
95            reverse: true,
96            authors: vec!["alice".to_string()],
97            since: Some("2020-01-01".to_string()),
98            until: Some("2021-01-01".to_string()),
99        };
100        assert_eq!(cli.into_options(), expected);
101    }
102
103    #[test]
104    fn into_options_uses_defaults_without_flags() {
105        let opts = Cli::try_parse_from(["git-stats"]).unwrap().into_options();
106
107        let expected = Options {
108            range: "HEAD".to_string(),
109            email: false,
110            reviews: false,
111            sort: SortBy::Commits,
112            reverse: false,
113            authors: Vec::new(),
114            since: None,
115            until: None,
116        };
117        assert_eq!(opts, expected);
118    }
119}