| @@ -1,44 +1,59 @@ |
| 1 | 1 | use anyhow::{anyhow, bail}; |
| 2 | +use clap::Clap; |
| 2 | 3 | use git_hash::{bstr::ByteSlice, ObjectId}; |
| 3 | 4 | use git_object::bstr::BString; |
| 4 | 5 | use git_odb::find::FindExt; |
| 5 | 6 | use git_traverse::commit; |
| 6 | 7 | use itertools::Itertools; |
| 7 | 8 | use rayon::prelude::*; |
| 8 | | −use std::{path::PathBuf, time::Instant}; |
| 9 | +use std::{ |
| 10 | + ffi::{OsStr, OsString}, |
| 11 | + path::PathBuf, |
| 12 | + time::Instant, |
| 13 | +}; |
| 14 | + |
| 15 | +fn is_repo_inner(dir: &OsStr) -> anyhow::Result<()> { |
| 16 | + let p = PathBuf::from(dir).join(".git").canonicalize()?; |
| 17 | + if p.extension().unwrap_or_default() == "git" |
| 18 | + || p.file_name().unwrap_or_default() == ".git" |
| 19 | + || p.join("HEAD").is_file() |
| 20 | + { |
| 21 | + Ok(()) |
| 22 | + } else { |
| 23 | + Err(anyhow!( |
| 24 | + "Path '{}' needs to be a directory containing '.git/'", |
| 25 | + p.display() |
| 26 | + )) |
| 27 | + } |
| 28 | +} |
| 29 | + |
| 30 | +fn is_repo(dir: &OsStr) -> Result<(), String> { |
| 31 | + is_repo_inner(dir).map_err(|err| err.to_string()) |
| 32 | +} |
| 33 | + |
| 34 | +#[derive(Clap)] |
| 35 | +#[clap(name = "git-hours", about = "Estimate hours worked basted on a commit history", version = clap::crate_version!())] |
| 36 | +struct Opts { |
| 37 | + /// The directory containing a '.git/' folder. |
| 38 | + #[clap(parse(from_os_str))] |
| 39 | + #[clap(validator_os = is_repo)] |
| 40 | + working_dir: PathBuf, |
| 41 | + /// The name of the ref like 'main' or 'master' at which to start iterating the commit graph. |
| 42 | + refname: OsString, |
| 43 | + /// Omit personally identifyable information, leaving only the summary. |
| 44 | + #[clap(short = 'p' long)] |
| 45 | + omit_pii: bool, |
| 46 | +} |
| 9 | 47 | |
| 10 | 48 | fn main() -> anyhow::Result<()> { |
| 11 | | − let mut args = std::env::args(); |
| 12 | | − let repo_git_dir = args |
| 13 | | − .nth(1) |
| 14 | | − .ok_or_else(|| anyhow!("First argument is the .git directory to work in")) |
| 15 | | − .and_then(|p| { |
| 16 | | − let p = PathBuf::from(p).join(".git").canonicalize()?; |
| 17 | | − if p.extension().unwrap_or_default() == "git" |
| 18 | | − || p.file_name().unwrap_or_default() == ".git" |
| 19 | | − || p.join("HEAD").is_file() |
| 20 | | − { |
| 21 | | − Ok(p) |
| 22 | | − } else { |
| 23 | | − Err(anyhow!( |
| 24 | | − "Path '{}' needs to be a directory containing '.git/'", |
| 25 | | − p.display() |
| 26 | | − )) |
| 27 | | − } |
| 28 | | − })?; |
| 29 | | − let commit_id = args |
| 30 | | − .next() |
| 31 | | − .ok_or_else(|| { |
| 32 | | − anyhow!("Second argument is the name of the branch from which to start iteration, like 'main' or 'master'") |
| 33 | | − }) |
| 34 | | − .and_then(|name| { |
| 35 | | − ObjectId::from_hex( |
| 36 | | − &std::fs::read(repo_git_dir.join("refs").join("heads").join(name))? |
| 37 | | − .as_bstr() |
| 38 | | − .trim(), |
| 39 | | − ) |
| 40 | | − .map_err(Into::into) |
| 41 | | − })?; |
| 49 | + let opts: Opts = Opts::parse(); |
| 50 | + let repo_git_dir = opts.working_dir.join(".git"); |
| 51 | + let commit_id = ObjectId::from_hex( |
| 52 | + &std::fs::read(repo_git_dir.join("refs").join("heads").join(opts.refname))? |
| 53 | + .as_bstr() |
| 54 | + .trim(), |
| 55 | + )?; |
| 56 | + |
| 42 | 57 | let repo_objects_dir = { |
| 43 | 58 | let mut d = repo_git_dir; |
| 44 | 59 | d.push("objects"); |