|
| 1 | +use gix_hash::ObjectId; |
| 2 | +use gix_object::bstr::BString; |
| 3 | +use smallvec::SmallVec; |
| 4 | +use std::ops::RangeInclusive; |
1 | 5 | use std::{
|
2 | 6 | num::NonZeroU32,
|
3 | 7 | ops::{AddAssign, Range, SubAssign},
|
4 | 8 | };
|
5 | 9 |
|
6 |
| -use gix_hash::ObjectId; |
7 |
| -use gix_object::bstr::BString; |
8 |
| -use smallvec::SmallVec; |
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 | +/// 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 | +} |
11 | 140 |
|
12 | 141 | /// Options to be passed to [`file()`](crate::file()).
|
13 | 142 | #[derive(Default, Debug, Clone)]
|
14 | 143 | pub struct Options {
|
15 | 144 | /// The algorithm to use for diffing.
|
16 | 145 | 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, |
21 | 148 | /// Don't consider commits before the given date.
|
22 | 149 | pub since: Option<gix_date::Time>,
|
23 | 150 | }
|
|
0 commit comments