From 8341e21f742ef6de9426edea3e40ae6fc4a703f1 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 25 Sep 2022 19:12:45 +0200 Subject: [PATCH 01/18] added args for show-authors --- src/main.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 00af81a..bf343fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ use thread_local::ThreadLocal; #[clap(version, about, long_about = None)] struct Opt { /// Start with the files with the smallest percentage - #[clap(short, long)] + #[clap(short, long, conflicts_with_all = &["show-authors", "num-authors"])] reverse: bool, /// Verbose mode (-v, -vv, -vvv, etc), disables progress bar @@ -30,16 +30,23 @@ struct Opt { no_progress: bool, /// Include all files, even the ones with no lines changed by you - #[clap(short, long)] + #[clap(short, long, conflicts_with_all = &["show-authors", "num-authors"])] all: bool, /// Your email address. You can specify multiple. Defaults to your configured `config.email` - #[clap(long)] + #[clap(long, conflicts_with_all = &["show-authors", "num-authors"])] email: Vec, /// Show percentage changed per directory #[clap(long)] tree: bool, + + /// Show the top authors of each file or directory + #[clap(long, conflicts_with_all = &["email", "all", "reverse"])] + show_authors: bool, + + #[clap(long, default_value_t = 3, conflicts_with_all = &["email", "all", "reverse"])] + num_authors: u32, } fn get_repo() -> Result { From 1a59d91802f62cc5ecf15015cab5f590107914d8 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 12:57:41 +0200 Subject: [PATCH 02/18] refactor --- src/main.rs | 108 +++++++++++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/src/main.rs b/src/main.rs index 089ac7d..7dfdc2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ use thread_local::ThreadLocal; #[command(version, about, long_about = None)] struct Opt { /// Start with the files with the smallest percentage - #[arg(short, long, conflicts_with_all = &["show-authors", "num-authors"])] + #[arg(short, long, conflicts_with_all = &["show_authors", "num_authors"])] reverse: bool, /// Verbose mode (-v, -vv, -vvv, etc), disables progress bar @@ -30,11 +30,11 @@ struct Opt { no_progress: bool, /// Include all files, even the ones with no lines changed by you - #[arg(short, long, conflicts_with_all = &["show-authors", "num-authors"])] + #[arg(short, long, conflicts_with_all = &["show_authors", "num_authors"])] all: bool, /// Your email address. You can specify multiple. Defaults to your configured `config.email` - #[arg(long, conflicts_with_all = &["show-authors", "num-authors"])] + #[arg(long, conflicts_with_all = &["show_authors", "num_authors"])] email: Vec, /// Show percentage changed per directory @@ -47,62 +47,77 @@ struct Opt { #[arg(long, default_value_t = 3, conflicts_with_all = &["email", "all", "reverse"])] num_authors: u32, + + // TODO add option to limit to subdirectory } fn get_repo() -> Result { Ok(Repository::discover(".")?) } -/// returns (lines by user with email, total lines) for the file at path -fn get_lines_in_file>( - repo: &Repository, - path: &Path, - emails: &[T], -) -> Result<(usize, usize)> { - let blame = repo.blame_file(path, Some(BlameOptions::new().use_mailmap(true)))?; - Ok(blame.iter().fold((0, 0), |acc, hunk| { - let lines = hunk.lines_in_hunk(); - let by_user = hunk - .final_signature() - .email() - .map(|e| emails.iter().any(|x| x.as_ref() == e)) - .unwrap_or(false); - (acc.0 + lines * by_user as usize, acc.1 + lines) - })) -} - -struct File<'a> { - path: &'a PathBuf, - lines_by_user: usize, +#[derive(Default)] +struct Contributions { + authors: BTreeMap, total_lines: usize, } -impl<'a> File<'a> { - fn ratio_changed(&self) -> f64 { - self.lines_by_user as f64 / self.total_lines as f64 +impl Contributions { + // TODO max commit age arg? + fn try_from_path(repo: &Repository, path: &Path) -> Result { + let blame = repo.blame_file(&path, Some(BlameOptions::new().use_mailmap(true)))?; + Ok(blame.iter().fold(Self::default(), |mut acc, hunk| { + let lines = hunk.lines_in_hunk(); + acc.total_lines += lines; + if let Some(email) = hunk.final_signature().email() { + *acc.authors.entry(email.into()).or_default() += lines; + } else { + // TODO keep track of unauthored hunks somehow? + warn!("hunk without email found in {}", path.to_string_lossy()); + } + acc + })) + } + + fn lines_by_user>(&self, author: &[S]) -> usize { + self + .authors + .iter() + .filter_map(|(key, value)| author.iter().any(|email| email.as_ref() == key).then_some(value)) + .sum() + } + + fn ratio_changed_by_user>(&self, author: &[S]) -> f64 { + let lines_by_user = self.lines_by_user(author); + lines_by_user as f64 / self.total_lines as f64 } } -fn print_files_sorted_percentage(mut files: Vec, reverse: bool, all: bool) { - files.sort_by(|a, b| { - let x = (b.ratio_changed()) - .partial_cmp(&a.ratio_changed()) - .unwrap_or(Ordering::Equal); +struct File<'a> { + path: &'a Path, + contributions: Contributions, +} + +fn print_files_sorted_percentage>(files: &[File<'_>], author: &[S], reverse: bool, all: bool) { + let mut contributions_by_author = files + .iter() + .map(|f| (f.path, f.contributions.ratio_changed_by_user(author))) + .collect::>(); + contributions_by_author.sort_by(|(_, a), (_, b)| { + let x = b.partial_cmp(&a).unwrap_or(Ordering::Equal); if reverse { x.reverse() } else { x } }); - for file in files { - let ratio = file.ratio_changed(); + for (path, ratio) in contributions_by_author { if all || ratio > 0.0 { - println!("{:>5.1}% - {}", ratio * 100.0, file.path.to_string_lossy()); + println!("{:>5.1}% - {}", ratio * 100.0, path.to_string_lossy()); } } } -fn print_tree_sorted_percentage(files: &Vec, reverse: bool, all: bool) { +fn print_tree_sorted_percentage>(files: &[File], author: &[S], reverse: bool, all: bool) { #[derive(Default)] struct Node<'a> { name: &'a OsStr, @@ -129,15 +144,15 @@ fn print_tree_sorted_percentage(files: &Vec, reverse: bool, all: bool) { let mut root = Node::new(OsStr::new("/")); for f in files { let mut node = &mut root; - node.lines_by_user += f.lines_by_user; - node.total_lines += f.total_lines; + node.lines_by_user += f.contributions.lines_by_user(author); + node.total_lines += f.contributions.total_lines; for p in f.path.iter() { node = node .children .entry(p) .or_insert_with(|| Box::new(Node::new(p))); - node.lines_by_user += f.lines_by_user; - node.total_lines += f.total_lines; + node.lines_by_user += f.contributions.lines_by_user(author); + node.total_lines += f.contributions.total_lines; } } @@ -222,18 +237,17 @@ fn main() -> Result<()> { progress.inc(1); debug!("{}", path.to_string_lossy()); let repo = repo_tls.get_or_try(get_repo).expect("unable to get repo"); - let (lines_by_user, total_lines) = match get_lines_in_file(repo, path, &emails) { - Ok(x) => x, + let contributions = match Contributions::try_from_path(repo, path) { + Ok(c) => c, Err(e) => { warn!("Error blaming file {} ({e})", path.to_string_lossy()); return None; } }; - if total_lines > 0 { + if contributions.total_lines > 0 { Some(File { path, - lines_by_user, - total_lines, + contributions, }) } else { None @@ -242,9 +256,9 @@ fn main() -> Result<()> { .collect(); if opt.tree { - print_tree_sorted_percentage(&files, opt.reverse, opt.all); + print_tree_sorted_percentage(&files, &emails, opt.reverse, opt.all); } else { - print_files_sorted_percentage(files, opt.reverse, opt.all); + print_files_sorted_percentage(&files, &emails, opt.reverse, opt.all); } Ok(()) From e4d00808f26220763bbbf837c908d0eb7aea78d2 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 12:59:35 +0200 Subject: [PATCH 03/18] cleanup --- src/main.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7dfdc2c..5feceab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,7 +47,6 @@ struct Opt { #[arg(long, default_value_t = 3, conflicts_with_all = &["email", "all", "reverse"])] num_authors: u32, - // TODO add option to limit to subdirectory } @@ -64,7 +63,7 @@ struct Contributions { impl Contributions { // TODO max commit age arg? fn try_from_path(repo: &Repository, path: &Path) -> Result { - let blame = repo.blame_file(&path, Some(BlameOptions::new().use_mailmap(true)))?; + let blame = repo.blame_file(path, Some(BlameOptions::new().use_mailmap(true)))?; Ok(blame.iter().fold(Self::default(), |mut acc, hunk| { let lines = hunk.lines_in_hunk(); acc.total_lines += lines; @@ -79,10 +78,14 @@ impl Contributions { } fn lines_by_user>(&self, author: &[S]) -> usize { - self - .authors + self.authors .iter() - .filter_map(|(key, value)| author.iter().any(|email| email.as_ref() == key).then_some(value)) + .filter_map(|(key, value)| { + author + .iter() + .any(|email| email.as_ref() == key) + .then_some(value) + }) .sum() } @@ -97,13 +100,18 @@ struct File<'a> { contributions: Contributions, } -fn print_files_sorted_percentage>(files: &[File<'_>], author: &[S], reverse: bool, all: bool) { +fn print_files_sorted_percentage>( + files: &[File<'_>], + author: &[S], + reverse: bool, + all: bool, +) { let mut contributions_by_author = files .iter() .map(|f| (f.path, f.contributions.ratio_changed_by_user(author))) .collect::>(); contributions_by_author.sort_by(|(_, a), (_, b)| { - let x = b.partial_cmp(&a).unwrap_or(Ordering::Equal); + let x = b.partial_cmp(a).unwrap_or(Ordering::Equal); if reverse { x.reverse() } else { @@ -117,7 +125,12 @@ fn print_files_sorted_percentage>(files: &[File<'_>], author: &[S] } } -fn print_tree_sorted_percentage>(files: &[File], author: &[S], reverse: bool, all: bool) { +fn print_tree_sorted_percentage>( + files: &[File], + author: &[S], + reverse: bool, + all: bool, +) { #[derive(Default)] struct Node<'a> { name: &'a OsStr, From f59f7a40c1d8f9b51e278567a4a33c926e1793c5 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 13:24:24 +0200 Subject: [PATCH 04/18] add option to limit to directory --- src/main.rs | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5feceab..99971b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,13 @@ use anyhow::{anyhow, Result}; use clap::{command, Parser}; use git2::{BlameOptions, ObjectType, Repository, TreeWalkMode, TreeWalkResult}; use indicatif::{ProgressBar, ProgressStyle}; -use log::{debug, info, warn}; +use log::{debug, info, warn, trace}; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use std::{ cmp::Ordering, collections::BTreeMap, ffi::OsStr, - path::{Path, PathBuf}, + path::{Path, PathBuf}, str::FromStr, }; use thread_local::ThreadLocal; @@ -47,11 +47,10 @@ struct Opt { #[arg(long, default_value_t = 3, conflicts_with_all = &["email", "all", "reverse"])] num_authors: u32, - // TODO add option to limit to subdirectory -} -fn get_repo() -> Result { - Ok(Repository::discover(".")?) + /// Limit to the specified directory. Defaults to the entire repo + #[arg(long)] + dir: Option, } #[derive(Default)] @@ -210,7 +209,12 @@ fn print_tree_sorted_percentage>( fn main() -> Result<()> { let opt = Opt::parse(); + let root = opt.dir.unwrap_or_else(|| PathBuf::from_str(".").unwrap()); stderrlog::new().verbosity(opt.verbose as usize).init()?; + let get_repo = || -> Result<_> { + Ok(Repository::discover(&root)?) + }; + let repo = get_repo()?; let emails = if !opt.email.is_empty() { opt.email.clone() @@ -229,17 +233,22 @@ fn main() -> Result<()> { ProgressBar::new_spinner() }; let mut paths = vec![]; - head.walk(TreeWalkMode::PreOrder, |root, entry| { + head.walk(TreeWalkMode::PreOrder, |dir, entry| { if let Some(ObjectType::Blob) = entry.kind() { if let Some(name) = entry.name() { - let path = PathBuf::from(format!("{root}{name}")); - paths.push(path); + let path = PathBuf::from(format!("{dir}{name}")); + if path.starts_with(&root) { + paths.push(path); + } else { + debug!("{} not in {}. skipping.", path.to_string_lossy(), root.to_string_lossy()); + } } else { - warn!("no name for entry in {root}"); + warn!("no name for entry in {dir}"); } } TreeWalkResult::Ok })?; + info!("blaming {paths:?}"); progress.set_style(ProgressStyle::default_bar()); progress.set_length(paths.len() as u64); let repo_tls: ThreadLocal = ThreadLocal::new(); @@ -248,8 +257,8 @@ fn main() -> Result<()> { .par_iter() .filter_map(|path| { progress.inc(1); - debug!("{}", path.to_string_lossy()); - let repo = repo_tls.get_or_try(get_repo).expect("unable to get repo"); + debug!("blaming {}", path.to_string_lossy()); + let repo = repo_tls.get_or_try(&get_repo).expect("unable to get repo"); let contributions = match Contributions::try_from_path(repo, path) { Ok(c) => c, Err(e) => { @@ -267,7 +276,7 @@ fn main() -> Result<()> { } }) .collect(); - + trace!("done blaming"); if opt.tree { print_tree_sorted_percentage(&files, &emails, opt.reverse, opt.all); } else { From 5c0f3574d95de6dae381f33eec18504e2f1615f9 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 13:27:12 +0200 Subject: [PATCH 05/18] cleanup --- src/main.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 99971b1..2631e30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,14 @@ use anyhow::{anyhow, Result}; use clap::{command, Parser}; use git2::{BlameOptions, ObjectType, Repository, TreeWalkMode, TreeWalkResult}; use indicatif::{ProgressBar, ProgressStyle}; -use log::{debug, info, warn, trace}; +use log::{debug, info, trace, warn}; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use std::{ cmp::Ordering, collections::BTreeMap, ffi::OsStr, - path::{Path, PathBuf}, str::FromStr, + path::{Path, PathBuf}, + str::FromStr, }; use thread_local::ThreadLocal; @@ -209,11 +210,12 @@ fn print_tree_sorted_percentage>( fn main() -> Result<()> { let opt = Opt::parse(); - let root = opt.dir.unwrap_or_else(|| PathBuf::from_str(".").unwrap()); + let root = opt + .dir + .clone() + .unwrap_or_else(|| PathBuf::from_str(".").unwrap()); stderrlog::new().verbosity(opt.verbose as usize).init()?; - let get_repo = || -> Result<_> { - Ok(Repository::discover(&root)?) - }; + let get_repo = || -> Result<_> { Ok(Repository::discover(&root)?) }; let repo = get_repo()?; let emails = if !opt.email.is_empty() { @@ -240,7 +242,11 @@ fn main() -> Result<()> { if path.starts_with(&root) { paths.push(path); } else { - debug!("{} not in {}. skipping.", path.to_string_lossy(), root.to_string_lossy()); + debug!( + "{} not in {}. skipping.", + path.to_string_lossy(), + root.to_string_lossy() + ); } } else { warn!("no name for entry in {dir}"); @@ -248,7 +254,11 @@ fn main() -> Result<()> { } TreeWalkResult::Ok })?; - info!("blaming {paths:?}"); + if opt.dir.is_some() { + info!("blaming limited to: {paths:?}"); + } else { + info!("blaming all paths"); + } progress.set_style(ProgressStyle::default_bar()); progress.set_length(paths.len() as u64); let repo_tls: ThreadLocal = ThreadLocal::new(); From 34aa55c5a49f67c7e73097af9a54789a5424199d Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 13:44:51 +0200 Subject: [PATCH 06/18] show authors --- src/main.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 2631e30..8286bcd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -125,6 +125,29 @@ fn print_files_sorted_percentage>( } } +fn print_file_authors(files: &[File<'_>], num_authors: usize) { + for f in files { + let mut authors = f + .contributions + .authors + .iter() + .map(|(email, lines)| { + ( + email.clone(), + *lines as f64 / f.contributions.total_lines as f64, + ) + }) + .collect::>(); + authors.sort_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + authors.truncate(num_authors); + print!("{} - [", f.path.to_string_lossy()); + for (email, contribution) in authors { + print!("({email}, {:>5.1}%)", contribution * 100.0); + } + println!("]"); + } +} + fn print_tree_sorted_percentage>( files: &[File], author: &[S], @@ -288,9 +311,14 @@ fn main() -> Result<()> { .collect(); trace!("done blaming"); if opt.tree { + // TODO show authors in the tree case as well print_tree_sorted_percentage(&files, &emails, opt.reverse, opt.all); } else { - print_files_sorted_percentage(&files, &emails, opt.reverse, opt.all); + if opt.show_authors { + print_file_authors(&files, opt.num_authors as usize); + } else { + print_files_sorted_percentage(&files, &emails, opt.reverse, opt.all); + } } Ok(()) From 910a3363d105c79810c6fab77d7173184c789e36 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 13:45:27 +0200 Subject: [PATCH 07/18] cleanup --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8286bcd..1c5c9fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,7 +101,7 @@ struct File<'a> { } fn print_files_sorted_percentage>( - files: &[File<'_>], + files: &[File], author: &[S], reverse: bool, all: bool, @@ -125,7 +125,7 @@ fn print_files_sorted_percentage>( } } -fn print_file_authors(files: &[File<'_>], num_authors: usize) { +fn print_file_authors(files: &[File], num_authors: usize) { for f in files { let mut authors = f .contributions From 236391e50698c0cd3fd20a86d0e9ad09f55ed99f Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 13:48:19 +0200 Subject: [PATCH 08/18] reverse --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1c5c9fd..7ed1e42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -138,11 +138,11 @@ fn print_file_authors(files: &[File], num_authors: usize) { ) }) .collect::>(); - authors.sort_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + authors.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(Ordering::Equal)); authors.truncate(num_authors); print!("{} - [", f.path.to_string_lossy()); for (email, contribution) in authors { - print!("({email}, {:>5.1}%)", contribution * 100.0); + print!("({email}, {:.1}%)", contribution * 100.0); } println!("]"); } From 2f87d385a330d38bb7b49a8a5b827460e30d29d9 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 15:58:44 +0200 Subject: [PATCH 09/18] printouts --- src/main.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7ed1e42..db1b565 100644 --- a/src/main.rs +++ b/src/main.rs @@ -140,11 +140,12 @@ fn print_file_authors(files: &[File], num_authors: usize) { .collect::>(); authors.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(Ordering::Equal)); authors.truncate(num_authors); - print!("{} - [", f.path.to_string_lossy()); - for (email, contribution) in authors { - print!("({email}, {:.1}%)", contribution * 100.0); - } - println!("]"); + let author_str = authors + .into_iter() + .map(|(email, contribution)| format!("{email}: {:.1}%", contribution * 100.0)) + .collect::>() + .join(", "); + println!("{} - ({author_str})", f.path.to_string_lossy()); } } From c1d3d0d9e44876f3c8c60bde29f14cb0e7b2cb5c Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 18:15:38 +0200 Subject: [PATCH 10/18] tree authors --- src/main.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index db1b565..23b4c01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use std::{ cmp::Ordering, collections::BTreeMap, ffi::OsStr, + fmt::{Display, Formatter, Write}, path::{Path, PathBuf}, str::FromStr, }; @@ -93,6 +94,22 @@ impl Contributions { let lines_by_user = self.lines_by_user(author); lines_by_user as f64 / self.total_lines as f64 } + + fn write_authors(&self, f: &mut W, num_authors: usize) -> Result<()> { + let mut authors = self + .authors + .iter() + .map(|(email, lines)| (email.clone(), *lines as f64 / self.total_lines as f64)) + .collect::>(); + authors.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(Ordering::Equal)); + authors.truncate(num_authors); + let author_str = authors + .into_iter() + .map(|(email, contribution)| format!("{email}: {:.1}%", contribution * 100.0)) + .collect::>() + .join(", "); + Ok(write!(f, "({author_str})")?) + } } struct File<'a> { @@ -232,6 +249,68 @@ fn print_tree_sorted_percentage>( print_node(&root, reverse, all, ""); } +fn print_tree_authors(files: &[File], num_authors: usize) { + #[derive(Default)] + struct Node<'a> { + // TODO is this needed? + name: &'a OsStr, + contributions: Contributions, + children: BTreeMap<&'a OsStr, Box>>, + } + + impl<'a> Node<'a> { + fn new(name: &'a OsStr) -> Self { + Self { + name, + ..Default::default() + } + } + + fn add_contribution(&mut self, contributions: &Contributions) { + self.contributions.total_lines += contributions.total_lines; + for (author, lines) in &contributions.authors { + *self + .contributions + .authors + .entry(author.clone()) + .or_default() += lines; + } + } + } + + let mut root = Node::new(OsStr::new("/")); + for f in files { + let mut node = &mut root; + node.add_contribution(&f.contributions); + for p in f.path.iter() { + node = node + .children + .entry(p) + .or_insert_with(|| Box::new(Node::new(p))); + node.add_contribution(&f.contributions); + } + } + + fn print_node<'a>(node: &Node<'a>, prefix: &str, num_authors: usize) { + print!("{} - (", node.name.to_string_lossy()); + node.contributions + .write_authors(&mut std::io::stdout(), num_authors).unwrap(); + println!(")"); + let mut it = node.children.iter().peekable(); + while let Some((_, child)) = it.next() { + print!("{prefix}"); + if it.peek().is_none() { + print!("╰── "); + print_node(&child, &format!("{prefix} "), num_authors); + } else { + print!("├── "); + print_node(&child, &format!("{prefix}│ "), num_authors); + } + } + } + print_node(&root, "", num_authors); +} + fn main() -> Result<()> { let opt = Opt::parse(); let root = opt @@ -313,7 +392,11 @@ fn main() -> Result<()> { trace!("done blaming"); if opt.tree { // TODO show authors in the tree case as well - print_tree_sorted_percentage(&files, &emails, opt.reverse, opt.all); + if opt.show_authors { + print_tree_authors(&files, opt.num_authors as usize); + } else { + print_tree_sorted_percentage(&files, &emails, opt.reverse, opt.all); + } } else { if opt.show_authors { print_file_authors(&files, opt.num_authors as usize); From 633b7f41f8917dedbabe6f0351f4836d0b80edb7 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 18:20:43 +0200 Subject: [PATCH 11/18] clippy fixes --- src/main.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 23b4c01..7091ed0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ use std::{ cmp::Ordering, collections::BTreeMap, ffi::OsStr, - fmt::{Display, Formatter, Write}, path::{Path, PathBuf}, str::FromStr, }; @@ -210,7 +209,7 @@ fn print_tree_sorted_percentage>( } } - fn print_node<'a>(node: &Node<'a>, reverse: bool, all: bool, prefix: &str) { + fn print_node(node: &Node, reverse: bool, all: bool, prefix: &str) { println!( "{} - {:.1}%", node.name.to_string_lossy(), @@ -294,17 +293,18 @@ fn print_tree_authors(files: &[File], num_authors: usize) { fn print_node<'a>(node: &Node<'a>, prefix: &str, num_authors: usize) { print!("{} - (", node.name.to_string_lossy()); node.contributions - .write_authors(&mut std::io::stdout(), num_authors).unwrap(); + .write_authors(&mut std::io::stdout(), num_authors) + .unwrap(); println!(")"); let mut it = node.children.iter().peekable(); while let Some((_, child)) = it.next() { print!("{prefix}"); if it.peek().is_none() { print!("╰── "); - print_node(&child, &format!("{prefix} "), num_authors); + print_node(child, &format!("{prefix} "), num_authors); } else { print!("├── "); - print_node(&child, &format!("{prefix}│ "), num_authors); + print_node(child, &format!("{prefix}│ "), num_authors); } } } @@ -398,6 +398,7 @@ fn main() -> Result<()> { print_tree_sorted_percentage(&files, &emails, opt.reverse, opt.all); } } else { + #[allow(clippy::collapsible_else_if)] if opt.show_authors { print_file_authors(&files, opt.num_authors as usize); } else { From c3ee2d1864852f3d0ec84b4d7cc3a899a1f57bf2 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 19:42:44 +0200 Subject: [PATCH 12/18] fix path bug --- src/main.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7091ed0..c7c6c84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,7 +71,7 @@ impl Contributions { *acc.authors.entry(email.into()).or_default() += lines; } else { // TODO keep track of unauthored hunks somehow? - warn!("hunk without email found in {}", path.to_string_lossy()); + warn!("hunk without email found in {}", path.display()); } acc })) @@ -136,7 +136,7 @@ fn print_files_sorted_percentage>( }); for (path, ratio) in contributions_by_author { if all || ratio > 0.0 { - println!("{:>5.1}% - {}", ratio * 100.0, path.to_string_lossy()); + println!("{:>5.1}% - {}", ratio * 100.0, path.display()); } } } @@ -161,7 +161,7 @@ fn print_file_authors(files: &[File], num_authors: usize) { .map(|(email, contribution)| format!("{email}: {:.1}%", contribution * 100.0)) .collect::>() .join(", "); - println!("{} - ({author_str})", f.path.to_string_lossy()); + println!("{} - ({author_str})", f.path.display()); } } @@ -313,14 +313,17 @@ fn print_tree_authors(files: &[File], num_authors: usize) { fn main() -> Result<()> { let opt = Opt::parse(); + stderrlog::new().verbosity(opt.verbose as usize).init()?; let root = opt .dir .clone() .unwrap_or_else(|| PathBuf::from_str(".").unwrap()); - stderrlog::new().verbosity(opt.verbose as usize).init()?; + let canonical_root = root.canonicalize()?; + info!("dir: {}", root.display()); let get_repo = || -> Result<_> { Ok(Repository::discover(&root)?) }; let repo = get_repo()?; + info!("repo: {}", repo.path().display()); let emails = if !opt.email.is_empty() { opt.email.clone() } else { @@ -342,14 +345,16 @@ fn main() -> Result<()> { if let Some(ObjectType::Blob) = entry.kind() { if let Some(name) = entry.name() { let path = PathBuf::from(format!("{dir}{name}")); - if path.starts_with(&root) { + let canonical_path = if let Ok(cpath) = path.canonicalize() { + cpath + } else { + warn!("unable to get canonical version of {}", path.display()); + return TreeWalkResult::Ok; + }; + if canonical_path.starts_with(&canonical_root) { paths.push(path); } else { - debug!( - "{} not in {}. skipping.", - path.to_string_lossy(), - root.to_string_lossy() - ); + debug!("{} not in {} skipping.", path.display(), root.display()); } } else { warn!("no name for entry in {dir}"); @@ -370,12 +375,12 @@ fn main() -> Result<()> { .par_iter() .filter_map(|path| { progress.inc(1); - debug!("blaming {}", path.to_string_lossy()); + debug!("blaming {}", path.display()); let repo = repo_tls.get_or_try(&get_repo).expect("unable to get repo"); let contributions = match Contributions::try_from_path(repo, path) { Ok(c) => c, Err(e) => { - warn!("Error blaming file {} ({e})", path.to_string_lossy()); + warn!("Error blaming file {} ({e})", path.display()); return None; } }; From afaa16e9d7b0ec9063efad2872fc79bf94702c56 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 19:47:31 +0200 Subject: [PATCH 13/18] cleanup --- src/main.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index c7c6c84..1693f2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,7 +94,7 @@ impl Contributions { lines_by_user as f64 / self.total_lines as f64 } - fn write_authors(&self, f: &mut W, num_authors: usize) -> Result<()> { + fn authors_str(&self, num_authors: usize) -> String { let mut authors = self .authors .iter() @@ -107,7 +107,7 @@ impl Contributions { .map(|(email, contribution)| format!("{email}: {:.1}%", contribution * 100.0)) .collect::>() .join(", "); - Ok(write!(f, "({author_str})")?) + format!("({author_str})") } } @@ -291,11 +291,7 @@ fn print_tree_authors(files: &[File], num_authors: usize) { } fn print_node<'a>(node: &Node<'a>, prefix: &str, num_authors: usize) { - print!("{} - (", node.name.to_string_lossy()); - node.contributions - .write_authors(&mut std::io::stdout(), num_authors) - .unwrap(); - println!(")"); + println!("{} - ({})", node.name.to_string_lossy(), node.contributions.authors_str(num_authors)); let mut it = node.children.iter().peekable(); while let Some((_, child)) = it.next() { print!("{prefix}"); From 915323a0409ef6960314a5a6fbd00390c07beb63 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 19:50:26 +0200 Subject: [PATCH 14/18] cleanup --- src/main.rs | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1693f2f..0a1063f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,7 @@ struct Opt { #[arg(long, conflicts_with_all = &["show_authors", "num_authors"])] email: Vec, + // TODO add option to limit the depth of tree printed /// Show percentage changed per directory #[arg(long)] tree: bool, @@ -143,25 +144,11 @@ fn print_files_sorted_percentage>( fn print_file_authors(files: &[File], num_authors: usize) { for f in files { - let mut authors = f - .contributions - .authors - .iter() - .map(|(email, lines)| { - ( - email.clone(), - *lines as f64 / f.contributions.total_lines as f64, - ) - }) - .collect::>(); - authors.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(Ordering::Equal)); - authors.truncate(num_authors); - let author_str = authors - .into_iter() - .map(|(email, contribution)| format!("{email}: {:.1}%", contribution * 100.0)) - .collect::>() - .join(", "); - println!("{} - ({author_str})", f.path.display()); + println!( + "{} - ({})", + f.path.display(), + f.contributions.authors_str(num_authors) + ); } } @@ -291,7 +278,11 @@ fn print_tree_authors(files: &[File], num_authors: usize) { } fn print_node<'a>(node: &Node<'a>, prefix: &str, num_authors: usize) { - println!("{} - ({})", node.name.to_string_lossy(), node.contributions.authors_str(num_authors)); + println!( + "{} - ({})", + node.name.to_string_lossy(), + node.contributions.authors_str(num_authors) + ); let mut it = node.children.iter().peekable(); while let Some((_, child)) = it.next() { print!("{prefix}"); @@ -392,7 +383,6 @@ fn main() -> Result<()> { .collect(); trace!("done blaming"); if opt.tree { - // TODO show authors in the tree case as well if opt.show_authors { print_tree_authors(&files, opt.num_authors as usize); } else { From 65725c71529adc0cf12d7479836a95de327ee2d0 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 19:51:14 +0200 Subject: [PATCH 15/18] change arg --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0a1063f..bcf12dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,7 +48,7 @@ struct Opt { show_authors: bool, #[arg(long, default_value_t = 3, conflicts_with_all = &["email", "all", "reverse"])] - num_authors: u32, + max_authors: u32, /// Limit to the specified directory. Defaults to the entire repo #[arg(long)] @@ -384,14 +384,14 @@ fn main() -> Result<()> { trace!("done blaming"); if opt.tree { if opt.show_authors { - print_tree_authors(&files, opt.num_authors as usize); + print_tree_authors(&files, opt.max_authors as usize); } else { print_tree_sorted_percentage(&files, &emails, opt.reverse, opt.all); } } else { #[allow(clippy::collapsible_else_if)] if opt.show_authors { - print_file_authors(&files, opt.num_authors as usize); + print_file_authors(&files, opt.max_authors as usize); } else { print_files_sorted_percentage(&files, &emails, opt.reverse, opt.all); } From c86ecee2a7fcf8803c56fdf5f3aa463a4ed0e9bc Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 19:54:31 +0200 Subject: [PATCH 16/18] arg fix --- src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index bcf12dc..cf2e8c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ use thread_local::ThreadLocal; #[command(version, about, long_about = None)] struct Opt { /// Start with the files with the smallest percentage - #[arg(short, long, conflicts_with_all = &["show_authors", "num_authors"])] + #[arg(short, long, conflicts_with_all = &["show_authors", "max_authors"])] reverse: bool, /// Verbose mode (-v, -vv, -vvv, etc), disables progress bar @@ -31,11 +31,11 @@ struct Opt { no_progress: bool, /// Include all files, even the ones with no lines changed by you - #[arg(short, long, conflicts_with_all = &["show_authors", "num_authors"])] + #[arg(short, long, conflicts_with_all = &["show_authors", "max_authors"])] all: bool, /// Your email address. You can specify multiple. Defaults to your configured `config.email` - #[arg(long, conflicts_with_all = &["show_authors", "num_authors"])] + #[arg(long, conflicts_with_all = &["show_authors", "max_authors"])] email: Vec, // TODO add option to limit the depth of tree printed @@ -47,7 +47,7 @@ struct Opt { #[arg(long, conflicts_with_all = &["email", "all", "reverse"])] show_authors: bool, - #[arg(long, default_value_t = 3, conflicts_with_all = &["email", "all", "reverse"])] + #[arg(long, default_value_t = 3, conflicts_with_all = &["email", "all", "reverse"], requires = "show_authors")] max_authors: u32, /// Limit to the specified directory. Defaults to the entire repo From b5e6283f297503d16ca9e125302610e6095ec91d Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 19:57:39 +0200 Subject: [PATCH 17/18] bumped version to 0.3.0 --- Cargo.lock | 28 +++++++++++++--------------- Cargo.toml | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d271f80..ff41c72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.0" +version = "4.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422592638015fe46332afb8fbf9361d9fa2d498d05c0c384e28710b4639e33a5" +checksum = "5840cd9093aabeabf7fd932754c435b7674520fc3ddc935c397837050f0f1e4b" dependencies = [ "atty", "bitflags", @@ -93,9 +93,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.0" +version = "4.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677ca5a153ca1804d4bf3e9d45f0f6b5ba4f950de155e373d457cd5f154cca9c" +checksum = "92289ffc6fb4a85d85c246ddb874c05a87a2e540fb6ad52f7ca07c8c1e1840b1" dependencies = [ "heck", "proc-macro-error", @@ -156,26 +156,24 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", - "once_cell", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -201,7 +199,7 @@ dependencies = [ [[package]] name = "git-suggest-ownership" -version = "0.2.0" +version = "0.3.0" dependencies = [ "anyhow", "clap", @@ -301,9 +299,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.133" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "libgit2-sys" @@ -432,9 +430,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index 5f4c2a8..65d71ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "git-suggest-ownership" authors = ["Joel Nises "] -version = "0.2.0" +version = "0.3.0" edition = "2021" [profile.release] From a42db409444d94cab09727f75d20cb5b8c422e12 Mon Sep 17 00:00:00 2001 From: Joel Nises Date: Sun, 2 Oct 2022 20:02:10 +0200 Subject: [PATCH 18/18] print fix --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index cf2e8c3..762167b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -145,7 +145,7 @@ fn print_files_sorted_percentage>( fn print_file_authors(files: &[File], num_authors: usize) { for f in files { println!( - "{} - ({})", + "{} - {}", f.path.display(), f.contributions.authors_str(num_authors) ); @@ -279,7 +279,7 @@ fn print_tree_authors(files: &[File], num_authors: usize) { fn print_node<'a>(node: &Node<'a>, prefix: &str, num_authors: usize) { println!( - "{} - ({})", + "{} - {}", node.name.to_string_lossy(), node.contributions.authors_str(num_authors) );