@@ -21,12 +21,14 @@ use crate::Error;
21
21
/// use gix_blame::BlameRanges;
22
22
///
23
23
/// // 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);
25
25
///
26
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
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
+ /// );
30
32
/// ```
31
33
///
32
34
/// # Line Number Representation
@@ -36,43 +38,61 @@ use crate::Error;
36
38
/// - This will be converted to `19..40` internally as the algorithm uses 0-based ranges that are exclusive at the end
37
39
///
38
40
/// # 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`.
42
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 > > ,
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 > > ) ,
47
49
}
48
50
49
51
/// Lifecycle
50
52
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
53
/// Create from a single range.
59
54
///
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] }
55
+ /// Note that the input range is 1-based inclusive, as used by git, and
56
+ /// the output is zero-based `BlameRanges` instance.
57
+ ///
58
+ /// @param range: A 1-based inclusive range.
59
+ /// @return: A `BlameRanges` instance representing the range.
60
+ pub fn from_one_based_inclusive_range ( range : RangeInclusive < u32 > ) -> Self {
61
+ let zero_based_range = Self :: inclusive_to_zero_based_exclusive ( range) ;
62
+ Self :: PartialFile ( vec ! [ zero_based_range] )
63
63
}
64
64
65
65
/// Create from multiple ranges.
66
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) ;
67
+ /// Note that the input ranges are 1-based inclusive, as used by git, and
68
+ /// the output is zero-based `BlameRanges` instance.
69
+ ///
70
+ /// If the input vector is empty, the result will be `WholeFile`.
71
+ ///
72
+ /// @param ranges: A vec of 1-based inclusive range.
73
+ /// @return: A `BlameRanges` instance representing the range.
74
+ pub fn from_one_based_inclusive_ranges ( ranges : Vec < RangeInclusive < u32 > > ) -> Self {
75
+ if ranges. is_empty ( ) {
76
+ return Self :: WholeFile ;
77
+ }
78
+
79
+ let zero_based_ranges = ranges
80
+ . into_iter ( )
81
+ . map ( Self :: inclusive_to_zero_based_exclusive)
82
+ . collect :: < Vec < _ > > ( ) ;
83
+ let mut result = Self :: PartialFile ( vec ! [ ] ) ;
84
+ for range in zero_based_ranges {
85
+ let _ = result. merge_range ( range) ;
73
86
}
74
87
result
75
88
}
89
+
90
+ /// Convert a 1-based inclusive range to a 0-based exclusive range.
91
+ fn inclusive_to_zero_based_exclusive ( range : RangeInclusive < u32 > ) -> Range < u32 > {
92
+ let start = range. start ( ) - 1 ;
93
+ let end = * range. end ( ) ;
94
+ start..end
95
+ }
76
96
}
77
97
78
98
impl BlameRanges {
@@ -81,60 +101,51 @@ impl BlameRanges {
81
101
/// The range should be 1-based inclusive.
82
102
/// If the new range overlaps with or is adjacent to an existing range,
83
103
/// 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) ;
104
+ pub fn add_range ( & mut self , new_range : RangeInclusive < u32 > ) -> Result < ( ) , Error > {
105
+ match self {
106
+ Self :: PartialFile ( _) => {
107
+ let zero_based_range = Self :: inclusive_to_zero_based_exclusive ( new_range) ;
108
+ self . merge_range ( zero_based_range)
109
+ }
110
+ _ => Err ( Error :: InvalidOneBasedLineRange ) ,
111
+ }
86
112
}
87
113
88
114
/// Attempts to merge the new range with any existing ranges.
89
115
/// 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 ;
116
+ fn merge_range ( & mut self , new_range : Range < u32 > ) -> Result < ( ) , Error > {
117
+ match self {
118
+ Self :: PartialFile ( ref mut ranges) => {
119
+ // Check if this range can be merged with any existing range
120
+ for range in & mut * ranges {
121
+ // Check if ranges overlap
122
+ if new_range. start <= range. end && range. start <= new_range. end {
123
+ * range = range. start . min ( new_range. start ) ..range. end . max ( new_range. end ) ;
124
+ return Ok ( ( ) ) ;
125
+ }
126
+ // Check if ranges are adjacent
127
+ if new_range. start == range. end || range. start == new_range. end {
128
+ * range = range. start . min ( new_range. start ) ..range. end . max ( new_range. end ) ;
129
+ return Ok ( ( ) ) ;
130
+ }
131
+ }
132
+ // If no overlap or adjacency found, add it as a new range
133
+ ranges. push ( new_range) ;
134
+ Ok ( ( ) )
97
135
}
136
+ _ => Err ( Error :: InvalidOneBasedLineRange ) ,
98
137
}
99
- // If no overlap found, add it as a new range
100
- self . ranges . push ( new_range) ;
101
138
}
102
139
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 ) ;
140
+ /// Convert the ranges to a vector of `Range<u32>`.
141
+ pub fn to_ranges ( & self , max_lines : u32 ) -> Vec < Range < u32 > > {
142
+ match self {
143
+ Self :: WholeFile => {
144
+ let full_range = 0 ..max_lines;
145
+ vec ! [ full_range]
129
146
}
130
- result . push ( start..end ) ;
147
+ Self :: PartialFile ( ranges ) => ranges . clone ( ) ,
131
148
}
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
149
}
139
150
}
140
151
@@ -334,6 +345,17 @@ pub struct UnblamedHunk {
334
345
}
335
346
336
347
impl UnblamedHunk {
348
+ /// Create a new instance
349
+ pub fn new ( range : Range < u32 > , suspect : ObjectId ) -> Self {
350
+ let range_start = range. start ;
351
+ let range_end = range. end ;
352
+
353
+ UnblamedHunk {
354
+ range_in_blamed_file : range_start..range_end,
355
+ suspects : [ ( suspect, range_start..range_end) ] . into ( ) ,
356
+ }
357
+ }
358
+
337
359
pub ( crate ) fn has_suspect ( & self , suspect : & ObjectId ) -> bool {
338
360
self . suspects . iter ( ) . any ( |entry| entry. 0 == * suspect)
339
361
}
0 commit comments