use std::sync::atomic::{AtomicUsize, Ordering}; use gix::bstr::{BStr, ByteSlice}; use crate::hours::core::HOURS_PER_WORKDAY; #[derive(Debug)] pub struct WorkByPerson { pub name: Vec<&'static BStr>, pub email: Vec<&'static BStr>, pub hours: f32, pub num_commits: u32, pub files: FileStats, pub lines: LineStats, } impl WorkByPerson { pub fn merge(&mut self, other: &WorkByEmail) { if !self.name.contains(&other.name) { self.name.push(other.name); } if !self.email.contains(&other.email) { self.email.push(other.email); } self.num_commits += other.num_commits; self.hours += other.hours; self.files.add(&other.files); self.lines.add(&other.lines); } } impl<'a> From<&'a WorkByEmail> for WorkByPerson { fn from(w: &'a WorkByEmail) -> Self { WorkByPerson { name: vec![w.name], email: vec![w.email], hours: w.hours, num_commits: w.num_commits, files: w.files, lines: w.lines, } } } /// Combine all iterator elements into one String, separated by `sep`. /// /// Use the `Display` implementation of each element. /// /// extracted from /// https://github.com/rust-itertools/itertools/blob/762643f1be2217140a972745cf4d6ed69435f722/src/lib.rs#L2295-L2324 fn join(mut iter: I, sep: &str) -> String where I: Iterator, ::Item: std::fmt::Display, { use ::std::fmt::Write; match iter.next() { None => String::new(), Some(first_elt) => { // estimate lower bound of capacity needed let (lower, _) = iter.size_hint(); let mut result = String::with_capacity(sep.len() * lower); write!(&mut result, "{first_elt}").expect("enough memory"); iter.for_each(|elt| { result.push_str(sep); write!(&mut result, "{elt}").expect("enough memory"); }); result } } } impl WorkByPerson { pub fn write_to( &self, total_hours: f32, total_files: Option, total_lines: Option, mut out: impl std::io::Write, ) -> std::io::Result<()> { writeln!( out, "{names} <{mails}>", names = join(self.name.iter(), ", "), mails = join(self.email.iter(), ", ") )?; writeln!(out, "{} commits found", self.num_commits)?; writeln!( out, "total time spent: {:.02}h ({:.02} 8h days, {:.02}%)", self.hours, self.hours / HOURS_PER_WORKDAY, (self.hours / total_hours) * 100.0 )?; if let Some(total) = total_files { writeln!( out, "total files added/removed/modified: {}/{}/{} ({:.02}%)", self.files.added, self.files.removed, self.files.modified, (self.files.sum() / total.sum()) * 100.0 )?; } if let Some(total) = total_lines { writeln!( out, "total lines added/removed: {}/{} ({:.02}%)", self.lines.added, self.lines.removed, (self.lines.sum() / total.sum()) * 100.0 )?; } Ok(()) } } #[derive(Debug)] pub struct WorkByEmail { pub name: &'static BStr, pub email: &'static BStr, pub hours: f32, pub num_commits: u32, pub files: FileStats, pub lines: LineStats, } /// File statistics for a particular commit. #[derive(Debug, Default, Copy, Clone)] pub struct FileStats { /// amount of added files pub added: usize, /// amount of removed files pub removed: usize, /// amount of modified files pub modified: usize, } /// Line statistics for a particular commit. #[derive(Debug, Default, Copy, Clone)] pub struct LineStats { /// amount of added lines pub added: usize, /// amount of removed lines pub removed: usize, } impl FileStats { pub fn add(&mut self, other: &FileStats) -> &mut Self { self.added += other.added; self.removed += other.removed; self.modified += other.modified; self } pub fn sum(&self) -> f32 { (self.added + self.removed + self.modified) as f32 } } impl LineStats { pub fn add(&mut self, other: &LineStats) -> &mut Self { self.added += other.added; self.removed += other.removed; self } pub fn sum(&self) -> f32 { (self.added + self.removed) as f32 } } /// An index able to address any commit pub type CommitIdx = u32; pub fn add_lines(line_stats: bool, lines_counter: &AtomicUsize, lines: &mut LineStats, id: gix::Id<'_>) { if let Some(Ok(blob)) = line_stats.then(|| id.object()) { let nl = blob.data.lines_with_terminator().count(); lines.added += nl; lines_counter.fetch_add(nl, Ordering::Relaxed); } } pub fn remove_lines(line_stats: bool, lines_counter: &AtomicUsize, lines: &mut LineStats, id: gix::Id<'_>) { if let Some(Ok(blob)) = line_stats.then(|| id.object()) { let nl = blob.data.lines_with_terminator().count(); lines.removed += nl; lines_counter.fetch_add(nl, Ordering::Relaxed); } }