Skip to content

Commit 59bd978

Browse files
authored
Merge pull request #1747 from cruessler/shortcut-tree-diffing
Cancel tree diffing early when matching path is found
2 parents af704f5 + 9ac36bd commit 59bd978

File tree

2 files changed

+102
-9
lines changed

2 files changed

+102
-9
lines changed

gix-blame/src/file/function.rs

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
use super::{process_changes, Change, UnblamedHunk};
22
use crate::{BlameEntry, Error, Outcome, Statistics};
33
use gix_diff::blob::intern::TokenSource;
4+
use gix_diff::tree::Visit;
45
use gix_hash::ObjectId;
5-
use gix_object::{bstr::BStr, FindExt};
6+
use gix_object::{
7+
bstr::{BStr, BString},
8+
FindExt,
9+
};
610
use std::num::NonZeroU32;
711
use std::ops::Range;
812

@@ -322,15 +326,102 @@ fn tree_diff_at_file_path(
322326
let tree_iter = odb.find_tree_iter(&tree_id, rhs_tree_buf)?;
323327
stats.trees_decoded += 1;
324328

325-
let mut recorder = gix_diff::tree::Recorder::default();
326-
gix_diff::tree(parent_tree_iter, tree_iter, state, &odb, &mut recorder)?;
329+
struct FindChangeToPath {
330+
inner: gix_diff::tree::Recorder,
331+
interesting_path: BString,
332+
change: Option<gix_diff::tree::recorder::Change>,
333+
}
334+
335+
impl FindChangeToPath {
336+
fn new(interesting_path: BString) -> Self {
337+
let inner =
338+
gix_diff::tree::Recorder::default().track_location(Some(gix_diff::tree::recorder::Location::Path));
339+
340+
FindChangeToPath {
341+
inner,
342+
interesting_path,
343+
change: None,
344+
}
345+
}
346+
}
347+
348+
impl Visit for FindChangeToPath {
349+
fn pop_front_tracked_path_and_set_current(&mut self) {
350+
self.inner.pop_front_tracked_path_and_set_current();
351+
}
352+
353+
fn push_back_tracked_path_component(&mut self, component: &BStr) {
354+
self.inner.push_back_tracked_path_component(component);
355+
}
356+
357+
fn push_path_component(&mut self, component: &BStr) {
358+
self.inner.push_path_component(component);
359+
}
360+
361+
fn pop_path_component(&mut self) {
362+
self.inner.pop_path_component();
363+
}
364+
365+
fn visit(&mut self, change: gix_diff::tree::visit::Change) -> gix_diff::tree::visit::Action {
366+
use gix_diff::tree::visit::Action::*;
367+
use gix_diff::tree::visit::Change::*;
368+
369+
if self.inner.path() == self.interesting_path {
370+
self.change = Some(match change {
371+
Deletion {
372+
entry_mode,
373+
oid,
374+
relation,
375+
} => gix_diff::tree::recorder::Change::Deletion {
376+
entry_mode,
377+
oid,
378+
path: self.inner.path_clone(),
379+
relation,
380+
},
381+
Addition {
382+
entry_mode,
383+
oid,
384+
relation,
385+
} => gix_diff::tree::recorder::Change::Addition {
386+
entry_mode,
387+
oid,
388+
path: self.inner.path_clone(),
389+
relation,
390+
},
391+
Modification {
392+
previous_entry_mode,
393+
previous_oid,
394+
entry_mode,
395+
oid,
396+
} => gix_diff::tree::recorder::Change::Modification {
397+
previous_entry_mode,
398+
previous_oid,
399+
entry_mode,
400+
oid,
401+
path: self.inner.path_clone(),
402+
},
403+
});
404+
405+
// When we return `Cancel`, `gix_diff::tree` will convert this `Cancel` into an
406+
// `Err(...)`. Keep this in mind when using `FindChangeToPath`.
407+
Cancel
408+
} else {
409+
Continue
410+
}
411+
}
412+
}
413+
414+
let mut recorder = FindChangeToPath::new(file_path.into());
415+
let result = gix_diff::tree(parent_tree_iter, tree_iter, state, &odb, &mut recorder);
327416
stats.trees_diffed += 1;
328417

329-
Ok(recorder.records.into_iter().find(|change| match change {
330-
gix_diff::tree::recorder::Change::Modification { path, .. } => path == file_path,
331-
gix_diff::tree::recorder::Change::Addition { path, .. } => path == file_path,
332-
gix_diff::tree::recorder::Change::Deletion { path, .. } => path == file_path,
333-
}))
418+
match result {
419+
// `recorder` cancels the traversal by returning `Cancel` when a change to `file_path` is
420+
// found. `gix_diff::tree` converts `Cancel` into `Err(Cancelled)` which is why we match on
421+
// `Err(Cancelled)` in addition to `Ok`.
422+
Ok(_) | Err(gix_diff::tree::Error::Cancelled) => Ok(recorder.change),
423+
Err(error) => Err(Error::DiffTree(error)),
424+
}
334425
}
335426

336427
fn blob_changes(

gix-blame/src/types.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ pub struct Statistics {
2828
pub commits_to_tree: usize,
2929
/// The amount of trees that were decoded to find the entry of the file to blame.
3030
pub trees_decoded: usize,
31-
/// The amount of fully-fledged tree-diffs to see if the filepath was added, deleted or modified.
31+
/// The amount of tree-diffs to see if the filepath was added, deleted or modified. These diffs
32+
/// are likely partial as they are cancelled as soon as a change to the blamed file is
33+
/// detected.
3234
pub trees_diffed: usize,
3335
/// The amount of blobs there were compared to each other to learn what changed between commits.
3436
/// Note that in order to diff a blob, one needs to load both versions from the database.

0 commit comments

Comments
 (0)