diff --git a/Cargo.lock b/Cargo.lock index d4ea2fe..61348d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,29 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "chrono" version = "0.4.19" @@ -21,22 +38,83 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "3.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1fe12880bae935d142c8702d500c63a4e8634b6c3c57ad72bf978fc7b6249a" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6db9e867166a43a53f7199b5e4d1f522a1e5bd626654be263c999ce59df39a" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87eba3c8c7f42ef17f6c659fc7416d0f4758cd3e58861ee63c5fa4a4dde649e4" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "commit-analyzer" version = "0.1.0" dependencies = [ "chrono", - "getopts", + "clap", "sysexits", ] [[package]] -name = "getopts" -version = "0.2.21" +name = "hashbrown" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ - "unicode-width", + "autocfg", + "hashbrown", ] [[package]] @@ -64,12 +142,98 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" + +[[package]] +name = "os_str_bytes" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sysexits" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c24dea646d0a2a4209a2d960275fd4416be9ab32c77927f51279a621e2b629" +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "time" version = "0.1.44" @@ -82,10 +246,16 @@ dependencies = [ ] [[package]] -name = "unicode-width" -version = "0.1.9" +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + +[[package]] +name = "version_check" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" @@ -109,6 +279,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index b7e3212..25b0ed2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,5 +10,5 @@ version = "0.1.0" [dependencies] chrono = "0.4.19" -getopts = "0.2.21" +clap = { version = "3.2.6", features = ["derive"] } sysexits = "0.3.0" diff --git a/src/lib.rs b/src/lib.rs index de412f0..e5b2f4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,268 @@ //! The utility functions and data structures of this project. +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +/// Parses the Git history. +#[derive(Parser, Debug)] +#[clap(version, about, long_about = None)] +pub struct Args { + /// Specifies how the program will read the Git history. + #[clap(subcommand)] + input_method: InputMethod, + + /// Always shows the entire output. + #[clap(short = 'v', long = "verbose")] + is_verbose: bool, + + /// Filters the LOC diff for a certain file extension (e.g. + /// `--file-extension cpp`). ORs if specified multiple times. + #[clap(short, long)] + file_extension: Vec, + + /// The time which may pass between two commits that still counts as working. + #[clap(short, long, default_value_t = 3)] + duration: u32, + + /// An output file for the commits per day in CSV format. + #[clap(short, long)] + output: Option, + + /// Filters for certain author names. ORs if specified multiple times. + #[clap(short, long)] + author_contains: Vec, + + /// Filters for certain author names. ORs if specified multiple times. + #[clap(long)] + author_equals: Vec, + + /// Filters for certain author emails. ORs if specified multiple times. + #[clap(short, long)] + email_contains: Vec, + + /// Filters for certain author emails. ORs if specified multiple times. + #[clap(long)] + email_equals: Vec, + + /// Filters for certain commit hashes. ORs if specified multiple times. + #[clap(short, long)] + commit_contains: Vec, + + /// Filters for certain commit hashes. ORs if specified multiple times. + #[clap(long)] + commit_equals: Vec, + + /// Filters for certain commit messages. ORs if specified multiple times. + #[clap(short, long)] + message_contains: Vec, + + /// Filters for certain commit messages. ORs if specified multiple times. + #[clap(long)] + message_equals: Vec, + + /// Filters for certain commit messages. ORs if specified multiple times. + #[clap(short = 'l', long)] + message_starts_with: Vec, +} + +impl Args { + /// Gets the input method specified by the user. + #[must_use] + pub fn input_method(&self) -> &InputMethod { + &self.input_method + } + + /// Gets the configured verbosity level of the program. + #[must_use] + pub fn is_verbose(&self) -> bool { + self.is_verbose + } + + /// Gets the maximum duration between two commits considered spent working. + #[must_use] + pub fn duration(&self) -> u32 { + self.duration + } + + /// Moves the output path specified by the user out of `Args` + /// + /// This method moves the specified path to the intended output file out of + /// this struct by calling `Option::take`. This should, hence, be called + /// just once but prevents an obsolete clone. + #[must_use] + pub fn take_output(&mut self) -> Option { + self.output.take() + } + + /// Creates a new Filter as specified by the user. + #[must_use] + pub fn filter(&self) -> Filter { + Filter { + author_contains: &self.author_contains, + author_equals: &self.author_equals, + commit_contains: &self.commit_contains, + commit_equals: &self.commit_equals, + email_contains: &self.email_contains, + email_equals: &self.email_equals, + file_extension: &self.file_extension, + message_contains: &self.message_contains, + message_equals: &self.message_equals, + message_starts_with: &self.message_starts_with, + } + } +} + +/// The possible input methods. +#[derive(Subcommand, Debug)] +pub enum InputMethod { + /// Reads the input from the local Git history. + GitHistory, + + /// Reads the specified input file. + LogFile { + /// The log file to read from. + log_file: PathBuf, + }, + + /// Reads from `stdin`. + Stdin, +} + +impl InputMethod { + /// Processes the configured input method. + pub fn read(&self) -> Result> { + match self { + Self::GitHistory => { + let process = std::process::Command::new("git") + .arg("log") + .arg("--numstat") + .output()?; + + Ok(String::from_utf8(process.stdout)?) + } + Self::LogFile { log_file } => Ok(std::fs::read_to_string(log_file)?), + Self::Stdin => { + let mut input = String::new(); + + loop { + let mut buffer = String::new(); + + if std::io::stdin().read_line(&mut buffer)? == 0 { + break; + } + + input.push_str(&buffer); + } + + Ok(input) + } + } + } +} + + +/// The revealed filter criteria. +/// +/// This data structure allows to filter the input commits by certain criteria. +#[derive(Debug, Default)] +pub struct Filter<'a> { + /// A set of substrings to be contained by some authors' names. + author_contains: &'a [String], + + /// A set of strings to match some authors's names. + author_equals: &'a [String], + + /// A set of substrings to be contained by some commits' hashes. + commit_contains: &'a [String], + + /// A set of strings to match some commits' hashes. + commit_equals: &'a [String], + + /// A set of substrings to be contained by some authors' email addresses. + email_contains: &'a [String], + + /// A set of strings to match some authors' email addresses. + email_equals: &'a [String], + + /// A set of file extensions to filter by. + file_extension: &'a [String], + + /// A set of substrings to be contained by some commits' messages. + message_contains: &'a [String], + + /// A set of strings to match some commits' messages. + message_equals: &'a [String], + + /// A set of strings to introduce some commits' messages. + message_starts_with: &'a [String], +} + +impl Filter<'_> { + /// Whether the author's email address matches the expectations. + fn check_author_email(&self, email: &str) -> bool { + let contains = + self.email_contains.is_empty() || self.email_contains.iter().any(|e| email.contains(e)); + let equals = self.email_equals.is_empty() || self.email_equals.iter().any(|e| e == email); + + equals && contains + } + + /// Whether the author's name matches the expectations. + fn check_author_name(&self, name: &str) -> bool { + let contains = self.author_contains.is_empty() + || self.author_contains.iter().any(|n| name.contains(n)); + let equals = self.author_equals.is_empty() || self.author_equals.iter().any(|n| n == name); + + equals && contains + } + + /// Whether the commit meta data matches the expectations. + fn check_commit(&self, commit: &str) -> bool { + let contains = self.commit_contains.is_empty() + || self.commit_contains.iter().any(|c| commit.contains(c)); + let equals = + self.commit_equals.is_empty() || self.commit_equals.iter().any(|c| c == commit); + + equals && contains + } + + /// Whether the LOC diff matches the expectations. + pub fn check_loc(&self, loc: &&crate::LocDiff) -> bool { + self.file_extension.is_empty() + || self + .file_extension + .iter() + .any(|ext| loc.file().ends_with(&format!(".{}", ext))) + } + + /// Whether the message matches the expectations. + fn check_message(&self, message: &str) -> bool { + let contains = self.message_contains.is_empty() + || self.message_contains.iter().any(|m| message.contains(m)); + let equals = + self.message_equals.is_empty() || self.message_equals.iter().any(|m| m == message); + let starts_with = self.message_starts_with.is_empty() + || self + .message_starts_with + .iter() + .any(|m| message.starts_with(m)); + + equals && contains && starts_with + } + + /// An abbreviation for the filter checks. + /// + /// This function checks whether the given `commit` matches the + /// expectations defined in this `filter`. + pub fn matches(&self, commit: &crate::Commit) -> bool { + self.check_author_name(commit.author().name()) + && self.check_author_email(commit.author().email()) + && self.check_commit(commit.commit()) + && self.check_message(commit.message()) + } +} + /// The author meta data. /// /// A valid author serialisation consists of @@ -29,7 +292,7 @@ impl Author { &self.name } - /// Extract the author information from the given line. + /// Extracts the author information from the given line. pub fn parse(author: &str) -> Result { let (name, remainder) = author.split_once('<').ok_or(AuthorParseError::NameFailed)?; @@ -110,7 +373,7 @@ impl Commit { &self.message } - /// Construct a new instance from the raw input data. + /// Constructs a new instance from the raw input data. pub fn parse(commit: &str) -> Result<(Self, &str), CommitParseError> { let (commit, remainder) = commit .strip_prefix("commit") @@ -234,210 +497,6 @@ pub enum CommitParseError { Unknown, } -/// The revealed filter creteria. -/// -/// This data structure allows to filter the input commits by certain creteria. -#[derive(Debug, Default)] -pub struct Filter { - /// A set of substrings to be contained by some authors' names. - author_contains: Vec, - - /// A set of strings to match some authors's names. - author_equals: Vec, - - /// A set of substrings to be contained by some commits' hashes. - commit_contains: Vec, - - /// A set of strings to match some commits' hashes. - commit_equals: Vec, - - /// A set of substrings to be contained by some authors' email - /// addresses. - email_contains: Vec, - - /// A set of strings to match some authors' email addresses. - email_equals: Vec, - - /// A set of file extensions to filter by. - file_extension: Vec, - - /// A set of substrings to be contained by some commits' messages. - message_contains: Vec, - - /// A set of strings to match some commits' messages. - message_equals: Vec, - - /// A set of strings to introduce some commits' messages. - message_starts_with: Vec, -} - -impl Filter { - /// Whether the author's email address matches the expectations. - fn check_author_email(&self, email: &str) -> bool { - let contains = - self.email_contains.is_empty() || self.email_contains.iter().any(|e| email.contains(e)); - let equals = self.email_equals.is_empty() || self.email_equals.iter().any(|e| e == email); - - equals && contains - } - - /// Whether the author's name matches the expectations. - fn check_author_name(&self, name: &str) -> bool { - let contains = self.author_contains.is_empty() - || self.author_contains.iter().any(|n| name.contains(n)); - let equals = self.author_equals.is_empty() || self.author_equals.iter().any(|n| n == name); - - equals && contains - } - - /// Whether the commit meta data matches the expectations. - fn check_commit(&self, commit: &str) -> bool { - let contains = self.commit_contains.is_empty() - || self.commit_contains.iter().any(|c| commit.contains(c)); - let equals = - self.commit_equals.is_empty() || self.commit_equals.iter().any(|c| c == commit); - - equals && contains - } - - /// Whether the LOC diff matches the expectations. - pub fn check_loc(&self, loc: &&crate::LocDiff) -> bool { - self.file_extension.is_empty() - || self - .file_extension - .iter() - .any(|ext| loc.file().ends_with(&format!(".{}", ext))) - } - - /// Whether the message matches the expectations. - fn check_message(&self, message: &str) -> bool { - let contains = self.message_contains.is_empty() - || self.message_contains.iter().any(|m| message.contains(m)); - let equals = - self.message_equals.is_empty() || self.message_equals.iter().any(|m| m == message); - let starts_with = self.message_starts_with.is_empty() - || self - .message_starts_with - .iter() - .any(|m| message.starts_with(m)); - - equals && contains && starts_with - } - - /// An abbreviation for the filter checks. - /// - /// This function checks whether the given `commit` matches the - /// expectations defined in this `filter`. - pub fn matches(&self, commit: &crate::Commit) -> bool { - self.check_author_name(commit.author().name()) - && self.check_author_email(commit.author().email()) - && self.check_commit(commit.commit()) - && self.check_message(commit.message()) - } - - /// Create a new instance from a given set of filter creteria. - pub fn new(matches: &getopts::Matches) -> Self { - Self { - author_contains: matches.opt_strs("author-contains"), - author_equals: matches.opt_strs("author-equals"), - commit_contains: matches.opt_strs("commit-contains"), - commit_equals: matches.opt_strs("commit-equals"), - email_contains: matches.opt_strs("email-contains"), - email_equals: matches.opt_strs("email-equals"), - file_extension: matches.opt_strs("file-extension"), - message_contains: matches.opt_strs("message-contains"), - message_equals: matches.opt_strs("message-equals"), - message_starts_with: matches.opt_strs("message-starts-with"), - } - } -} - -/// The possible input methods. -pub enum InputMethod { - /// Read the input from the local Git history. - GitHistory, - - /// Read the specified input file. - LogFile(String), - - /// Read from `stdin`. - Stdin, -} - -impl InputMethod { - /// Create a new instance from the given command line options. - /// - /// For each application call, there is only one input method specification - /// allowed. In case that multiple methods should be given by the - /// corresponding command line options, the application will quit with an - /// according error message and exit code since it is not clear which method - /// shall be preferred in case of different input per method. - /// - /// The check itself is performed as follows: - /// - /// * Each input method is associated with a certain bit. - /// * If an input method is requested, its bit will be set. - /// * If the resulting integer is equal to two to the power of a natural - /// number or zero, the corresponding input method will be configured; - /// else, the operation is invalid. - pub fn parse(matches: &getopts::Matches) -> Option { - const GIT: i32 = 1; - const INPUT: i32 = 2; - const STDIN: i32 = 4; - - let mut method = 0; - - if matches.opt_present("git") { - method |= GIT - } - - if matches.opt_present("input") { - method |= INPUT - } - - if matches.opt_present("stdin") { - method |= STDIN - } - - match method { - GIT => Some(Self::GitHistory), - INPUT => Some(Self::LogFile(matches.opt_str("input").unwrap())), - STDIN => Some(Self::Stdin), - _ => None, - } - } - - /// Process the configured input method. - pub fn read(&self) -> Result> { - match self { - Self::GitHistory => { - let process = std::process::Command::new("git") - .arg("log") - .arg("--numstat") - .output()?; - - Ok(String::from_utf8(process.stdout)?) - } - Self::LogFile(string) => Ok(std::fs::read_to_string(string)?), - Self::Stdin => { - let mut input = String::new(); - - loop { - let mut buffer = String::new(); - - if std::io::stdin().read_line(&mut buffer)? == 0 { - break; - } - - input.push_str(&buffer); - } - - Ok(input) - } - } - } -} - /// The LOC diff a certain commit introduces. /// /// LOC is the abbreviation for the number of **l**ines **o**f **c**ode a @@ -471,7 +530,7 @@ impl LocDiff { &self.file } - /// Calculate the LOC diff. + /// Calculates the LOC diff. pub fn loc(&self) -> i64 { if self.added.is_none() && self.removed.is_none() { 0 @@ -480,7 +539,7 @@ impl LocDiff { } } - /// Extract the LOC diff information from the given line. + /// Extracts the LOC diff information from the given line. pub fn parse(loc: &str) -> Result { let (added, remainder) = loc .split_once('\t') @@ -520,18 +579,3 @@ pub enum LocParseError { /// The tab character between the deletions and file name is missing. SecondTabulatorMissing, } - -/// A brief in-app documentation. -/// -/// This function will write a brief usage information, including a short -/// introduction to the meaning of the configured `options`, to `stdout`. -pub fn usage(options: &getopts::Options) { - let description = "Parses the Git history."; - let name = "commit-analyzer"; - let synopsis = "[OPTIONS]"; - - println!( - "{name}.\n{description}\n\n{}", - options.usage(&format!("Usage: {name} {synopsis}")) - ); -} diff --git a/src/main.rs b/src/main.rs index ed21ee2..9dcd1ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,135 +1,19 @@ use std::{collections::HashMap, io::Write, ops::AddAssign}; +use clap::Parser; + fn main() -> sysexits::ExitCode { - let mut opts = getopts::Options::new(); - let opts = opts - .optflag("", "git", "Grab the input data from the local Git history.") - .optflag("h", "help", "Show this help and exit.") - .optflag("", "stdin", "Read input data from `stdin`.") - .optflag("v", "verbose", "Always show the entire output.") - .optmulti( - "a", - "author-contains", - "Filter for certain author names. ORs if specified multiple times.", - "NAME", - ) - .optmulti( - "", - "author-equals", - "Filter for certain author names. ORs if specified multiple times.", - "NAME", - ) - .optmulti( - "e", - "email-contains", - "Filter for certain author emails. ORs if specified multiple times.", - "EMAIL", - ) - .optmulti( - "", - "email-equals", - "Filter for certain author emails. ORs if specified multiple times.", - "EMAIL", - ) - .optmulti( - "c", - "commit-contains", - "Filter for certain commit hashes. ORs if specified multiple times.", - "HASH", - ) - .optmulti( - "", - "commit-equals", - "Filter for certain commit hashes. ORs if specified multiple times.", - "HASH", - ) - .optmulti( - "f", - "file-extension", - "Filter loc for certain file extension (e.g. `--file-extension cpp`). ORs if specified multiple times.", - "EXTENSION", - ) - .optmulti( - "m", - "message-contains", - "Filter for certain commit messages. ORs if specified multiple times.", - "MESSAGE", - ) - .optmulti( - "", - "message-equals", - "Filter for certain commit messages. ORs if specified multiple times.", - "MESSAGE", - ) - .optmulti( - "l", - "message-starts-with", - "Filter for certain commit messages. ORs if specified multiple times.", - "MESSAGE", - ) - .optopt( - "d", - "duration", - "The time which may pass between two commits that still counts as working.", - "HOURS", - ) - .optopt("i", "input", "The log file to read from.", "FILE") - .optopt( - "o", - "output", - "An output file for the commits per day in CSV format.", - "FILE", - ); - let matches = match opts.parse(std::env::args()) { - Ok(it) => it, - Err(getopts::Fail::ArgumentMissing(string)) => { - eprintln!( - "{}", - opts.usage(&format!( - "There was an argument expected for option '{string}'." - )) - ); - return sysexits::ExitCode::Usage; - } - Err(getopts::Fail::UnrecognizedOption(string)) => { - eprintln!("{}", opts.usage(&format!("Unknown option '{string}'."))); - return sysexits::ExitCode::Usage; - } - Err(err) => { - eprintln!("{}", opts.usage(&format!("{err}"))); - return sysexits::ExitCode::Config; - } - }; - if matches.opt_present("help") { - commit_analyzer::usage(opts); - return sysexits::ExitCode::Ok; - } - let input = match commit_analyzer::InputMethod::parse(&matches) { - Some(input) => input, - None => { - eprintln!("{}", opts.usage("Please specify one input method.")); - return sysexits::ExitCode::Usage; - } - }; - let is_verbose = matches.opt_present("verbose"); - let max_diff_hours = match matches.opt_str("duration").map(|str| str.parse::()) { - None => 3, - Some(Ok(it)) => it, - Some(Err(error)) => { - eprintln!("Invalid duration: {error}!"); - return sysexits::ExitCode::Usage; - } - }; - let path = matches.opt_str("output"); - let commits = match input.read() { + let mut args = commit_analyzer::Args::parse(); + + let commits = match args.input_method().read() { Ok(string) => string, - Err(_) => match input { + Err(_) => match args.input_method() { commit_analyzer::InputMethod::GitHistory => { eprintln!("Reading from the Git history was not possible."); return sysexits::ExitCode::Unavailable; } - commit_analyzer::InputMethod::LogFile(string) => { - eprintln!("The input file '{string}' could not be read."); + commit_analyzer::InputMethod::LogFile { log_file: input } => { + eprintln!("The input file '{}' could not be read.", input.display()); return sysexits::ExitCode::NoInput; } commit_analyzer::InputMethod::Stdin => { @@ -155,7 +39,7 @@ fn main() -> sysexits::ExitCode { } let mut last_time = None; let mut duration = chrono::Duration::zero(); - let filter = commit_analyzer::Filter::new(&matches); + let filter = args.filter(); let mut commit_count = 0; let mut commits_per_day = HashMap::new(); let mut loc_per_day = HashMap::new(); @@ -172,12 +56,12 @@ fn main() -> sysexits::ExitCode { .add_assign(commit.loc(&filter)); if let Some(last_time) = last_time { let diff: chrono::Duration = *commit.date() - last_time; - if diff.num_hours() <= max_diff_hours as i64 { + if diff.num_hours() <= args.duration() as i64 { duration = duration + diff; } } last_time = Some(*commit.date()); - if is_verbose { + if args.is_verbose() { println!("{:#?}", commit); } } @@ -186,7 +70,7 @@ fn main() -> sysexits::ExitCode { println!("Estimated time was {}h", duration.num_hours()); println!("Found {} commits overall", commit_count); - if let Some(path) = path { + if let Some(path) = args.take_output() { let mut file = match std::fs::File::create(path) { Ok(file) => file, Err(_) => return sysexits::ExitCode::CantCreat,