Skip to content

Commit aa65934

Browse files
committed
Diff forward algorithm
Add an algorithm that takes an existing blame and diff changes and computes the new blame and unblamed hunks.
1 parent 2efce72 commit aa65934

File tree

3 files changed

+440
-1
lines changed

3 files changed

+440
-1
lines changed

gix-blame/src/file/mod.rs

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::ops::Range;
55

66
use gix_hash::ObjectId;
77

8-
use crate::types::{BlameEntry, Either, LineRange};
8+
use crate::types::{BlameEntry, BlameLines, ChangeLines, Either, LineRange};
99
use crate::types::{Change, Offset, UnblamedHunk};
1010

1111
pub(super) mod function;
@@ -357,6 +357,147 @@ fn process_changes(
357357
new_hunks_to_blame
358358
}
359359

360+
/// Consume `cached_blames` and `changes`. With the changes we update the cached blames.
361+
/// This function returns the updated blames and the new hunks to blame.
362+
fn update_blame_with_changes(
363+
cached_blames: Vec<BlameEntry>,
364+
changes: Vec<Change>,
365+
head_id: ObjectId,
366+
) -> (Vec<BlameEntry>, Vec<UnblamedHunk>) {
367+
fn blame_fully_contained_by_change(
368+
blame_lines: &BlameLines,
369+
blame: &BlameEntry,
370+
change_lines: &ChangeLines,
371+
change: &Change,
372+
) -> bool {
373+
blame_lines.get_remaining(blame) < change_lines.get_remaining(change)
374+
}
375+
376+
let mut updated_blames = Vec::new();
377+
let mut new_hunks_to_blame = Vec::new();
378+
379+
let mut blame_iter = cached_blames.into_iter().peekable();
380+
381+
// This is a nested loop where we iterate over the changes and the blames.
382+
// We keep track of the assigned lines in the change and the blame.
383+
// For each of the three possible cases (Unchanged, Deleted, AddedOrReplaced) we have different
384+
// rules for how to update the blame.
385+
'change: for change in changes {
386+
let mut change_assigned = ChangeLines::default();
387+
while let Some(blame) = blame_iter.peek_mut() {
388+
let mut blame_assigned = BlameLines::default();
389+
390+
// For each of the three cases we have to check if the blame is fully contained by the change.
391+
// If so we can update the blame with the remaining length of the blame.
392+
// If not we have to update the blame with the remaining length of the change.
393+
match change {
394+
Change::Unchanged(ref range) => {
395+
match blame_fully_contained_by_change(&blame_assigned, blame, &change_assigned, &change) {
396+
true => {
397+
updated_blames.push(BlameEntry {
398+
start_in_blamed_file: range.start + change_assigned.assigned.get_assigned(),
399+
start_in_source_file: blame.start_in_source_file,
400+
len: blame.len,
401+
commit_id: blame.commit_id,
402+
});
403+
404+
change_assigned.assigned.add_assigned(blame.len.get());
405+
blame_assigned.assigned.add_assigned(blame.len.get());
406+
}
407+
false => {
408+
updated_blames.push(BlameEntry {
409+
start_in_blamed_file: range.start + change_assigned.assigned.get_assigned(),
410+
start_in_source_file: blame.start_in_source_file,
411+
len: NonZeroU32::new(change_assigned.get_remaining(&change)).unwrap(),
412+
commit_id: blame.commit_id,
413+
});
414+
415+
blame_assigned
416+
.assigned
417+
.add_assigned(change_assigned.get_remaining(&change));
418+
change_assigned
419+
.assigned
420+
.add_assigned(change_assigned.get_remaining(&change));
421+
}
422+
}
423+
}
424+
Change::Deleted(_start_deletion, _lines_deleted) => {
425+
match blame_fully_contained_by_change(&blame_assigned, blame, &change_assigned, &change) {
426+
true => {
427+
blame_assigned.assigned.add_assigned(blame.len.get());
428+
change_assigned.assigned.add_assigned(blame.len.get());
429+
}
430+
false => {
431+
blame_assigned
432+
.assigned
433+
.add_assigned(change_assigned.get_remaining(&change));
434+
change_assigned
435+
.assigned
436+
.add_assigned(change_assigned.get_remaining(&change));
437+
}
438+
}
439+
}
440+
Change::AddedOrReplaced(ref range, lines_deleted) => {
441+
let new_unblamed_hunk = |range: &Range<u32>, head_id: ObjectId| UnblamedHunk {
442+
range_in_blamed_file: range.clone(),
443+
suspects: [(head_id, range.clone())].into(),
444+
};
445+
match blame_fully_contained_by_change(&blame_assigned, blame, &change_assigned, &change) {
446+
true => {
447+
if lines_deleted == 0 {
448+
new_hunks_to_blame.push(new_unblamed_hunk(range, head_id));
449+
}
450+
451+
change_assigned.assigned.add_assigned(blame.len.get());
452+
blame_assigned.assigned.add_assigned(blame.len.get());
453+
}
454+
false => {
455+
new_hunks_to_blame.push(new_unblamed_hunk(range, head_id));
456+
457+
blame_assigned
458+
.assigned
459+
.add_assigned(change_assigned.get_remaining(&change));
460+
change_assigned
461+
.assigned
462+
.add_assigned(change_assigned.get_remaining(&change));
463+
}
464+
}
465+
}
466+
}
467+
468+
// Check if the blame or the change is fully assigned.
469+
// If the blame is fully assigned we can continue with the next blame.
470+
// If the change is fully assigned we can continue with the next change.
471+
// Since we have a mutable reference to the blame we can update it and reset the assigned blame lines.
472+
// If both are fully assigned we can continue with the next blame and change.
473+
match (
474+
blame_assigned.has_remaining(blame),
475+
change_assigned.has_remaining(&change),
476+
) {
477+
(true, true) => {
478+
// Both have remaining
479+
blame.update_blame(&blame_assigned.assigned);
480+
}
481+
(true, false) => {
482+
// Change is fully assigned
483+
blame.update_blame(&blame_assigned.assigned);
484+
continue 'change;
485+
}
486+
(false, true) => {
487+
// Blame is fully assigned
488+
blame_iter.next();
489+
}
490+
(false, false) => {
491+
// Both are fully assigned
492+
blame_iter.next();
493+
continue 'change;
494+
}
495+
};
496+
}
497+
}
498+
(updated_blames, new_hunks_to_blame)
499+
}
500+
360501
impl UnblamedHunk {
361502
fn shift_by(mut self, suspect: ObjectId, offset: Offset) -> Self {
362503
self.suspects.entry(suspect).and_modify(|e| *e = e.shift_by(offset));

0 commit comments

Comments
 (0)