diff --git a/gitoxide-core/src/repository/blame.rs b/gitoxide-core/src/repository/blame.rs index 2d2930b30dd..a783f9e4d39 100644 --- a/gitoxide-core/src/repository/blame.rs +++ b/gitoxide-core/src/repository/blame.rs @@ -43,6 +43,7 @@ pub fn blame_file( suspect, cache, &mut resource_cache, + None, file.as_bstr(), range, )?; diff --git a/gix-blame/src/file/function.rs b/gix-blame/src/file/function.rs index 4a99152eb3f..0be7fcbb545 100644 --- a/gix-blame/src/file/function.rs +++ b/gix-blame/src/file/function.rs @@ -1,10 +1,11 @@ -use super::{process_changes, Change, UnblamedHunk}; +use super::{process_changes, update_blame_with_changes, Change, UnblamedHunk}; +use crate::types::BlameCacheObject; use crate::{BlameEntry, Error, Outcome, Statistics}; use gix_diff::blob::intern::TokenSource; use gix_diff::tree::Visit; use gix_hash::ObjectId; use gix_object::{ - bstr::{BStr, BString}, + bstr::{BStr, BString, ByteSlice}, FindExt, }; use gix_traverse::commit::find as find_commit; @@ -66,6 +67,7 @@ pub fn file( suspect: ObjectId, cache: Option, resource_cache: &mut gix_diff::blob::Platform, + blame_cache: Option, file_path: &BStr, range: Option>, ) -> Result { @@ -87,17 +89,56 @@ pub fn file( } let range_in_blamed_file = one_based_inclusive_to_zero_based_exclusive_range(range, num_lines_in_blamed)?; - let mut hunks_to_blame = vec![UnblamedHunk { - range_in_blamed_file: range_in_blamed_file.clone(), - suspects: [(suspect, range_in_blamed_file)].into(), - }]; + + let (blame_entries, mut hunks_to_blame) = match blame_cache { + Some(blame_cache) => { + // If there is a cache, we first get the diff between the current commit and the commit + // we passed as the cache. + let old_file_id = file_id(&blame_cache.cache_id, &mut buf, &mut buf2)?; + let changes = blob_changes( + &odb, + resource_cache, + blamed_file_entry_id, + old_file_id, + file_path.as_bstr(), + &mut stats, + )?; + + // If there are no changes, we can return the cache as is immediately. + if changes.iter().all(|change| matches!(change, Change::Unchanged(_))) { + return Ok(Outcome { + entries: blame_cache.entries.clone(), + blob: blamed_file_blob, + statistics: stats, + }); + } + // Otherwise, we update the cache with the new changes. + let (blame_entries, hunks_to_blame) = update_blame_with_changes(blame_cache.entries, changes, suspect); + // If there are no more hunks to blame, we can return the result immediately. + if hunks_to_blame.is_empty() { + return Ok(Outcome { + entries: blame_entries, + blob: blamed_file_blob, + statistics: stats, + }); + } + (blame_entries, hunks_to_blame) + } + None => { + let hunks_to_blame = vec![UnblamedHunk { + range_in_blamed_file: range_in_blamed_file.clone(), + suspects: [(suspect, range_in_blamed_file)].into(), + }]; + (Vec::new(), hunks_to_blame) + } + }; let (mut buf, mut buf2) = (Vec::new(), Vec::new()); let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?; let mut queue: gix_revwalk::PriorityQueue = gix_revwalk::PriorityQueue::new(); queue.insert(commit_time(commit)?, suspect); - let mut out = Vec::new(); + let mut out = blame_entries; let mut diff_state = gix_diff::tree::State::default(); let mut previous_entry: Option<(ObjectId, ObjectId)> = None; 'outer: while let Some(suspect) = queue.pop_value() { diff --git a/gix-blame/src/lib.rs b/gix-blame/src/lib.rs index 489434b5b3d..e0bb3671cba 100644 --- a/gix-blame/src/lib.rs +++ b/gix-blame/src/lib.rs @@ -17,7 +17,7 @@ mod error; pub use error::Error; mod types; -pub use types::{BlameEntry, Outcome, Statistics}; +pub use types::{BlameCacheObject, BlameEntry, Outcome, Statistics}; mod file; pub use file::function::file; diff --git a/gix-blame/src/types.rs b/gix-blame/src/types.rs index 5a0b76baf35..ae9f3de9c2e 100644 --- a/gix-blame/src/types.rs +++ b/gix-blame/src/types.rs @@ -117,6 +117,15 @@ impl SubAssign for Offset { } } +#[derive(Debug, PartialEq)] +/// A cache of blame entries that can be used to speed up subsequent blames. +pub struct BlameCacheObject { + /// The entries of the cache. + pub entries: Vec, + /// The commit that was blamed to produce these entries. + pub cache_id: ObjectId, +} + /// A mapping of a section of the *Blamed File* to the section in a *Source File* that introduced it. /// /// Both ranges are of the same size, but may use different [starting points](Range::start). Naturally, diff --git a/gix-blame/tests/blame.rs b/gix-blame/tests/blame.rs index 926118ef432..7329f5a6e71 100644 --- a/gix-blame/tests/blame.rs +++ b/gix-blame/tests/blame.rs @@ -190,6 +190,7 @@ macro_rules! mktest { suspect, None, &mut resource_cache, + None, format!("{}.txt", $case).as_str().into(), None, )? @@ -257,6 +258,7 @@ fn diff_disparity() { suspect, None, &mut resource_cache, + None, format!("{case}.txt").as_str().into(), None, ) @@ -285,6 +287,7 @@ fn line_range() { suspect, None, &mut resource_cache, + None, "simple.txt".into(), Some(1..2), )