diff --git a/gix-blame/src/file/function.rs b/gix-blame/src/file/function.rs index d9299689c21..08d90b0127d 100644 --- a/gix-blame/src/file/function.rs +++ b/gix-blame/src/file/function.rs @@ -114,7 +114,7 @@ pub fn file( break; } - let is_still_suspect = hunks_to_blame.iter().any(|hunk| hunk.suspects.contains_key(&suspect)); + let is_still_suspect = hunks_to_blame.iter().any(|hunk| hunk.has_suspect(&suspect)); if !is_still_suspect { // There are no `UnblamedHunk`s associated with this `suspect`, so we can continue with // the next one. @@ -189,7 +189,7 @@ pub fn file( .collect(); for hunk in hunks_to_blame.iter() { - if let Some(range_in_suspect) = hunk.suspects.get(&suspect) { + if let Some(range_in_suspect) = hunk.get_range(&suspect) { let range_in_blamed_file = hunk.range_in_blamed_file.clone(); for (blamed_line_number, source_line_number) in range_in_blamed_file.zip(range_in_suspect.clone()) { diff --git a/gix-blame/src/file/mod.rs b/gix-blame/src/file/mod.rs index 668be563533..4783ab30b19 100644 --- a/gix-blame/src/file/mod.rs +++ b/gix-blame/src/file/mod.rs @@ -39,7 +39,7 @@ fn process_change( // 3. `change` *must* be returned if it is not fully included in `hunk`. match (hunk, change) { (Some(hunk), Some(Change::Unchanged(unchanged))) => { - let Some(range_in_suspect) = hunk.suspects.get(&suspect) else { + let Some(range_in_suspect) = hunk.get_range(&suspect) else { // We don’t clone blame to `parent` as `suspect` has nothing to do with this // `hunk`. new_hunks_to_blame.push(hunk); @@ -102,7 +102,7 @@ fn process_change( } } (Some(hunk), Some(Change::AddedOrReplaced(added, number_of_lines_deleted))) => { - let Some(range_in_suspect) = hunk.suspects.get(&suspect).cloned() else { + let Some(range_in_suspect) = hunk.get_range(&suspect).cloned() else { new_hunks_to_blame.push(hunk); return (None, Some(Change::AddedOrReplaced(added, number_of_lines_deleted))); }; @@ -247,7 +247,7 @@ fn process_change( } } (Some(hunk), Some(Change::Deleted(line_number_in_destination, number_of_lines_deleted))) => { - let Some(range_in_suspect) = hunk.suspects.get(&suspect) else { + let Some(range_in_suspect) = hunk.get_range(&suspect) else { new_hunks_to_blame.push(hunk); return ( None, @@ -359,12 +359,16 @@ fn process_changes( impl UnblamedHunk { fn shift_by(mut self, suspect: ObjectId, offset: Offset) -> Self { - self.suspects.entry(suspect).and_modify(|e| *e = e.shift_by(offset)); + if let Some(position) = self.suspects.iter().position(|entry| entry.0 == suspect) { + if let Some((_, ref mut range_in_suspect)) = self.suspects.get_mut(position) { + *range_in_suspect = range_in_suspect.shift_by(offset); + } + } self } fn split_at(self, suspect: ObjectId, line_number_in_destination: u32) -> Either { - match self.suspects.get(&suspect) { + match self.get_range(&suspect) { None => Either::Left(self), Some(range_in_suspect) => { if !range_in_suspect.contains(&line_number_in_destination) { @@ -405,34 +409,38 @@ impl UnblamedHunk { /// This is like [`Self::pass_blame()`], but easier to use in places where the 'passing' is /// done 'inline'. fn passed_blame(mut self, from: ObjectId, to: ObjectId) -> Self { - if let Some(range_in_suspect) = self.suspects.remove(&from) { - self.suspects.insert(to, range_in_suspect); + if let Some(position) = self.suspects.iter().position(|entry| entry.0 == from) { + if let Some((ref mut commit_id, _)) = self.suspects.get_mut(position) { + *commit_id = to; + } } self } /// Transfer all ranges from the commit at `from` to the commit at `to`. fn pass_blame(&mut self, from: ObjectId, to: ObjectId) { - if let Some(range_in_suspect) = self.suspects.remove(&from) { - self.suspects.insert(to, range_in_suspect); + if let Some(position) = self.suspects.iter().position(|entry| entry.0 == from) { + if let Some((ref mut commit_id, _)) = self.suspects.get_mut(position) { + *commit_id = to; + } } } fn clone_blame(&mut self, from: ObjectId, to: ObjectId) { - if let Some(range_in_suspect) = self.suspects.get(&from) { - self.suspects.insert(to, range_in_suspect.clone()); + if let Some(range_in_suspect) = self.get_range(&from) { + self.suspects.push((to, range_in_suspect.clone())); } } fn remove_blame(&mut self, suspect: ObjectId) { - self.suspects.remove(&suspect); + self.suspects.retain(|entry| entry.0 != suspect); } } impl BlameEntry { /// Create an offset from a portion of the *Blamed File*. fn from_unblamed_hunk(unblamed_hunk: &UnblamedHunk, commit_id: ObjectId) -> Option { - let range_in_source_file = unblamed_hunk.suspects.get(&commit_id)?; + let range_in_source_file = unblamed_hunk.get_range(&commit_id)?; Some(Self { start_in_blamed_file: unblamed_hunk.range_in_blamed_file.start, diff --git a/gix-blame/src/types.rs b/gix-blame/src/types.rs index 9f5bc7a528c..f7fdb6608d1 100644 --- a/gix-blame/src/types.rs +++ b/gix-blame/src/types.rs @@ -1,11 +1,9 @@ use crate::file::function::tokens_for_diffing; use gix_hash::ObjectId; use gix_object::bstr::BString; +use smallvec::SmallVec; use std::num::NonZeroU32; -use std::{ - collections::BTreeMap, - ops::{AddAssign, Range, SubAssign}, -}; +use std::ops::{AddAssign, Range, SubAssign}; /// Options to be passed to [`file()`](crate::file()). #[derive(Default, Debug, Clone)] @@ -198,8 +196,23 @@ impl LineRange for Range { pub struct UnblamedHunk { /// The range in the file that is being blamed that this hunk represents. pub range_in_blamed_file: Range, - /// Maps a commit to the range in a source file (i.e. *Blamed File* at a revision) that is equal to `range_in_blamed_file`. - pub suspects: BTreeMap>, + /// Maps a commit to the range in a source file (i.e. *Blamed File* at a revision) that is + /// equal to `range_in_blamed_file`. Since `suspects` rarely contains more than 1 item, it can + /// efficiently be stored as a `SmallVec`. + pub suspects: SmallVec<[(ObjectId, Range); 1]>, +} + +impl UnblamedHunk { + pub(crate) fn has_suspect(&self, suspect: &ObjectId) -> bool { + self.suspects.iter().any(|entry| entry.0 == *suspect) + } + + pub(crate) fn get_range(&self, suspect: &ObjectId) -> Option<&Range> { + self.suspects + .iter() + .find(|entry| entry.0 == *suspect) + .map(|entry| &entry.1) + } } #[derive(Debug)]