Skip to content

Commit 667e626

Browse files
committed
Don't panic when suspect isn't known when converting unblamed to blame-entry
This can apparently happen, and now we handle this case and keep looking for the remaining blame entries. Also, exit early when no work is left to be done.
1 parent 3ac8be1 commit 667e626

File tree

3 files changed

+45
-27
lines changed

3 files changed

+45
-27
lines changed

gix-blame/src/file/function.rs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ where
8383
.count()
8484
};
8585

86+
// Binary or otherwise empty?
87+
if num_lines_in_blamed == 0 {
88+
return Ok(Outcome::default());
89+
}
90+
8691
let mut hunks_to_blame = vec![{
8792
let range_in_blamed_file = 0..num_lines_in_blamed as u32;
8893
UnblamedHunk {
@@ -94,6 +99,9 @@ where
9499
let mut out = Vec::new();
95100
let mut diff_state = gix_diff::tree::State::default();
96101
'outer: while let Some(item) = traverse.next() {
102+
if hunks_to_blame.is_empty() {
103+
break;
104+
}
97105
let commit = item.map_err(|err| Error::Traverse(err.into()))?;
98106
let suspect = commit.id;
99107
stats.commits_traversed += 1;
@@ -106,12 +114,13 @@ where
106114
// remaining lines to it, even though we don’t explicitly check whether that is true
107115
// here. We could perhaps use diff-tree-to-tree to compare `suspect`
108116
// against an empty tree to validate this assumption.
109-
unblamed_to_out(&mut hunks_to_blame, &mut out, suspect);
110-
break;
111-
} else {
112-
// There is more, keep looking.
113-
continue;
117+
if unblamed_to_out_is_done(&mut hunks_to_blame, &mut out, suspect) {
118+
break 'outer;
119+
}
114120
}
121+
122+
// There is more, keep looking.
123+
continue;
115124
}
116125

117126
let Some(entry) = find_path_entry_in_commit(&odb, &suspect, file_path, &mut buf, &mut buf2, &mut stats)? else {
@@ -162,8 +171,9 @@ where
162171
// implies that the file comes from a different parent, compared to which
163172
// it was modified, not added.
164173
} else {
165-
unblamed_to_out(&mut hunks_to_blame, &mut out, suspect);
166-
break;
174+
if unblamed_to_out_is_done(&mut hunks_to_blame, &mut out, suspect) {
175+
break 'outer;
176+
}
167177
}
168178
}
169179
gix_diff::tree::recorder::Change::Deletion { .. } => {
@@ -209,13 +219,22 @@ fn pass_blame_from_to(from: ObjectId, to: ObjectId, hunks_to_blame: &mut Vec<Unb
209219
}
210220

211221
/// Convert each of the unblamed hunk in `hunks_to_blame` into a [`BlameEntry`], consuming them in the process.
212-
/// `suspect` is expected to be present in the suspect-map in each [`UnblamedHunk`].
213-
fn unblamed_to_out(hunks_to_blame: &mut Vec<UnblamedHunk>, out: &mut Vec<BlameEntry>, suspect: ObjectId) {
214-
out.extend(
215-
hunks_to_blame
216-
.drain(..)
217-
.map(|hunk| BlameEntry::from_unblamed_hunk(hunk, suspect)),
218-
);
222+
///
223+
/// Return `true` if we are done because `hunks_to_blame` is empty.
224+
fn unblamed_to_out_is_done(
225+
hunks_to_blame: &mut Vec<UnblamedHunk>,
226+
out: &mut Vec<BlameEntry>,
227+
suspect: ObjectId,
228+
) -> bool {
229+
let mut without_suspect = Vec::new();
230+
out.extend(hunks_to_blame.drain(..).filter_map(|hunk| {
231+
BlameEntry::from_unblamed_hunk(&hunk, suspect).or_else(|| {
232+
without_suspect.push(hunk);
233+
None
234+
})
235+
}));
236+
*hunks_to_blame = without_suspect;
237+
hunks_to_blame.is_empty()
219238
}
220239

221240
/// This function merges adjacent blame entries. It merges entries that are adjacent both in the

gix-blame/src/file/mod.rs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,13 @@ fn process_change(
253253
}
254254
}
255255
(Some(hunk), Some(Change::Deleted(line_number_in_destination, number_of_lines_deleted))) => {
256-
let range_in_suspect = hunk
257-
.suspects
258-
.get(&suspect)
259-
.expect("Internal and we know suspect is present");
256+
let Some(range_in_suspect) = hunk.suspects.get(&suspect) else {
257+
new_hunks_to_blame.push(hunk);
258+
return (
259+
None,
260+
Some(Change::Deleted(line_number_in_destination, number_of_lines_deleted)),
261+
);
262+
};
260263

261264
if line_number_in_destination < range_in_suspect.start {
262265
// <---> (hunk)
@@ -431,7 +434,6 @@ impl UnblamedHunk {
431434
}
432435

433436
fn remove_blame(&mut self, suspect: ObjectId) {
434-
// TODO: figure out why it can try to remove suspects that don't exist.
435437
self.suspects.remove(&suspect);
436438
}
437439
}
@@ -468,18 +470,15 @@ impl BlameEntry {
468470
}
469471

470472
/// Create an offset from a portion of the *Blamed File*.
471-
fn from_unblamed_hunk(mut unblamed_hunk: UnblamedHunk, commit_id: ObjectId) -> Self {
472-
let range_in_source_file = unblamed_hunk
473-
.suspects
474-
.remove(&commit_id)
475-
.expect("Private and only called when we now `commit_id` is in the suspect list");
473+
fn from_unblamed_hunk(unblamed_hunk: &UnblamedHunk, commit_id: ObjectId) -> Option<Self> {
474+
let range_in_source_file = unblamed_hunk.suspects.get(&commit_id)?;
476475

477-
Self {
476+
Some(Self {
478477
start_in_blamed_file: unblamed_hunk.range_in_blamed_file.start,
479478
start_in_source_file: range_in_source_file.start,
480479
len: force_non_zero(range_in_source_file.len() as u32),
481480
commit_id,
482-
}
481+
})
483482
}
484483
}
485484

gix-blame/src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::{
88
};
99

1010
/// The outcome of [`file()`](crate::file()).
11-
#[derive(Debug, Clone)]
11+
#[derive(Debug, Default, Clone)]
1212
pub struct Outcome {
1313
/// One entry in sequential order, to associate a hunk in the blamed file with the source commit (and its lines)
1414
/// that introduced it.

0 commit comments

Comments
 (0)