1use std::{
2 collections::{hash_map::Entry, HashMap},
3 sync::{
4 atomic::{AtomicUsize, Ordering},
5 Arc,
6 },
7};
8
9use gix::bstr::BStr;
10
11use crate::hours::{
12 util::{add_lines, remove_lines},
13 CommitIdx, FileStats, LineStats, WorkByEmail, WorkByPerson,
14};
15
16const MINUTES_PER_HOUR: f32 = 60.0;
17pub const HOURS_PER_WORKDAY: f32 = 8.0;
18
19pub fn estimate_hours(
20 commits: &[(u32, super::SignatureRef<'static>)],
21 stats: &[(u32, FileStats, LineStats)],
22) -> WorkByEmail {
23 assert!(!commits.is_empty());
24 const MAX_COMMIT_DIFFERENCE_IN_MINUTES: f32 = 2.0 * MINUTES_PER_HOUR;
25 const FIRST_COMMIT_ADDITION_IN_MINUTES: f32 = 2.0 * MINUTES_PER_HOUR;
26
27 let hours_for_commits = {
28 let mut hours = 0.0;
29
30 let mut commits = commits.iter().map(|t| &t.1).rev();
31 let mut cur = commits.next().expect("at least one commit if we are here");
32
33 for next in commits {
34 let change_in_minutes = (next.seconds().saturating_sub(cur.seconds())) as f32 / MINUTES_PER_HOUR;
35 if change_in_minutes < MAX_COMMIT_DIFFERENCE_IN_MINUTES {
36 hours += change_in_minutes / MINUTES_PER_HOUR;
37 } else {
38 hours += FIRST_COMMIT_ADDITION_IN_MINUTES / MINUTES_PER_HOUR;
39 }
40 cur = next;
41 }
42
43 hours
44 };
45
46 let author = &commits[0].1;
47 let (files, lines) = (!stats.is_empty())
48 .then(|| {
49 commits
50 .iter()
51 .map(|t| &t.0)
52 .fold((FileStats::default(), LineStats::default()), |mut acc, id| match stats
53 .binary_search_by(|t| t.0.cmp(id))
54 {
55 Ok(idx) => {
56 let t = &stats[idx];
57 acc.0.add(&t.1);
58 acc.1.add(&t.2);
59 acc
60 }
61 Err(_) => acc,
62 })
63 })
64 .unwrap_or_default();
65 WorkByEmail {
66 name: author.name,
67 email: author.email,
68 hours: FIRST_COMMIT_ADDITION_IN_MINUTES / 60.0 + hours_for_commits,
69 num_commits: commits.len() as u32,
70 files,
71 lines,
72 }
73}
74
75type CommitChangeLineCounters = (Arc<AtomicUsize>, Arc<AtomicUsize>, Arc<AtomicUsize>);
76
77type SpawnResultWithReturnChannelAndWorkers<'scope> = (
78 crossbeam_channel::Sender<Vec<(CommitIdx, Option<gix::hash::ObjectId>, gix::hash::ObjectId)>>,
79 Vec<std::thread::ScopedJoinHandle<'scope, anyhow::Result<Vec<(CommitIdx, FileStats, LineStats)>>>>,
80);
81
82pub fn spawn_tree_delta_threads<'scope>(
83 scope: &'scope std::thread::Scope<'scope, '_>,
84 threads: usize,
85 line_stats: bool,
86 repo: gix::Repository,
87 stat_counters: CommitChangeLineCounters,
88) -> SpawnResultWithReturnChannelAndWorkers<'scope> {
89 let (tx, rx) = crossbeam_channel::unbounded::<Vec<(CommitIdx, Option<gix::hash::ObjectId>, gix::hash::ObjectId)>>();
90 let stat_workers = (0..threads)
91 .map(|_| {
92 scope.spawn({
93 let stats_counters = stat_counters.clone();
94 let mut repo = repo.clone();
95 repo.object_cache_size_if_unset((850 * 1024 * 1024) / threads);
96 let rx = rx.clone();
97 move || -> Result<_, anyhow::Error> {
98 let mut out = Vec::new();
99 let (commits, changes, lines_count) = stats_counters;
100 let mut cache = line_stats
101 .then(|| -> anyhow::Result<_> {
102 Ok(repo.diff_resource_cache(gix::diff::blob::pipeline::Mode::ToGit, Default::default())?)
103 })
104 .transpose()?;
105 for chunk in rx {
106 for (commit_idx, parent_commit, commit) in chunk {
107 if let Some(cache) = cache.as_mut() {
108 cache.clear_resource_cache_keep_allocation();
109 }
110 commits.fetch_add(1, Ordering::Relaxed);
111 if gix::interrupt::is_triggered() {
112 return Ok(out);
113 }
114 let mut files = FileStats::default();
115 let mut lines = LineStats::default();
116 let from = match parent_commit {
117 Some(id) => match repo.find_object(id).ok().and_then(|c| c.peel_to_tree().ok()) {
118 Some(tree) => tree,
119 None => continue,
120 },
121 None => repo.empty_tree(),
122 };
123 let to = match repo.find_object(commit).ok().and_then(|c| c.peel_to_tree().ok()) {
124 Some(c) => c,
125 None => continue,
126 };
127 from.changes()?
128 .options(|opts| {
129 opts.track_filename().track_rewrites(None);
130 })
131 .for_each_to_obtain_tree(&to, |change| {
132 use gix::object::tree::diff::Change::*;
133 changes.fetch_add(1, Ordering::Relaxed);
134 match change {
135 Rewrite { .. } => {
136 unreachable!("we turned that off")
137 }
138 Addition { entry_mode, id, .. } => {
139 if entry_mode.is_no_tree() {
140 files.added += 1;
141 add_lines(line_stats, &lines_count, &mut lines, id);
142 }
143 }
144 Deletion { entry_mode, id, .. } => {
145 if entry_mode.is_no_tree() {
146 files.removed += 1;
147 remove_lines(line_stats, &lines_count, &mut lines, id);
148 }
149 }
150 Modification {
151 entry_mode,
152 previous_entry_mode,
153 id,
154 previous_id,
155 ..
156 } => match (previous_entry_mode.is_blob(), entry_mode.is_blob()) {
157 (false, false) => {}
158 (false, true) => {
159 files.added += 1;
160 add_lines(line_stats, &lines_count, &mut lines, id);
161 }
162 (true, false) => {
163 files.removed += 1;
164 remove_lines(line_stats, &lines_count, &mut lines, previous_id);
165 }
166 (true, true) => {
167 files.modified += 1;
168 if let Some(cache) = cache.as_mut() {
169 let mut diff = change.diff(cache).map_err(std::io::Error::other)?;
170 let mut nl = 0;
171 if let Some(counts) =
172 diff.line_counts().map_err(std::io::Error::other)?
173 {
174 nl += counts.insertions as usize + counts.removals as usize;
175 lines.added += counts.insertions as usize;
176 lines.removed += counts.removals as usize;
177 lines_count.fetch_add(nl, Ordering::Relaxed);
178 }
179 }
180 }
181 },
182 }
183 Ok::<_, std::io::Error>(Default::default())
184 })?;
185 out.push((commit_idx, files, lines));
186 }
187 }
188 Ok(out)
189 }
190 })
191 })
192 .collect::<Vec<_>>();
193 (tx, stat_workers)
194}
195
196pub fn deduplicate_identities(persons: &[WorkByEmail]) -> Vec<WorkByPerson> {
197 let mut email_to_index = HashMap::<&'static BStr, usize>::with_capacity(persons.len());
198 let mut name_to_index = HashMap::<&'static BStr, usize>::with_capacity(persons.len());
199 let mut out = Vec::<WorkByPerson>::with_capacity(persons.len());
200 for person_by_email in persons {
201 match email_to_index.entry(person_by_email.email) {
202 Entry::Occupied(email_entry) => {
203 out[*email_entry.get()].merge(person_by_email);
204 name_to_index.insert(person_by_email.name, *email_entry.get());
205 }
206 Entry::Vacant(email_entry) => match name_to_index.entry(person_by_email.name) {
207 Entry::Occupied(name_entry) => {
208 out[*name_entry.get()].merge(person_by_email);
209 email_entry.insert(*name_entry.get());
210 }
211 Entry::Vacant(name_entry) => {
212 let idx = out.len();
213 name_entry.insert(idx);
214 email_entry.insert(idx);
215 out.push(person_by_email.into());
216 }
217 },
218 }
219 }
220 out
221}