Skip to content

Commit de13b16

Browse files
authored
Merge pull request #1973 from holodorum/feature/blame-range-support
Support for multiple blame ranges
2 parents d2f2333 + d4461e7 commit de13b16

File tree

9 files changed

+261
-76
lines changed

9 files changed

+261
-76
lines changed

gix-blame/Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ authors = ["Christoph Rüßler <[email protected]>", "Sebastian Thi
1010
edition = "2021"
1111
rust-version = "1.70"
1212

13-
[lib]
14-
doctest = false
15-
1613
[dependencies]
1714
gix-commitgraph = { version = "^0.28.0", path = "../gix-commitgraph" }
1815
gix-revwalk = { version = "^0.20.1", path = "../gix-revwalk" }

gix-blame/src/file/function.rs

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,15 @@ pub fn file(
9494
return Ok(Outcome::default());
9595
}
9696

97-
let range_in_blamed_file = one_based_inclusive_to_zero_based_exclusive_range(options.range, num_lines_in_blamed)?;
98-
let mut hunks_to_blame = vec![UnblamedHunk {
99-
range_in_blamed_file: range_in_blamed_file.clone(),
100-
suspects: [(suspect, range_in_blamed_file)].into(),
101-
}];
97+
let ranges = options.range.to_zero_based_exclusive(num_lines_in_blamed)?;
98+
let mut hunks_to_blame = Vec::with_capacity(ranges.len());
99+
100+
for range in ranges {
101+
hunks_to_blame.push(UnblamedHunk {
102+
range_in_blamed_file: range.clone(),
103+
suspects: [(suspect, range)].into(),
104+
});
105+
}
102106

103107
let (mut buf, mut buf2) = (Vec::new(), Vec::new());
104108
let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?;
@@ -342,25 +346,6 @@ pub fn file(
342346
})
343347
}
344348

345-
/// This function assumes that `range` has 1-based inclusive line numbers and converts it to the
346-
/// format internally used: 0-based line numbers stored in ranges that are exclusive at the
347-
/// end.
348-
fn one_based_inclusive_to_zero_based_exclusive_range(
349-
range: Option<Range<u32>>,
350-
max_lines: u32,
351-
) -> Result<Range<u32>, Error> {
352-
let Some(range) = range else { return Ok(0..max_lines) };
353-
if range.start == 0 {
354-
return Err(Error::InvalidLineRange);
355-
}
356-
let start = range.start - 1;
357-
let end = range.end;
358-
if start >= max_lines || end > max_lines || start == end {
359-
return Err(Error::InvalidLineRange);
360-
}
361-
Ok(start..end)
362-
}
363-
364349
/// Pass ownership of each unblamed hunk of `from` to `to`.
365350
///
366351
/// This happens when `from` didn't actually change anything in the blamed file.

gix-blame/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
mod error;
1818
pub use error::Error;
1919
mod types;
20-
pub use types::{BlameEntry, Options, Outcome, Statistics};
20+
pub use types::{BlameEntry, BlameRanges, Options, Outcome, Statistics};
2121

2222
mod file;
2323
pub use file::function::file;

gix-blame/src/types.rs

Lines changed: 135 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,150 @@
1+
use gix_hash::ObjectId;
2+
use gix_object::bstr::BString;
3+
use smallvec::SmallVec;
4+
use std::ops::RangeInclusive;
15
use std::{
26
num::NonZeroU32,
37
ops::{AddAssign, Range, SubAssign},
48
};
59

