|
1 | 1 | use std::collections::{hash_map::Entry, HashMap};
|
| 2 | +use std::sync::atomic::{AtomicUsize, Ordering}; |
| 3 | +use std::sync::Arc; |
2 | 4 |
|
3 | 5 | use gix::bstr::BStr;
|
| 6 | +use gix::odb::FindExt; |
4 | 7 | use itertools::Itertools;
|
| 8 | +use smallvec::SmallVec; |
5 | 9 |
|
6 |
| -use crate::hours::{FileStats, LineStats, WorkByEmail, WorkByPerson}; |
| 10 | +use crate::hours::util::{add_lines, remove_lines}; |
| 11 | +use crate::hours::{CommitIdx, FileStats, LineStats, WorkByEmail, WorkByPerson}; |
7 | 12 |
|
8 | 13 | const MINUTES_PER_HOUR: f32 = 60.0;
|
9 | 14 | pub const HOURS_PER_WORKDAY: f32 = 8.0;
|
@@ -61,6 +66,168 @@ pub fn estimate_hours(
|
61 | 66 | }
|
62 | 67 | }
|
63 | 68 |
|
| 69 | +type CommitChangeLineCounters = ( |
| 70 | + Option<Arc<AtomicUsize>>, |
| 71 | + Option<Arc<AtomicUsize>>, |
| 72 | + Option<Arc<AtomicUsize>>, |
| 73 | +); |
| 74 | + |
| 75 | +type SpawnResultWithReturnChannelAndWorkers<'scope> = ( |
| 76 | + crossbeam_channel::Sender<Vec<(CommitIdx, Option<gix::hash::ObjectId>, gix::hash::ObjectId)>>, |
| 77 | + Vec<std::thread::ScopedJoinHandle<'scope, anyhow::Result<Vec<(CommitIdx, FileStats, LineStats)>>>>, |
| 78 | +); |
| 79 | + |
| 80 | +pub fn spawn_tree_delta_threads<'scope>( |
| 81 | + scope: &'scope std::thread::Scope<'scope, '_>, |
| 82 | + threads: usize, |
| 83 | + line_stats: bool, |
| 84 | + repo: gix::Repository, |
| 85 | + stat_counters: CommitChangeLineCounters, |
| 86 | +) -> SpawnResultWithReturnChannelAndWorkers<'scope> { |
| 87 | + let (tx, rx) = crossbeam_channel::unbounded::<Vec<(CommitIdx, Option<gix::hash::ObjectId>, gix::hash::ObjectId)>>(); |
| 88 | + let stat_workers = (0..threads) |
| 89 | + .map(|_| { |
| 90 | + scope.spawn({ |
| 91 | + let stats_counters = stat_counters.clone(); |
| 92 | + let mut repo = repo.clone(); |
| 93 | + repo.object_cache_size_if_unset((850 * 1024 * 1024) / threads); |
| 94 | + let rx = rx.clone(); |
| 95 | + move || -> Result<_, anyhow::Error> { |
| 96 | + let mut out = Vec::new(); |
| 97 | + let (commit_counter, change_counter, lines_counter) = stats_counters; |
| 98 | + let mut attributes = line_stats |
| 99 | + .then(|| -> anyhow::Result<_> { |
| 100 | + repo.index_or_load_from_head().map_err(Into::into).and_then(|index| { |
| 101 | + repo.attributes( |
| 102 | + &index, |
| 103 | + gix::worktree::cache::state::attributes::Source::IdMapping, |
| 104 | + gix::worktree::cache::state::ignore::Source::IdMapping, |
| 105 | + None, |
| 106 | + ) |
| 107 | + .map_err(Into::into) |
| 108 | + .map(|attrs| { |
| 109 | + let matches = attrs.selected_attribute_matches(["binary", "text"]); |
| 110 | + (attrs, matches) |
| 111 | + }) |
| 112 | + }) |
| 113 | + }) |
| 114 | + .transpose()?; |
| 115 | + for chunk in rx { |
| 116 | + for (commit_idx, parent_commit, commit) in chunk { |
| 117 | + if let Some(c) = commit_counter.as_ref() { |
| 118 | + c.fetch_add(1, Ordering::SeqCst); |
| 119 | + } |
| 120 | + if gix::interrupt::is_triggered() { |
| 121 | + return Ok(out); |
| 122 | + } |
| 123 | + let mut files = FileStats::default(); |
| 124 | + let mut lines = LineStats::default(); |
| 125 | + let from = match parent_commit { |
| 126 | + Some(id) => match repo.find_object(id).ok().and_then(|c| c.peel_to_tree().ok()) { |
| 127 | + Some(tree) => tree, |
| 128 | + None => continue, |
| 129 | + }, |
| 130 | + None => repo.empty_tree(), |
| 131 | + }; |
| 132 | + let to = match repo.find_object(commit).ok().and_then(|c| c.peel_to_tree().ok()) { |
| 133 | + Some(c) => c, |
| 134 | + None => continue, |
| 135 | + }; |
| 136 | + from.changes()? |
| 137 | + .track_filename() |
| 138 | + .track_rewrites(None) |
| 139 | + .for_each_to_obtain_tree(&to, |change| { |
| 140 | + use gix::object::tree::diff::change::Event::*; |
| 141 | + if let Some(c) = change_counter.as_ref() { |
| 142 | + c.fetch_add(1, Ordering::SeqCst); |
| 143 | + } |
| 144 | + match change.event { |
| 145 | + Rewrite { .. } => { |
| 146 | + unreachable!("we turned that off") |
| 147 | + } |
| 148 | + Addition { entry_mode, id } => { |
| 149 | + if entry_mode.is_no_tree() { |
| 150 | + files.added += 1; |
| 151 | + add_lines(line_stats, lines_counter.as_deref(), &mut lines, id); |
| 152 | + } |
| 153 | + } |
| 154 | + Deletion { entry_mode, id } => { |
| 155 | + if entry_mode.is_no_tree() { |
| 156 | + files.removed += 1; |
| 157 | + remove_lines(line_stats, lines_counter.as_deref(), &mut lines, id); |
| 158 | + } |
| 159 | + } |
| 160 | + Modification { |
| 161 | + entry_mode, |
| 162 | + previous_entry_mode, |
| 163 | + id, |
| 164 | + previous_id, |
| 165 | + } => { |
| 166 | + match (previous_entry_mode.is_blob(), entry_mode.is_blob()) { |
| 167 | + (false, false) => {} |
| 168 | + (false, true) => { |
| 169 | + files.added += 1; |
| 170 | + add_lines(line_stats, lines_counter.as_deref(), &mut lines, id); |
| 171 | + } |
| 172 | + (true, false) => { |
| 173 | + files.removed += 1; |
| 174 | + remove_lines( |
| 175 | + line_stats, |
| 176 | + lines_counter.as_deref(), |
| 177 | + &mut lines, |
| 178 | + previous_id, |
| 179 | + ); |
| 180 | + } |
| 181 | + (true, true) => { |
| 182 | + files.modified += 1; |
| 183 | + if let Some((attrs, matches)) = attributes.as_mut() { |
| 184 | + let entry = attrs.at_entry( |
| 185 | + change.location, |
| 186 | + Some(false), |
| 187 | + |id, buf| repo.objects.find_blob(id, buf), |
| 188 | + )?; |
| 189 | + let is_text_file = if entry.matching_attributes(matches) { |
| 190 | + let attrs: SmallVec<[_; 2]> = |
| 191 | + matches.iter_selected().collect(); |
| 192 | + let binary = &attrs[0]; |
| 193 | + let text = &attrs[1]; |
| 194 | + !binary.assignment.state.is_set() |
| 195 | + && !text.assignment.state.is_unset() |
| 196 | + } else { |
| 197 | + // In the absence of binary or text markers, we assume it's text. |
| 198 | + true |
| 199 | + }; |
| 200 | + |
| 201 | + if let Some(Ok(diff)) = |
| 202 | + is_text_file.then(|| change.event.diff()).flatten() |
| 203 | + { |
| 204 | + let mut nl = 0; |
| 205 | + let counts = diff.line_counts(); |
| 206 | + nl += counts.insertions as usize + counts.removals as usize; |
| 207 | + lines.added += counts.insertions as usize; |
| 208 | + lines.removed += counts.removals as usize; |
| 209 | + if let Some(c) = lines_counter.as_ref() { |
| 210 | + c.fetch_add(nl, Ordering::SeqCst); |
| 211 | + } |
| 212 | + } |
| 213 | + } |
| 214 | + } |
| 215 | + } |
| 216 | + } |
| 217 | + } |
| 218 | + Ok::<_, std::io::Error>(Default::default()) |
| 219 | + })?; |
| 220 | + out.push((commit_idx, files, lines)); |
| 221 | + } |
| 222 | + } |
| 223 | + Ok(out) |
| 224 | + } |
| 225 | + }) |
| 226 | + }) |
| 227 | + .collect::<Vec<_>>(); |
| 228 | + (tx, stat_workers) |
| 229 | +} |
| 230 | + |
64 | 231 | pub fn deduplicate_identities(persons: &[WorkByEmail]) -> Vec<WorkByPerson> {
|
65 | 232 | let mut email_to_index = HashMap::<&'static BStr, usize>::with_capacity(persons.len());
|
66 | 233 | let mut name_to_index = HashMap::<&'static BStr, usize>::with_capacity(persons.len());
|
|
0 commit comments