gitoxide_core/hours/
core.rs

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}