@@ -8,16 +8,142 @@ use gix_object::bstr::BString;
88use smallvec:: SmallVec ;
99
1010use 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+ }
11139
12140/// Options to be passed to [`file()`](crate::file()).
13141#[ derive( Default , Debug , Clone ) ]
14142pub struct Options {
15143 /// The algorithm to use for diffing.
16144 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 ,
21147 /// Don't consider commits before the given date.
22148 pub since : Option < gix_date:: Time > ,
23149}
0 commit comments