@@ -21,12 +21,14 @@ use crate::Error;
2121/// use gix_blame::BlameRanges;
2222///
2323/// // Blame lines 20 through 40 (inclusive)
24- /// let range = BlameRanges::from_range (20..=40);
24+ /// let range = BlameRanges::from_one_based_inclusive_range (20..=40);
2525///
2626/// // 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
27+ /// let mut ranges = BlameRanges::from_one_based_inclusive_ranges(vec![
28+ /// 1..=4, // Lines 1-4
29+ /// 10..=14, // Lines 10-14
30+ /// ]
31+ /// );
3032/// ```
3133///
3234/// # Line Number Representation
@@ -36,105 +38,102 @@ use crate::Error;
3638/// - This will be converted to `19..40` internally as the algorithm uses 0-based ranges that are exclusive at the end
3739///
3840/// # 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.
41+ /// You can blame the entire file by calling `BlameRanges::default()`, or by passing an empty vector to `from_one_based_inclusive_ranges`.
4242#[ 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 > > ,
43+ pub enum BlameRanges {
44+ /// Blame the entire file.
45+ #[ default]
46+ WholeFile ,
47+ /// Blame ranges in 0-based exclusive format.
48+ PartialFile ( Vec < Range < u32 > > ) ,
4749}
4850
4951/// Lifecycle
5052impl BlameRanges {
51- /// Create a new empty BlameRanges instance .
53+ /// Create from a single 0-based range .
5254 ///
53- /// An empty instance means to blame the entire file.
54- pub fn new ( ) -> Self {
55- Self :: default ( )
55+ /// Note that the input range is 1-based inclusive, as used by git, and
56+ /// the output is a zero-based `BlameRanges` instance.
57+ pub fn from_one_based_inclusive_range ( range : RangeInclusive < u32 > ) -> Result < Self , Error > {
58+ let zero_based_range = Self :: inclusive_to_zero_based_exclusive ( range) ?;
59+ Ok ( Self :: PartialFile ( vec ! [ zero_based_range] ) )
5660 }
5761
58- /// Create from a single range.
62+ /// Create from multiple 0-based ranges.
63+ ///
64+ /// Note that the input ranges are 1-based inclusive, as used by git, and
65+ /// the output is a zero-based `BlameRanges` instance.
5966 ///
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] }
67+ /// If the input vector is empty, the result will be `WholeFile`.
68+ pub fn from_one_based_inclusive_ranges ( ranges : Vec < RangeInclusive < u32 > > ) -> Result < Self , Error > {
69+ if ranges. is_empty ( ) {
70+ return Ok ( Self :: WholeFile ) ;
71+ }
72+
73+ let zero_based_ranges = ranges
74+ . into_iter ( )
75+ . map ( Self :: inclusive_to_zero_based_exclusive)
76+ . collect :: < Vec < _ > > ( ) ;
77+ let mut result = Self :: PartialFile ( vec ! [ ] ) ;
78+ for range in zero_based_ranges {
79+ result. merge_zero_based_exclusive_range ( range?) ;
80+ }
81+ Ok ( result)
6382 }
6483
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) ;
84+ /// Convert a 1-based inclusive range to a 0-based exclusive range.
85+ fn inclusive_to_zero_based_exclusive ( range : RangeInclusive < u32 > ) -> Result < Range < u32 > , Error > {
86+ if range. start ( ) == & 0 {
87+ return Err ( Error :: InvalidOneBasedLineRange ) ;
7388 }
74- result
89+ let start = range. start ( ) - 1 ;
90+ let end = * range. end ( ) ;
91+ Ok ( start..end)
7592 }
7693}
7794
7895impl BlameRanges {
7996 /// Add a single range to blame.
8097 ///
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- }
98+ /// The new range will be merged with any overlapping existing ranges.
99+ pub fn add_one_based_inclusive_range ( & mut self , new_range : RangeInclusive < u32 > ) -> Result < ( ) , Error > {
100+ let zero_based_range = Self :: inclusive_to_zero_based_exclusive ( new_range) ?;
101+ self . merge_zero_based_exclusive_range ( zero_based_range) ;
87102
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) ;
103+ Ok ( ( ) )
101104 }
102105
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- }
106+ /// Adds a new ranges, merging it with any existing overlapping ranges.
107+ fn merge_zero_based_exclusive_range ( & mut self , new_range : Range < u32 > ) {
108+ match self {
109+ Self :: PartialFile ( ref mut ranges) => {
110+ // Partition ranges into those that don't overlap and those that do.
111+ let ( mut non_overlapping, overlapping) : ( Vec < _ > , Vec < _ > ) = ranges
112+ . drain ( ..)
113+ . partition ( |range| new_range. end < range. start || range. end < new_range. start ) ;
119114
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 ) ;
115+ let merged_range = overlapping. into_iter ( ) . fold ( new_range, |acc, range| {
116+ acc. start . min ( range. start ) ..acc. end . max ( range. end )
117+ } ) ;
118+
119+ non_overlapping. push ( merged_range) ;
120+
121+ * ranges = non_overlapping;
122+ ranges. sort_by ( |a, b| a. start . cmp ( & b. start ) ) ;
129123 }
130- result . push ( start..end ) ;
124+ Self :: WholeFile => * self = Self :: PartialFile ( vec ! [ new_range ] ) ,
131125 }
132- Ok ( result)
133126 }
134127
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 ( )
128+ /// Gets zero-based exclusive ranges.
129+ pub fn to_zero_based_exclusive_ranges ( & self , max_lines : u32 ) -> Vec < Range < u32 > > {
130+ match self {
131+ Self :: WholeFile => {
132+ let full_range = 0 ..max_lines;
133+ vec ! [ full_range]
134+ }
135+ Self :: PartialFile ( ranges) => ranges. clone ( ) ,
136+ }
138137 }
139138}
140139
@@ -380,6 +379,17 @@ pub struct UnblamedHunk {
380379}
381380
382381impl UnblamedHunk {
382+ pub ( crate ) fn new ( from_range_in_blamed_file : Range < u32 > , suspect : ObjectId ) -> Self {
383+ let range_start = from_range_in_blamed_file. start ;
384+ let range_end = from_range_in_blamed_file. end ;
385+
386+ UnblamedHunk {
387+ range_in_blamed_file : range_start..range_end,
388+ suspects : [ ( suspect, range_start..range_end) ] . into ( ) ,
389+ source_file_name : None ,
390+ }
391+ }
392+
383393 pub ( crate ) fn has_suspect ( & self , suspect : & ObjectId ) -> bool {
384394 self . suspects . iter ( ) . any ( |entry| entry. 0 == * suspect)
385395 }
0 commit comments