6-
use gix_hash::ObjectId;
7-
use gix_object::bstr::BString;
8-
use smallvec::SmallVec;
9-
1010
use crate::file::function::tokens_for_diffing;
11+
use crate::Error;
12+
13+
/// A type to represent one or more line ranges to blame in a file.
14+
///
15+
/// It handles the conversion between git's 1-based inclusive ranges and the internal
16+
/// 0-based exclusive ranges used by the blame algorithm.
17+
///
18+
/// # Examples
19+
///
20+
/// ```rust
21+
/// use gix_blame::BlameRanges;
22+
///
23+
/// // Blame lines 20 through 40 (inclusive)
24+
/// let range = BlameRanges::from_range(20..=40);
25+
///
26+
/// // Blame multiple ranges
27+
/// let mut ranges = BlameRanges::new();
28+
/// ranges.add_range(1..=4); // Lines 1-4
29+
/// ranges.add_range(10..=14); // Lines 10-14
30+
/// ```
31+
///
32+
/// # Line Number Representation
33+
///
34+
/// This type uses 1-based inclusive ranges to mirror `git`'s behaviour:
35+
/// - A range of `20..=40` represents 21 lines, spanning from line 20 up to and including line 40
36+
/// - This will be converted to `19..40` internally as the algorithm uses 0-based ranges that are exclusive at the end
37+
///
38+
/// # Empty Ranges
39+
///
40+
/// An empty `BlameRanges` (created via `BlameRanges::new()` or `BlameRanges::default()`) means
41+
/// to blame the entire file, similar to running `git blame` without line number arguments.
42+
#[derive(Debug, Clone, Default)]
43+
pub struct BlameRanges {
44+
/// The ranges to blame, stored as 1-based inclusive ranges
45+
/// An empty Vec means blame the entire file
46+
ranges: Vec<RangeInclusive<u32>>,
47+
}
48+
49+
/// Lifecycle
50+
impl BlameRanges {
51+
/// Create a new empty BlameRanges instance.
52+
///
53+
/// An empty instance means to blame the entire file.
54+
pub fn new() -> Self {
55+
Self::default()
56+
}
57+
58+
/// Create from a single range.
59+
///
60+
/// The range is 1-based, similar to git's line number format.
61+
pub fn from_range(range: RangeInclusive<u32>) -> Self {
62+
Self { ranges: vec![range] }
63+
}
64+
65+
/// Create from multiple ranges.
66+
///
67+
/// All ranges are 1-based.
68+
/// Overlapping or adjacent ranges will be merged.
69+
pub fn from_ranges(ranges: Vec<RangeInclusive<u32>>) -> Self {
70+
let mut result = Self::new();
71+
for range in ranges {
72+
result.merge_range(range);
73+
}
74+
result
75+
}
76+
}
77+
78+
impl BlameRanges {
79+
/// Add a single range to blame.
80+
///
81+
/// The range should be 1-based inclusive.
82+
/// If the new range overlaps with or is adjacent to an existing range,
83+
/// they will be merged into a single range.
84+
pub fn add_range(&mut self, new_range: RangeInclusive<u32>) {
85+
self.merge_range(new_range);
86+
}
87+
88+
/// Attempts to merge the new range with any existing ranges.
89+
/// If no merge is possible, add it as a new range.
90+
fn merge_range(&mut self, new_range: RangeInclusive<u32>) {
91+
// Check if this range can be merged with any existing range
92+
for range in &mut self.ranges {
93+
// Check if ranges overlap or are adjacent
94+
if new_range.start() <= range.end() && range.start() <= new_range.end() {
95+
*range = *range.start().min(new_range.start())..=*range.end().max(new_range.end());
96+
return;
97+
}
98+
}
99+
// If no overlap found, add it as a new range
100+
self.ranges.push(new_range);
101+
}
102+
103+
/// Convert the 1-based inclusive ranges to 0-based exclusive ranges.
104+
///
105+
/// This is used internally by the blame algorithm to convert from git's line number format
106+
/// to the internal format used for processing.
107+
///
108+
/// # Errors
109+
///
110+
/// Returns `Error::InvalidLineRange` if:
111+
/// - Any range starts at 0 (must be 1-based)
112+
/// - Any range extends beyond the file's length
113+
/// - Any range has the same start and end
114+
pub fn to_zero_based_exclusive(&self, max_lines: u32) -> Result<Vec<Range<u32>>, Error> {
115+
if self.ranges.is_empty() {
116+
let range = 0..max_lines;
117+
return Ok(vec![range]);
118+
}
119+
120+
let mut result = Vec::with_capacity(self.ranges.len());
121+
for range in &self.ranges {
122+
if *range.start() == 0 {
123+
return Err(Error::InvalidLineRange);
124+
}
125+
let start = range.start() - 1;
126+
let end = *range.end();
127+
if start >= max_lines || end > max_lines || start == end {
128+
return Err(Error::InvalidLineRange);
129+
}
130+
result.push(start..end);
131+
}
132+
Ok(result)
133+
}
134+
135+
/// Returns true if no specific ranges are set (meaning blame entire file)
136+
pub fn is_empty(&self) -> bool {
137+
self.ranges.is_empty()
138+
}
139+
}
11140

12141
/// Options to be passed to [`file()`](crate::file()).
13142
#[derive(Default, Debug, Clone)]
14143
pub struct Options {
15144
/// The algorithm to use for diffing.
16145
pub diff_algorithm: gix_diff::blob::Algorithm,
17-
/// A 1-based inclusive range, in order to mirror `git`’s behaviour. `Some(20..40)` represents
18-
/// 21 lines, spanning from line 20 up to and including line 40. This will be converted to
19-
/// `19..40` internally as the algorithm uses 0-based ranges that are exclusive at the end.
20-
pub range: Option<std::ops::Range<u32>>,
146+
/// The ranges to blame in the file.
147+
pub range: BlameRanges,
21148
/// Don't consider commits before the given date.
22149
pub since: Option<gix_date::Time>,
23150
}

0 commit comments

Comments
 (0)