git_bot_feedback/file_utils/
mod.rs

1use std::ops::Range;
2
3pub mod file_filter;
4use crate::DiffHunkHeader;
5
6/// An enum to help determine what constitutes a changed file based on the diff contents.
7#[derive(PartialEq, Clone, Debug, Default)]
8#[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
9pub enum LinesChangedOnly {
10    /// File is included regardless of changed lines in the diff.
11    ///
12    /// Use [`FileFilter`](crate::FileFilter) to filter files by
13    /// extension and/or path.
14    #[default]
15    Off,
16
17    /// Only include files with lines in the diff.
18    ///
19    /// Note, this *includes* files that only have lines with deletions.
20    /// But, this *excludes* files that have no line changes at all
21    /// (eg. renamed files with unmodified contents, or deleted files, or
22    /// binary files).
23    Diff,
24
25    /// Only include files with lines in the diff that have additions.
26    ///
27    /// Note, this *excludes* files that only have lines with deletions.
28    /// So, this is like [`LinesChangedOnly::Diff`] but stricter.
29    On,
30}
31
32impl LinesChangedOnly {
33    pub(crate) fn is_change_valid(&self, added_lines: bool, diff_hunks: bool) -> bool {
34        match self {
35            LinesChangedOnly::Off => true,
36            LinesChangedOnly::Diff => diff_hunks,
37            LinesChangedOnly::On => added_lines,
38        }
39    }
40}
41
42/// A structure to represent a file's changes per line numbers.
43#[derive(Debug, Clone, Default)]
44#[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
45pub struct FileDiffLines {
46    /// The list of lines numbers with additions.
47    pub added_lines: Vec<u32>,
48
49    /// The list of ranges that span only lines numbers with additions.
50    ///
51    /// The line numbers here disregard the old line numbers in the diff hunks.
52    /// Each range describes the beginning and ending of a group of consecutive line numbers.
53    pub added_ranges: Vec<Range<u32>>,
54
55    /// The list of ranges that span the lines numbers present in diff chunks.
56    ///
57    /// The line numbers here disregard the old line numbers in the diff hunks.
58    pub diff_hunks: Vec<Range<u32>>,
59}
60
61impl FileDiffLines {
62    /// Instantiate an object with changed lines information.
63    pub fn with_info(added_lines: Vec<u32>, diff_chunks: Vec<Range<u32>>) -> Self {
64        let added_ranges = Self::consolidate_numbers_to_ranges(&added_lines);
65        Self {
66            added_lines,
67            added_ranges,
68            diff_hunks: diff_chunks,
69        }
70    }
71
72    /// A helper function to consolidate a [Vec<u32>] of line numbers into a
73    /// [Vec<Range<u32>>] in which each range describes the beginning and
74    /// ending of a group of consecutive line numbers.
75    fn consolidate_numbers_to_ranges(lines: &[u32]) -> Vec<Range<u32>> {
76        let mut iter_lines = lines.iter().enumerate();
77        if let Some((_, start)) = iter_lines.next() {
78            let mut range_start = *start;
79            let mut ranges: Vec<Range<u32>> = Vec::new();
80            let last_entry = lines.len() - 1;
81            for (index, number) in iter_lines {
82                if let Some(prev) = lines.get(index - 1)
83                    && (number - 1) != *prev
84                {
85                    // non-consecutive number found
86                    // push the previous range
87                    ranges.push(range_start..(*prev + 1));
88                    // and start a new range
89                    // from the current number
90                    range_start = *number;
91                }
92                if index == last_entry {
93                    // last number
94                    ranges.push(range_start..(*number + 1));
95                }
96            }
97            ranges
98        } else {
99            Vec::new()
100        }
101    }
102
103    pub fn get_ranges(&self, lines_changed_only: &LinesChangedOnly) -> Option<Vec<Range<u32>>> {
104        match lines_changed_only {
105            LinesChangedOnly::Diff => Some(self.diff_hunks.to_vec()),
106            LinesChangedOnly::On => Some(self.added_ranges.to_vec()),
107            _ => None,
108        }
109    }
110
111    /// Is the range from [`DiffHunkHeader`] contained in a single item of
112    /// [`FileDiffLines::diff_hunks`]?
113    pub fn is_hunk_in_diff(&self, hunk: &DiffHunkHeader) -> Option<(u32, u32)> {
114        let (start_line, end_line) = if hunk.old_lines > 0 {
115            // if old hunk's total lines is > 0
116            let start = hunk.old_start;
117            (start, start + hunk.old_lines)
118        } else {
119            // old hunk's total lines is 0, meaning changes were only added
120            let start = hunk.new_start;
121            // make old hunk's range span 1 line
122            (start, start + 1)
123        };
124        let inclusive_end = end_line - 1;
125        for range in &self.diff_hunks {
126            if range.contains(&start_line) && range.contains(&inclusive_end) {
127                return Some((start_line, end_line));
128            }
129        }
130        None
131    }
132
133    /// Similar to [`FileDiffLines::is_hunk_in_diff()`] but looks for a single line instead of
134    /// all lines in a [`DiffHunkHeader`].
135    pub fn is_line_in_diff(&self, line: &u32) -> bool {
136        for range in &self.diff_hunks {
137            if range.contains(line) {
138                return true;
139            }
140        }
141        false
142    }
143}
144
145#[cfg(test)]
146mod test {
147    use super::{FileDiffLines, LinesChangedOnly};
148
149    #[test]
150    fn get_ranges_none() {
151        let file_obj = FileDiffLines::default();
152        let ranges = file_obj.get_ranges(&LinesChangedOnly::Off);
153        assert!(ranges.is_none());
154    }
155
156    #[test]
157    fn get_ranges_diff() {
158        let diff_chunks = vec![1..11];
159        let added_lines = vec![4, 5, 9];
160        let file_obj = FileDiffLines::with_info(added_lines, diff_chunks.clone());
161        let ranges = file_obj.get_ranges(&LinesChangedOnly::Diff);
162        assert_eq!(ranges.unwrap(), diff_chunks);
163    }
164
165    #[test]
166    fn get_ranges_added() {
167        let diff_chunks = vec![1..11];
168        let added_lines = vec![4, 5, 9];
169        let file_obj = FileDiffLines::with_info(added_lines, diff_chunks);
170        let ranges = file_obj.get_ranges(&LinesChangedOnly::On);
171        assert_eq!(ranges.unwrap(), vec![4..6, 9..10]);
172    }
173
174    #[test]
175    fn line_not_in_diff() {
176        let file_obj = FileDiffLines::default();
177        assert!(!file_obj.is_line_in_diff(&42));
178    }
179}