Skip to content

Commit b756839

Browse files
holodorumByron
authored andcommitted
Diff forward algorithm
Add an algorithm that takes an existing blame and diff changes and computes the new blame and unblamed hunks.
1 parent 8776a3e commit b756839

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)