@@ -8,16 +8,142 @@ use gix_object::bstr::BString;
8
8
use smallvec:: SmallVec ;
9
9
10
10
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
+ /// This type 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..41);
25
+ ///
26
+ /// // Blame multiple ranges
27
+ /// let mut ranges = BlameRanges::new();
28
+ /// ranges.add_range(1..5); // Lines 1-4
29
+ /// ranges.add_range(10..15); // 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..41` 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 < Range < u32 > > ,
47
+ }
48
+
49
+ impl BlameRanges {
50
+ /// Create a new empty BlameRanges instance.
51
+ ///
52
+ /// An empty instance means to blame the entire file.
53
+ pub fn new ( ) -> Self {
54
+ Self { ranges : Vec :: new ( ) }
55
+ }
56
+
57
+ /// Add a single range to blame.
58
+ ///
59
+ /// The range should be 1-based inclusive.
60
+ /// If the new range overlaps with or is adjacent to an existing range,
61
+ /// they will be merged into a single range.
62
+ pub fn add_range ( & mut self , new_range : Range < u32 > ) {
63
+ self . merge_range ( new_range) ;
64
+ }
65
+
66
+ /// Create from a single range.
67
+ ///
68
+ /// The range should be 1-based inclusive, similar to git's line number format.
69
+ pub fn from_range ( range : Range < u32 > ) -> Self {
70
+ Self { ranges : vec ! [ range] }
71
+ }
72
+
73
+ /// Create from multiple ranges.
74
+ ///
75
+ /// All ranges should be 1-based inclusive.
76
+ /// Overlapping or adjacent ranges will be merged.
77
+ pub fn from_ranges ( ranges : Vec < Range < u32 > > ) -> Self {
78
+ let mut result = Self :: new ( ) ;
79
+ for range in ranges {
80
+ result. merge_range ( range) ;
81
+ }
82
+ result
83
+ }
84
+
85
+ /// Attempts to merge the new range with any existing ranges.
86
+ /// If no merge is possible, adds it as a new range.
87
+ fn merge_range ( & mut self , new_range : Range < u32 > ) {
88
+ // First check if this range can be merged with any existing range
89
+ for range in & mut self . ranges {
90
+ // Check if ranges overlap or are adjacent
91
+ if new_range. start <= range. end && range. start <= new_range. end {
92
+ // Merge the ranges by taking the minimum start and maximum end
93
+ range. start = range. start . min ( new_range. start ) ;
94
+ range. end = range. end . max ( new_range. end ) ;
95
+ return ;
96
+ }
97
+ }
98
+ // If no overlap found, add as new range
99
+ self . ranges . push ( new_range) ;
100
+ }
101
+
102
+ /// Convert the 1-based inclusive ranges to 0-based exclusive ranges.
103
+ ///
104
+ /// This is used internally by the blame algorithm to convert from git's line number format
105
+ /// to the internal format used for processing.
106
+ ///
107
+ /// # Errors
108
+ ///
109
+ /// Returns `Error::InvalidLineRange` if:
110
+ /// - Any range starts at 0 (must be 1-based)
111
+ /// - Any range extends beyond the file's length
112
+ /// - Any range has the same start and end
113
+ pub fn to_zero_based_exclusive ( & self , max_lines : u32 ) -> Result < Vec < Range < u32 > > , Error > {
114
+ if self . ranges . is_empty ( ) {
115
+ let range = 0 ..max_lines;
116
+ return Ok ( vec ! [ range] ) ;
117
+ }
118
+
119
+ let mut result = Vec :: with_capacity ( self . ranges . len ( ) ) ;
120
+ for range in & self . ranges {
121
+ if range. start == 0 {
122
+ return Err ( Error :: InvalidLineRange ) ;
123
+ }
124
+ let start = range. start - 1 ;
125
+ let end = range. end ;
126
+ if start >= max_lines || end > max_lines || start == end {
127
+ return Err ( Error :: InvalidLineRange ) ;
128
+ }
129
+ result. push ( start..end) ;
130
+ }
131
+ Ok ( result)
132
+ }
133
+
134
+ /// Returns true if no specific ranges are set (meaning blame entire file)
135
+ pub fn is_empty ( & self ) -> bool {
136
+ self . ranges . is_empty ( )
137
+ }
138
+ }
11
139
12
140
/// Options to be passed to [`file()`](crate::file()).
13
141
#[ derive( Default , Debug , Clone ) ]
14
142
pub struct Options {
15
143
/// The algorithm to use for diffing.
16
144
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 > > ,
145
+ /// The ranges to blame in the file.
146
+ pub range : BlameRanges ,
21
147
/// Don't consider commits before the given date.
22
148
pub since : Option < gix_date:: Time > ,
23
149
}
0 commit comments