Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Sep 20, 2022
1 parent 69ef31f commit 17b0462
Show file tree
Hide file tree
Showing 3 changed files with 274 additions and 258 deletions.
92 changes: 92 additions & 0 deletions gitoxide-core/src/hours/core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use crate::hours::FileStats;
use crate::hours::LineStats;
use crate::hours::WorkByEmail;
use crate::hours::WorkByPerson;
use git::bstr::BStr;
use git_repository as git;
use itertools::Itertools;
use std::collections::hash_map::Entry;
use std::collections::HashMap;

const MINUTES_PER_HOUR: f32 = 60.0;
pub const HOURS_PER_WORKDAY: f32 = 8.0;

pub fn estimate_hours(
commits: &[(u32, git::actor::SignatureRef<'static>)],
stats: &[(u32, FileStats, LineStats)],
) -> WorkByEmail {
assert!(!commits.is_empty());
const MAX_COMMIT_DIFFERENCE_IN_MINUTES: f32 = 2.0 * MINUTES_PER_HOUR;
const FIRST_COMMIT_ADDITION_IN_MINUTES: f32 = 2.0 * MINUTES_PER_HOUR;

let hours_for_commits = commits.iter().map(|t| &t.1).rev().tuple_windows().fold(
0_f32,
|hours, (cur, next): (&git::actor::SignatureRef<'_>, &git::actor::SignatureRef<'_>)| {
let change_in_minutes = (next
.time
.seconds_since_unix_epoch
.saturating_sub(cur.time.seconds_since_unix_epoch)) as f32
/ MINUTES_PER_HOUR;
if change_in_minutes < MAX_COMMIT_DIFFERENCE_IN_MINUTES {
hours + change_in_minutes as f32 / MINUTES_PER_HOUR
} else {
hours + (FIRST_COMMIT_ADDITION_IN_MINUTES / MINUTES_PER_HOUR)
}
},
);

let author = &commits[0].1;
let (files, lines) = (!stats.is_empty())
.then(|| {
commits
.iter()
.map(|t| &t.0)
.fold((FileStats::default(), LineStats::default()), |mut acc, id| match stats
.binary_search_by(|t| t.0.cmp(id))
{
Ok(idx) => {
let t = &stats[idx];
acc.0.add(&t.1);
acc.1.add(&t.2);
acc
}
Err(_) => acc,
})
})
.unwrap_or_default();
WorkByEmail {
name: author.name,
email: author.email,
hours: FIRST_COMMIT_ADDITION_IN_MINUTES / 60.0 + hours_for_commits,
num_commits: commits.len() as u32,
files,
lines,
}
}

pub fn deduplicate_identities(persons: &[WorkByEmail]) -> Vec<WorkByPerson> {
let mut email_to_index = HashMap::<&'static BStr, usize>::with_capacity(persons.len());
let mut name_to_index = HashMap::<&'static BStr, usize>::with_capacity(persons.len());
let mut out = Vec::<WorkByPerson>::with_capacity(persons.len());
for person_by_email in persons {
match email_to_index.entry(person_by_email.email) {
Entry::Occupied(email_entry) => {
out[*email_entry.get()].merge(person_by_email);
name_to_index.insert(&person_by_email.name, *email_entry.get());
}
Entry::Vacant(email_entry) => match name_to_index.entry(&person_by_email.name) {
Entry::Occupied(name_entry) => {
out[*name_entry.get()].merge(person_by_email);
email_entry.insert(*name_entry.get());
}
Entry::Vacant(name_entry) => {
let idx = out.len();
name_entry.insert(idx);
email_entry.insert(idx);
out.push(person_by_email.into());
}
},
}
}
out
}
264 changes: 6 additions & 258 deletions gitoxide-core/src/hours.rs → gitoxide-core/src/hours/mod.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
use std::collections::BTreeSet;
use std::convert::Infallible;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{
collections::{hash_map::Entry, HashMap},
io,
path::Path,
time::Instant,
};
use std::sync::atomic::Ordering;
use std::{io, path::Path, time::Instant};

use anyhow::{anyhow, bail};
use git_repository as git;
use git_repository::bstr::BStr;
use git_repository::{actor, bstr::ByteSlice, interrupt, prelude::*, progress, Progress};
use itertools::Itertools;

/// Additional configuration for the hours estimation functionality.
pub struct Context<W> {
Expand Down Expand Up @@ -441,254 +435,8 @@ where
Ok(())
}

fn add_lines(line_stats: bool, lines_counter: Option<&AtomicUsize>, mut lines: &mut LineStats, id: git::Id<'_>) {
if let Some(Ok(blob)) = line_stats.then(|| id.object()) {
let nl = blob.data.lines_with_terminator().count();
lines.added += nl;
if let Some(c) = lines_counter {
c.fetch_add(nl, Ordering::SeqCst);
}
}
}
fn remove_lines(line_stats: bool, lines_counter: Option<&AtomicUsize>, mut lines: &mut LineStats, id: git::Id<'_>) {
if let Some(Ok(blob)) = line_stats.then(|| id.object()) {
let nl = blob.data.lines_with_terminator().count();
lines.removed += nl;
if let Some(c) = lines_counter {
c.fetch_add(nl, Ordering::SeqCst);
}
}
}

const MINUTES_PER_HOUR: f32 = 60.0;
const HOURS_PER_WORKDAY: f32 = 8.0;

fn estimate_hours(
commits: &[(u32, actor::SignatureRef<'static>)],
stats: &[(u32, FileStats, LineStats)],
) -> WorkByEmail {
assert!(!commits.is_empty());
const MAX_COMMIT_DIFFERENCE_IN_MINUTES: f32 = 2.0 * MINUTES_PER_HOUR;
const FIRST_COMMIT_ADDITION_IN_MINUTES: f32 = 2.0 * MINUTES_PER_HOUR;

let hours_for_commits = commits.iter().map(|t| &t.1).rev().tuple_windows().fold(
0_f32,
|hours, (cur, next): (&actor::SignatureRef<'_>, &actor::SignatureRef<'_>)| {
let change_in_minutes = (next
.time
.seconds_since_unix_epoch
.saturating_sub(cur.time.seconds_since_unix_epoch)) as f32
/ MINUTES_PER_HOUR;
if change_in_minutes < MAX_COMMIT_DIFFERENCE_IN_MINUTES {
hours + change_in_minutes as f32 / MINUTES_PER_HOUR
} else {
hours + (FIRST_COMMIT_ADDITION_IN_MINUTES / MINUTES_PER_HOUR)
}
},
);

let author = &commits[0].1;
let (files, lines) = (!stats.is_empty())
.then(|| {
commits
.iter()
.map(|t| &t.0)
.fold((FileStats::default(), LineStats::default()), |mut acc, id| match stats
.binary_search_by(|t| t.0.cmp(id))
{
Ok(idx) => {
let t = &stats[idx];
acc.0.add(&t.1);
acc.1.add(&t.2);
acc
}
Err(_) => acc,
})
})
.unwrap_or_default();
WorkByEmail {
name: author.name,
email: author.email,
hours: FIRST_COMMIT_ADDITION_IN_MINUTES / 60.0 + hours_for_commits,
num_commits: commits.len() as u32,
files,
lines,
}
}

fn deduplicate_identities(persons: &[WorkByEmail]) -> Vec<WorkByPerson> {
let mut email_to_index = HashMap::<&'static BStr, usize>::with_capacity(persons.len());
let mut name_to_index = HashMap::<&'static BStr, usize>::with_capacity(persons.len());
let mut out = Vec::<WorkByPerson>::with_capacity(persons.len());
for person_by_email in persons {
match email_to_index.entry(person_by_email.email) {
Entry::Occupied(email_entry) => {
out[*email_entry.get()].merge(person_by_email);
name_to_index.insert(&person_by_email.name, *email_entry.get());
}
Entry::Vacant(email_entry) => match name_to_index.entry(&person_by_email.name) {
Entry::Occupied(name_entry) => {
out[*name_entry.get()].merge(person_by_email);
email_entry.insert(*name_entry.get());
}
Entry::Vacant(name_entry) => {
let idx = out.len();
name_entry.insert(idx);
email_entry.insert(idx);
out.push(person_by_email.into());
}
},
}
}
out
}

#[derive(Debug)]
struct WorkByPerson {
name: Vec<&'static BStr>,
email: Vec<&'static BStr>,
hours: f32,
num_commits: u32,
files: FileStats,
lines: LineStats,
}

impl<'a> WorkByPerson {
fn merge(&mut self, other: &'a 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,
}
}
}
mod core;
use self::core::{deduplicate_identities, estimate_hours, HOURS_PER_WORKDAY};

impl WorkByPerson {
fn write_to(
&self,
total_hours: f32,
total_files: Option<FileStats>,
total_lines: Option<LineStats>,
mut out: impl std::io::Write,
) -> std::io::Result<()> {
writeln!(
out,
"{} <{}>",
self.name.iter().join(", "),
self.email.iter().join(", ")
)?;
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)]
struct WorkByEmail {
name: &'static BStr,
email: &'static BStr,
hours: f32,
num_commits: u32,
files: FileStats,
lines: LineStats,
}

/// File statistics for a particular commit.
#[derive(Debug, Default, Copy, Clone)]
struct FileStats {
/// amount of added files
added: usize,
/// amount of removed files
removed: usize,
/// amount of modified files
modified: usize,
}

/// Line statistics for a particular commit.
#[derive(Debug, Default, Copy, Clone)]
struct LineStats {
/// amount of added lines
added: usize,
/// amount of removed lines
removed: usize,
}

impl FileStats {
fn add(&mut self, other: &FileStats) -> &mut Self {
self.added += other.added;
self.removed += other.removed;
self.modified += other.modified;
self
}

fn added(&self, other: &FileStats) -> Self {
let mut a = *self;
a.add(other);
a
}

fn sum(&self) -> f32 {
(self.added + self.removed + self.modified) as f32
}
}

impl LineStats {
fn add(&mut self, other: &LineStats) -> &mut Self {
self.added += other.added;
self.removed += other.removed;
self
}

fn added(&self, other: &LineStats) -> Self {
let mut a = *self;
a.add(other);
a
}

fn sum(&self) -> f32 {
(self.added + self.removed) as f32
}
}
mod util;
use util::{add_lines, remove_lines, FileStats, LineStats, WorkByEmail, WorkByPerson};

0 comments on commit 17b0462

Please sign in to comment.