Skip to content

Commit 240a79b

Browse files
committed
Correctly pass blame for some merge commits
- Merge commits where a file was added in one chain of ancestors - Merge commits where a conflict was resolved
1 parent 911f12e commit 240a79b

File tree

3 files changed

+61
-35
lines changed

3 files changed

+61
-35
lines changed

gix-blame/src/lib.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,13 @@ pub fn process_change(
407407
}
408408
}
409409
(Some(hunk), Some(Change::Added(added, number_of_lines_deleted))) => {
410-
let range_in_suspect = hunk.suspects.get(&suspect).expect("TODO").clone();
410+
let Some(range_in_suspect) = hunk.suspects.get(&suspect) else {
411+
new_hunks_to_blame.push(hunk);
412+
413+
return (None, Some(Change::Added(added, number_of_lines_deleted)));
414+
};
415+
416+
let range_in_suspect = range_in_suspect.clone();
411417

412418
match (
413419
range_in_suspect.contains(&added.start),
@@ -853,10 +859,37 @@ pub fn blame_file<E>(
853859
}
854860
} else {
855861
for parent_id in parent_ids {
856-
// Pass blame to parent.
857-
hunks_to_blame
858-
.iter_mut()
859-
.for_each(|unblamed_hunk| unblamed_hunk.clone_blame(suspect, parent_id));
862+
let changes_for_file_path = get_changes_for_file_path(&odb, file_path, item.id, parent_id);
863+
864+
let [ref modification]: [gix_diff::tree::recorder::Change] = changes_for_file_path[..] else {
865+
// None of the changes affected the file we’re currently blaming. Pass blame
866+
// to parent.
867+
hunks_to_blame
868+
.iter_mut()
869+
.for_each(|unblamed_hunk| unblamed_hunk.clone_blame(suspect, parent_id));
870+
871+
continue;
872+
};
873+
874+
match modification {
875+
gix_diff::tree::recorder::Change::Addition { .. } => {
876+
// Do nothing under the assumption that this always (or almost always)
877+
// implies that the file comes from a different parent, compared to which
878+
// it was modified, not added.
879+
//
880+
// TODO: I still have to figure out whether this is correct in all cases.
881+
}
882+
gix_diff::tree::recorder::Change::Deletion { .. } => todo!(),
883+
gix_diff::tree::recorder::Change::Modification { previous_oid, oid, .. } => {
884+
let changes = get_changes(&odb, resource_cache, *oid, *previous_oid, file_path);
885+
886+
hunks_to_blame = process_changes(&mut out, &hunks_to_blame, &changes, suspect);
887+
888+
hunks_to_blame
889+
.iter_mut()
890+
.for_each(|unblamed_hunk| unblamed_hunk.pass_blame(suspect, parent_id));
891+
}
892+
}
860893
}
861894

862895
hunks_to_blame

gix-blame/tests/blame.rs

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -235,36 +235,13 @@ mktest!(added_line_before_changed_line, "added-line-before-changed-line", 3);
235235
mktest!(same_line_changed_twice, "same-line-changed-twice", 2);
236236
mktest!(coalesce_adjacent_hunks, "coalesce-adjacent-hunks", 1);
237237

238-
#[test]
239-
#[ignore = "TODO: as of October 2024, passing blames to more than one parent is not fully implemented yet"]
240-
fn merge_commits() {
241-
for case in ["resolved-conflict"] {
242-
let Fixture {
243-
worktree_path,
244-
odb,
245-
mut resource_cache,
246-
suspect,
247-
commits,
248-
} = Fixture::new().unwrap();
249-
250-
let lines_blamed = blame_file(
251-
&odb,
252-
commits,
253-
&mut resource_cache,
254-
suspect,
255-
worktree_path,
256-
format!("{case}.txt").as_str().into(),
257-
)
258-
.unwrap();
259-
260-
assert_ne!(lines_blamed.len(), 0);
261-
262-
let git_dir = fixture_path().join(".git");
263-
let baseline = Baseline::collect(git_dir.join(format!("{case}.baseline"))).unwrap();
264-
265-
assert_eq!(lines_blamed, baseline, "{case}");
266-
}
267-
}
238+
mktest!(resolved_conflict, "resolved-conflict", 2);
239+
mktest!(file_in_one_chain_of_ancestors, "file-in-one-chain-of-ancestors", 1);
240+
mktest!(
241+
different_file_in_another_chain_of_ancestors,
242+
"different-file-in-another-chain-of-ancestors",
243+
1
244+
);
268245

269246
#[test]
270247
#[ignore = "TBD: figure out what the problem is"]

gix-blame/tests/fixtures/make_blame_repo.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,20 @@ echo -e "line 1 conflict resolved\nline 2\n line 3" > resolved-conflict.txt
154154
git add resolved-conflict.txt
155155
git commit -q -m c9
156156

157+
echo -e "line 1\nline 2\n line 3" > file-in-one-chain-of-ancestors.txt
158+
git add file-in-one-chain-of-ancestors.txt
159+
git commit -q -m c10
160+
161+
git checkout -b different-branch-that-does-not-contain-file
162+
git reset --hard HEAD~1
163+
164+
echo -e "line 4\nline 5\n line 6" > different-file-in-another-chain-of-ancestors.txt
165+
git add different-file-in-another-chain-of-ancestors.txt
166+
git commit -q -m c11
167+
168+
git checkout main
169+
git merge different-branch-that-does-not-contain-file || true
170+
157171
git blame --porcelain simple.txt > .git/simple.baseline
158172
git blame --porcelain multiline-hunks.txt > .git/multiline-hunks.baseline
159173
git blame --porcelain deleted-lines.txt > .git/deleted-lines.baseline
@@ -168,6 +182,8 @@ git blame --porcelain same-line-changed-twice.txt > .git/same-line-changed-twice
168182
git blame --porcelain coalesce-adjacent-hunks.txt > .git/coalesce-adjacent-hunks.baseline
169183

170184
git blame --porcelain resolved-conflict.txt > .git/resolved-conflict.baseline
185+
git blame --porcelain file-in-one-chain-of-ancestors.txt > .git/file-in-one-chain-of-ancestors.baseline
186+
git blame --porcelain different-file-in-another-chain-of-ancestors.txt > .git/different-file-in-another-chain-of-ancestors.baseline
171187

172188
git blame --porcelain empty-lines-histogram.txt > .git/empty-lines-histogram.baseline
173189

0 commit comments

Comments
 (0)