Skip to content

Commit b8f2f8b

Browse files
committed
feat: ein tool hours -s shows statistics about files added/removed/modified. (#470)
1 parent 67ec2c7 commit b8f2f8b

File tree

1 file changed

+102
-47
lines changed

1 file changed

+102
-47
lines changed

gitoxide-core/src/hours.rs

+102-47
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ where
5555
let commit_id = repo.rev_parse_single(rev_spec)?.detach();
5656
let mut string_heap = BTreeSet::<&'static [u8]>::new();
5757

58-
let (commit_authors, is_shallow) = {
58+
let (commit_authors, stats, is_shallow) = {
5959
let stat_progress = stats.then(|| progress.add_child("extract stats")).map(|mut p| {
6060
p.init(None, progress::count("commits"));
6161
p
@@ -65,14 +65,14 @@ where
6565
let mut progress = progress.add_child("traverse commit graph");
6666
progress.init(None, progress::count("commits"));
6767

68-
std::thread::scope(|scope| -> anyhow::Result<(Vec<actor::SignatureRef<'static>>, bool)> {
68+
std::thread::scope(|scope| -> anyhow::Result<_> {
6969
let start = Instant::now();
70-
let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
70+
let (tx, rx) = std::sync::mpsc::channel::<(u32, Vec<u8>)>();
7171
let mailmap = repo.open_mailmap();
7272

73-
let commit_thread = scope.spawn(move || -> anyhow::Result<Vec<actor::SignatureRef<'static>>> {
73+
let commit_thread = scope.spawn(move || -> anyhow::Result<Vec<_>> {
7474
let mut out = Vec::new();
75-
for commit_data in rx {
75+
for (commit_idx, commit_data) in rx {
7676
if let Some(author) = git::objs::CommitRefIter::from_bytes(&commit_data)
7777
.author()
7878
.map(|author| mailmap.resolve_cow(author.trim()))
@@ -91,19 +91,22 @@ where
9191
let name = string_ref(author.name.as_ref());
9292
let email = string_ref(&author.email.as_ref());
9393

94-
out.push(actor::SignatureRef {
95-
name,
96-
email,
97-
time: author.time,
98-
});
94+
out.push((
95+
commit_idx,
96+
actor::SignatureRef {
97+
name,
98+
email,
99+
time: author.time,
100+
},
101+
));
99102
}
100103
}
101104
out.shrink_to_fit();
102105
out.sort_by(|a, b| {
103-
a.email.cmp(&b.email).then(
104-
a.time
106+
a.1.email.cmp(&b.1.email).then(
107+
a.1.time
105108
.seconds_since_unix_epoch
106-
.cmp(&b.time.seconds_since_unix_epoch)
109+
.cmp(&b.1.time.seconds_since_unix_epoch)
107110
.reverse(),
108111
)
109112
});
@@ -181,18 +184,18 @@ where
181184
let commit_iter = interrupt::Iter::new(
182185
commit_id.ancestors(|oid, buf| {
183186
progress.inc();
184-
repo.objects.find(oid, buf).map(|o| {
185-
tx.send(o.data.to_owned()).ok();
187+
repo.objects.find(oid, buf).map(|obj| {
188+
tx.send((commit_idx, obj.data.to_owned())).ok();
186189
if let Some((tx_tree, first_parent, commit)) = tx_tree_id.as_ref().and_then(|tx| {
187-
git::objs::CommitRefIter::from_bytes(o.data)
190+
git::objs::CommitRefIter::from_bytes(obj.data)
188191
.parent_ids()
189192
.next()
190193
.map(|first_parent| (tx, Some(first_parent), oid.to_owned()))
191194
}) {
192195
tx_tree.send((commit_idx, first_parent, commit)).ok();
193196
}
194-
commit_idx += 1;
195-
git::objs::CommitRefIter::from_bytes(o.data)
197+
commit_idx = commit_idx.checked_add(1).expect("less then 4 billion commits");
198+
git::objs::CommitRefIter::from_bytes(obj.data)
196199
})
197200
}),
198201
|| anyhow!("Cancelled by user"),
@@ -213,20 +216,25 @@ where
213216
progress.show_throughput(start);
214217
drop(progress);
215218

216-
let _stats_by_commit_idx = match stat_progress {
219+
let stats_by_commit_idx = match stat_progress {
217220
Some(mut progress) => {
218221
progress.set_max(Some(commit_idx as usize));
219222
let mut stats = Vec::new();
220223
for handle in stat_threads {
221224
stats.extend(handle.join().expect("no panic")?);
222225
}
226+
stats.sort_by_key(|t| t.0);
223227
progress.show_throughput(start);
224228
stats
225229
}
226230
None => Vec::new(),
227231
};
228232

229-
Ok((commit_thread.join().expect("no panic")?, is_shallow))
233+
Ok((
234+
commit_thread.join().expect("no panic")?,
235+
stats_by_commit_idx,
236+
is_shallow,
237+
))
230238
})?
231239
};
232240

@@ -235,13 +243,13 @@ where
235243
}
236244

237245
let start = Instant::now();
238-
let mut current_email = &commit_authors[0].email;
246+
let mut current_email = &commit_authors[0].1.email;
239247
let mut slice_start = 0;
240248
let mut results_by_hours = Vec::new();
241249
let mut ignored_bot_commits = 0_u32;
242-
for (idx, elm) in commit_authors.iter().enumerate() {
250+
for (idx, (_, elm)) in commit_authors.iter().enumerate() {
243251
if elm.email != *current_email {
244-
let estimate = estimate_hours(&commit_authors[slice_start..idx]);
252+
let estimate = estimate_hours(&commit_authors[slice_start..idx], &stats);
245253
slice_start = idx;
246254
current_email = &elm.email;
247255
if ignore_bots && estimate.name.contains_str(b"[bot]") {
@@ -252,7 +260,7 @@ where
252260
}
253261
}
254262
if let Some(commits) = commit_authors.get(slice_start..) {
255-
results_by_hours.push(estimate_hours(commits));
263+
results_by_hours.push(estimate_hours(commits, &stats));
256264
}
257265

258266
let num_authors = results_by_hours.len();
@@ -275,15 +283,16 @@ where
275283
));
276284

277285
let num_unique_authors = results_by_hours.len();
278-
let (total_hours, total_commits) = results_by_hours
286+
let (total_hours, total_commits, total_stats) = results_by_hours
279287
.iter()
280-
.map(|e| (e.hours, e.num_commits))
281-
.reduce(|a, b| (a.0 + b.0, a.1 + b.1))
288+
.map(|e| (e.hours, e.num_commits, e.stats))
289+
.reduce(|a, b| (a.0 + b.0, a.1 + b.1, a.2.clone().added(&b.2)))
282290
.expect("at least one commit at this point");
283291
if show_pii {
284292
results_by_hours.sort_by(|a, b| a.hours.partial_cmp(&b.hours).unwrap_or(std::cmp::Ordering::Equal));
293+
let show_stats = !stats.is_empty();
285294
for entry in results_by_hours.iter() {
286-
entry.write_to(total_hours, &mut out)?;
295+
entry.write_to(total_hours, show_stats, &mut out)?;
287296
writeln!(out)?;
288297
}
289298
}
@@ -296,6 +305,13 @@ where
296305
is_shallow.then(|| " (shallow)").unwrap_or_default(),
297306
num_authors
298307
)?;
308+
if !stats.is_empty() {
309+
writeln!(
310+
out,
311+
"total files added/removed/modified: {}/{}/{}",
312+
total_stats.added, total_stats.removed, total_stats.modified
313+
)?;
314+
}
299315
if !omit_unify_identities {
300316
writeln!(
301317
out,
@@ -318,30 +334,42 @@ where
318334
const MINUTES_PER_HOUR: f32 = 60.0;
319335
const HOURS_PER_WORKDAY: f32 = 8.0;
320336

321-
fn estimate_hours(commits: &[actor::SignatureRef<'static>]) -> WorkByEmail {
337+
fn estimate_hours(commits: &[(u32, actor::SignatureRef<'static>)], stats: &[(u32, Stats)]) -> WorkByEmail {
322338
assert!(!commits.is_empty());
323339
const MAX_COMMIT_DIFFERENCE_IN_MINUTES: f32 = 2.0 * MINUTES_PER_HOUR;
324340
const FIRST_COMMIT_ADDITION_IN_MINUTES: f32 = 2.0 * MINUTES_PER_HOUR;
325341

326-
let hours = FIRST_COMMIT_ADDITION_IN_MINUTES / 60.0
327-
+ commits.iter().rev().tuple_windows().fold(
328-
0_f32,
329-
|hours, (cur, next): (&actor::SignatureRef<'_>, &actor::SignatureRef<'_>)| {
330-
let change_in_minutes =
331-
(next.time.seconds_since_unix_epoch - cur.time.seconds_since_unix_epoch) as f32 / MINUTES_PER_HOUR;
332-
if change_in_minutes < MAX_COMMIT_DIFFERENCE_IN_MINUTES {
333-
hours + change_in_minutes as f32 / MINUTES_PER_HOUR
334-
} else {
335-
hours + (FIRST_COMMIT_ADDITION_IN_MINUTES / MINUTES_PER_HOUR)
336-
}
337-
},
338-
);
339-
let author = &commits[0];
342+
let hours_for_commits = commits.iter().map(|t| &t.1).rev().tuple_windows().fold(
343+
0_f32,
344+
|hours, (cur, next): (&actor::SignatureRef<'_>, &actor::SignatureRef<'_>)| {
345+
let change_in_minutes = (next
346+
.time
347+
.seconds_since_unix_epoch
348+
.saturating_sub(cur.time.seconds_since_unix_epoch)) as f32
349+
/ MINUTES_PER_HOUR;
350+
if change_in_minutes < MAX_COMMIT_DIFFERENCE_IN_MINUTES {
351+
hours + change_in_minutes as f32 / MINUTES_PER_HOUR
352+
} else {
353+
hours + (FIRST_COMMIT_ADDITION_IN_MINUTES / MINUTES_PER_HOUR)
354+
}
355+
},
356+
);
357+
358+
let author = &commits[0].1;
340359
WorkByEmail {
341360
name: author.name,
342361
email: author.email,
343-
hours,
362+
hours: FIRST_COMMIT_ADDITION_IN_MINUTES / 60.0 + hours_for_commits,
344363
num_commits: commits.len() as u32,
364+
stats: commits.iter().map(|t| &t.0).fold(Stats::default(), |mut acc, id| {
365+
match stats.binary_search_by(|t| t.0.cmp(id)) {
366+
Ok(idx) => {
367+
acc.add(&stats[idx].1);
368+
acc
369+
}
370+
Err(_) => acc,
371+
}
372+
}),
345373
}
346374
}
347375

@@ -378,6 +406,7 @@ struct WorkByPerson {
378406
email: Vec<&'static BStr>,
379407
hours: f32,
380408
num_commits: u32,
409+
stats: Stats,
381410
}
382411

383412
impl<'a> WorkByPerson {
@@ -390,6 +419,7 @@ impl<'a> WorkByPerson {
390419
}
391420
self.num_commits += other.num_commits;
392421
self.hours += other.hours;
422+
self.stats.add(&other.stats);
393423
}
394424
}
395425

@@ -400,12 +430,13 @@ impl<'a> From<&'a WorkByEmail> for WorkByPerson {
400430
email: vec![w.email],
401431
hours: w.hours,
402432
num_commits: w.num_commits,
433+
stats: w.stats,
403434
}
404435
}
405436
}
406437

407438
impl WorkByPerson {
408-
fn write_to(&self, total_hours: f32, mut out: impl std::io::Write) -> std::io::Result<()> {
439+
fn write_to(&self, total_hours: f32, show_stats: bool, mut out: impl std::io::Write) -> std::io::Result<()> {
409440
writeln!(
410441
out,
411442
"{} <{}>",
@@ -419,7 +450,15 @@ impl WorkByPerson {
419450
self.hours,
420451
self.hours / HOURS_PER_WORKDAY,
421452
(self.hours / total_hours) * 100.0
422-
)
453+
)?;
454+
if show_stats {
455+
writeln!(
456+
out,
457+
"total files added/removed/modified: {}/{}/{}",
458+
self.stats.added, self.stats.removed, self.stats.modified
459+
)?;
460+
}
461+
Ok(())
423462
}
424463
}
425464

@@ -429,10 +468,11 @@ struct WorkByEmail {
429468
email: &'static BStr,
430469
hours: f32,
431470
num_commits: u32,
471+
stats: Stats,
432472
}
433473

434474
/// Statistics for a particular commit.
435-
#[derive(Debug, Default)]
475+
#[derive(Debug, Default, Copy, Clone)]
436476
struct Stats {
437477
/// amount of added files
438478
added: usize,
@@ -441,3 +481,18 @@ struct Stats {
441481
/// amount of modified files
442482
modified: usize,
443483
}
484+
485+
impl Stats {
486+
fn add(&mut self, other: &Stats) -> &mut Self {
487+
self.added += other.added;
488+
self.removed += other.removed;
489+
self.modified += other.modified;
490+
self
491+
}
492+
493+
fn added(&self, other: &Stats) -> Self {
494+
let mut a = *self;
495+
a.add(other);
496+
a
497+
}
498+
}

0 commit comments

Comments
 (0)