Skip to content

Commit 663b41e

Browse files
authored
Merge pull request #2204 from cruessler/improve-blame-ranges
Improve blame ranges
2 parents 6e89afa + 9d5b033 commit 663b41e

File tree

8 files changed

+254
-110
lines changed

8 files changed

+254
-110
lines changed

gix-blame/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub enum Error {
3030
#[error(transparent)]
3131
DiffTreeWithRewrites(#[from] gix_diff::tree_with_rewrites::Error),
3232
#[error("Invalid line range was given, line range is expected to be a 1-based inclusive range in the format '<start>,<end>'")]
33-
InvalidLineRange,
33+
InvalidOneBasedLineRange,
3434
#[error("Failure to decode commit during traversal")]
3535
DecodeCommit(#[from] gix_object::decode::Error),
3636
#[error("Failed to get parent from commitgraph during traversal")]

gix-blame/src/file/function.rs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ use crate::{types::BlamePathEntry, BlameEntry, Error, Options, Outcome, Statisti
2424
/// - The first commit to be responsible for parts of `file_path`.
2525
/// * `cache`
2626
/// - Optionally, the commitgraph cache.
27-
/// * `file_path`
28-
/// - A *slash-separated* worktree-relative path to the file to blame.
29-
/// * `range`
30-
/// - A 1-based inclusive range, in order to mirror `git`’s behaviour. `Some(20..40)` represents
31-
/// 21 lines, spanning from line 20 up to and including line 40. This will be converted to
32-
/// `19..40` internally as the algorithm uses 0-based ranges that are exclusive at the end.
3327
/// * `resource_cache`
3428
/// - Used for diffing trees.
29+
/// * `file_path`
30+
/// - A *slash-separated* worktree-relative path to the file to blame.
31+
/// * `options`
32+
/// - An instance of [`Options`].
3533
///
3634
/// ## The algorithm
3735
///
@@ -95,16 +93,11 @@ pub fn file(
9593
return Ok(Outcome::default());
9694
}
9795

98-
let ranges = options.range.to_zero_based_exclusive(num_lines_in_blamed)?;
99-
let mut hunks_to_blame = Vec::with_capacity(ranges.len());
100-
101-
for range in ranges {
102-
hunks_to_blame.push(UnblamedHunk {
103-
range_in_blamed_file: range.clone(),
104-
suspects: [(suspect, range)].into(),
105-
source_file_name: None,
106-
});
107-
}
96+
let ranges_to_blame = options.ranges.to_zero_based_exclusive_ranges(num_lines_in_blamed);
97+
let mut hunks_to_blame = ranges_to_blame
98+
.into_iter()
99+
.map(|range| UnblamedHunk::new(range, suspect))
100+
.collect::<Vec<_>>();
108101

109102
let (mut buf, mut buf2) = (Vec::new(), Vec::new());
110103
let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?;

gix-blame/src/file/tests.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,3 +985,129 @@ mod process_changes {
985985
);
986986
}
987987
}
988+
989+
mod blame_ranges {
990+
use crate::{BlameRanges, Error};
991+
992+
#[test]
993+
fn create_with_invalid_range() {
994+
let ranges = BlameRanges::from_one_based_inclusive_range(0..=10);
995+
996+
assert!(matches!(ranges, Err(Error::InvalidOneBasedLineRange)));
997+
}
998+
999+
#[test]
1000+
fn create_from_single_range() {
1001+
let ranges = BlameRanges::from_one_based_inclusive_range(20..=40).unwrap();
1002+
1003+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![19..40]);
1004+
}
1005+
1006+
#[test]
1007+
fn create_from_multiple_ranges() {
1008+
let ranges = BlameRanges::from_one_based_inclusive_ranges(vec![1..=4, 10..=14]).unwrap();
1009+
1010+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..4, 9..14]);
1011+
}
1012+
1013+
#[test]
1014+
fn create_with_empty_ranges() {
1015+
let ranges = BlameRanges::from_one_based_inclusive_ranges(vec![]).unwrap();
1016+
1017+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..100]);
1018+
}
1019+
1020+
#[test]
1021+
fn add_range_merges_overlapping() {
1022+
let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=5).unwrap();
1023+
ranges.add_one_based_inclusive_range(3..=7).unwrap();
1024+
1025+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..7]);
1026+
}
1027+
1028+
#[test]
1029+
fn add_range_merges_overlapping_both() {
1030+
let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=3).unwrap();
1031+
ranges.add_one_based_inclusive_range(5..=7).unwrap();
1032+
ranges.add_one_based_inclusive_range(2..=6).unwrap();
1033+
1034+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..7]);
1035+
}
1036+
1037+
#[test]
1038+
fn add_range_non_sorted() {
1039+
let mut ranges = BlameRanges::from_one_based_inclusive_range(5..=7).unwrap();
1040+
ranges.add_one_based_inclusive_range(1..=3).unwrap();
1041+
1042+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..3, 4..7]);
1043+
}
1044+
1045+
#[test]
1046+
fn add_range_merges_adjacent() {
1047+
let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=5).unwrap();
1048+
ranges.add_one_based_inclusive_range(6..=10).unwrap();
1049+
1050+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..10]);
1051+
}
1052+
1053+
#[test]
1054+
fn non_sorted_ranges() {
1055+
let ranges = BlameRanges::from_one_based_inclusive_ranges(vec![10..=15, 1..=5]).unwrap();
1056+
1057+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..5, 9..15]);
1058+
}
1059+
1060+
#[test]
1061+
fn convert_to_zero_based_exclusive() {
1062+
let ranges = BlameRanges::from_one_based_inclusive_ranges(vec![1..=5, 10..=15]).unwrap();
1063+
1064+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..5, 9..15]);
1065+
}
1066+
1067+
#[test]
1068+
fn convert_full_file_to_zero_based() {
1069+
let ranges = BlameRanges::WholeFile;
1070+
1071+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..100]);
1072+
}
1073+
1074+
#[test]
1075+
fn adding_a_range_turns_whole_file_into_partial_file() {
1076+
let mut ranges = BlameRanges::default();
1077+
1078+
ranges.add_one_based_inclusive_range(1..=10).unwrap();
1079+
1080+
assert_eq!(ranges.to_zero_based_exclusive_ranges(100), vec![0..10]);
1081+
}
1082+
1083+
#[test]
1084+
fn to_zero_based_exclusive_ignores_range_past_max_lines() {
1085+
let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=5).unwrap();
1086+
ranges.add_one_based_inclusive_range(16..=20).unwrap();
1087+
1088+
assert_eq!(ranges.to_zero_based_exclusive_ranges(7), vec![0..5]);
1089+
}
1090+
1091+
#[test]
1092+
fn to_zero_based_exclusive_range_doesnt_exceed_max_lines() {
1093+
let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=5).unwrap();
1094+
ranges.add_one_based_inclusive_range(6..=10).unwrap();
1095+
1096+
assert_eq!(ranges.to_zero_based_exclusive_ranges(7), vec![0..7]);
1097+
}
1098+
1099+
#[test]
1100+
fn to_zero_based_exclusive_merged_ranges_dont_exceed_max_lines() {
1101+
let mut ranges = BlameRanges::from_one_based_inclusive_range(1..=4).unwrap();
1102+
ranges.add_one_based_inclusive_range(6..=10).unwrap();
1103+
1104+
assert_eq!(ranges.to_zero_based_exclusive_ranges(7), vec![0..4, 5..7]);
1105+
}
1106+
1107+
#[test]
1108+
fn default_is_full_file() {
1109+
let ranges = BlameRanges::default();
1110+
1111+
assert!(matches!(ranges, BlameRanges::WholeFile));
1112+
}
1113+
}

0 commit comments

Comments
 (0)