git_bot_feedback/file_utils/
mod.rs1#[cfg(feature = "pyo3")]
2use pyo3::prelude::*;
3
4use std::ops::Range;
5
6pub mod file_filter;
7use crate::DiffHunkHeader;
8
9#[derive(PartialEq, Clone, Copy, Debug, Default)]
11#[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
12#[cfg_attr(feature = "pyo3", pyclass(module = "git_bot_feedback", from_py_object))]
13pub enum LinesChangedOnly {
14 #[default]
19 Off,
20
21 Diff,
28
29 On,
34}
35
36impl LinesChangedOnly {
37 pub(crate) fn is_change_valid(&self, added_lines: bool, diff_hunks: bool) -> bool {
38 match self {
39 LinesChangedOnly::Off => true,
40 LinesChangedOnly::Diff => diff_hunks,
41 LinesChangedOnly::On => added_lines,
42 }
43 }
44}
45
46impl std::fmt::Display for LinesChangedOnly {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 LinesChangedOnly::Off => write!(f, "false"),
50 LinesChangedOnly::Diff => write!(f, "diff"),
51 LinesChangedOnly::On => write!(f, "true"),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Default)]
58#[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
59#[cfg_attr(feature = "pyo3", pyclass(module = "git_bot_feedback", from_py_object))]
60pub struct FileDiffLines {
61 pub added_lines: Vec<u32>,
63
64 pub added_ranges: Vec<Range<u32>>,
69
70 pub diff_hunks: Vec<Range<u32>>,
74}
75
76impl FileDiffLines {
77 pub fn with_info(added_lines: Vec<u32>, diff_hunks: Vec<Range<u32>>) -> Self {
79 let added_ranges = Self::consolidate_numbers_to_ranges(&added_lines);
80 Self {
81 added_lines,
82 added_ranges,
83 diff_hunks,
84 }
85 }
86
87 fn consolidate_numbers_to_ranges(lines: &[u32]) -> Vec<Range<u32>> {
91 let mut iter_lines = lines.iter().enumerate();
92 if let Some((_, start)) = iter_lines.next() {
93 let mut range_start = *start;
94 let mut ranges: Vec<Range<u32>> = Vec::new();
95 let last_entry = lines.len() - 1;
96 for (index, number) in iter_lines {
97 if let Some(prev) = lines.get(index - 1)
98 && (number - 1) != *prev
99 {
100 ranges.push(range_start..(*prev + 1));
103 range_start = *number;
106 }
107 if index == last_entry {
108 ranges.push(range_start..(*number + 1));
110 }
111 }
112 ranges
113 } else {
114 Vec::new()
115 }
116 }
117
118 pub fn get_ranges(&self, lines_changed_only: &LinesChangedOnly) -> Option<Vec<Range<u32>>> {
123 match lines_changed_only {
124 LinesChangedOnly::Diff => Some(self.diff_hunks.to_vec()),
125 LinesChangedOnly::On => Some(self.added_ranges.to_vec()),
126 _ => None,
127 }
128 }
129
130 pub fn is_hunk_in_diff(&self, hunk: &DiffHunkHeader) -> Option<(u32, u32)> {
133 let (start_line, end_line) = if hunk.old_lines > 0 {
134 let start = hunk.old_start;
136 (start, start + hunk.old_lines)
137 } else {
138 let start = hunk.new_start;
140 (start, start + 1)
142 };
143 let inclusive_end = end_line - 1;
144 for range in &self.diff_hunks {
145 if range.contains(&start_line) && range.contains(&inclusive_end) {
146 return Some((start_line, end_line));
147 }
148 }
149 None
150 }
151
152 pub fn is_line_in_diff(&self, line: &u32) -> bool {
155 for range in &self.diff_hunks {
156 if range.contains(line) {
157 return true;
158 }
159 }
160 false
161 }
162}
163
164#[cfg(feature = "pyo3")]
165#[pymethods]
166impl FileDiffLines {
167 #[new]
172 #[pyo3(
173 signature = (added_lines, added_ranges, diff_hunks),
174 text_signature = "(added_lines: list[int], added_ranges: list[tuple[int, int]], diff_hunks: list[tuple[int, int]])"
175 )]
176 pub fn new_py(
177 added_lines: Vec<u32>,
178 added_ranges: Vec<(u32, u32)>,
179 diff_hunks: Vec<(u32, u32)>,
180 ) -> Self {
181 Self {
182 added_lines,
183 added_ranges: added_ranges
184 .into_iter()
185 .map(|(start, end)| start..end)
186 .collect(),
187 diff_hunks: diff_hunks
188 .into_iter()
189 .map(|(start, end)| start..end)
190 .collect(),
191 }
192 }
193
194 #[staticmethod]
199 #[pyo3(
200 signature = (added_lines, diff_hunks),
201 text_signature = "(added_lines: list[int], diff_hunks: list[tuple[int, int]]) -> FileDiffLines"
202 )]
203 pub fn from_info(added_lines: Vec<u32>, diff_hunks: Vec<(u32, u32)>) -> Self {
204 Self::with_info(
205 added_lines,
206 diff_hunks
207 .into_iter()
208 .map(|(start, end)| start..end)
209 .collect(),
210 )
211 }
212
213 #[getter]
218 pub fn get_added_ranges(&self) -> Vec<(u32, u32)> {
219 self.added_ranges
220 .iter()
221 .map(|range| (range.start, range.end))
222 .collect()
223 }
224
225 #[getter]
227 pub fn get_added_lines(&self) -> Vec<u32> {
228 self.added_lines.clone()
229 }
230
231 #[getter]
236 pub fn get_diff_hunks(&self) -> Vec<(u32, u32)> {
237 self.diff_hunks
238 .iter()
239 .map(|range| (range.start, range.end))
240 .collect()
241 }
242
243 #[pyo3(
245 name = "is_hunk_in_diff",
246 signature = (hunk),
247 text_signature = "(hunk: DiffHunkHeader) -> tuple[int, int] | None"
248 )]
249 pub fn is_hunk_in_diff_py(&self, hunk: &DiffHunkHeader) -> Option<(u32, u32)> {
250 self.is_hunk_in_diff(hunk)
251 }
252
253 #[pyo3(
255 name = "is_line_in_diff",
256 signature = (line),
257 text_signature = "(line: int) -> bool"
258 )]
259 pub fn is_line_in_diff_py(&self, line: u32) -> bool {
260 self.is_line_in_diff(&line)
261 }
262}
263
264#[cfg(test)]
265mod test {
266 #![allow(clippy::unwrap_used)]
267
268 use super::{FileDiffLines, LinesChangedOnly};
269
270 #[test]
271 fn display_lines_changed_only() {
272 assert_eq!(LinesChangedOnly::Off.to_string(), "false");
273 assert_eq!(LinesChangedOnly::Diff.to_string(), "diff");
274 assert_eq!(LinesChangedOnly::On.to_string(), "true");
275 }
276
277 #[test]
278 fn get_ranges_none() {
279 let file_obj = FileDiffLines::default();
280 let ranges = file_obj.get_ranges(&LinesChangedOnly::Off);
281 assert!(ranges.is_none());
282 }
283
284 #[test]
285 fn get_ranges_diff() {
286 #[allow(clippy::single_range_in_vec_init)]
287 let diff_chunks = vec![1..11];
288 let added_lines = vec![4, 5, 9];
289 let file_obj = FileDiffLines::with_info(added_lines, diff_chunks.clone());
290 let ranges = file_obj.get_ranges(&LinesChangedOnly::Diff);
291 assert_eq!(ranges.unwrap(), diff_chunks);
292 }
293
294 #[test]
295 fn get_ranges_added() {
296 #[allow(clippy::single_range_in_vec_init)]
297 let diff_chunks = vec![1..11];
298 let added_lines = vec![4, 5, 9];
299 let file_obj = FileDiffLines::with_info(added_lines, diff_chunks);
300 let ranges = file_obj.get_ranges(&LinesChangedOnly::On);
301 assert_eq!(ranges.unwrap(), vec![4..6, 9..10]);
302 }
303
304 #[test]
305 fn line_not_in_diff() {
306 let file_obj = FileDiffLines::default();
307 assert!(!file_obj.is_line_in_diff(&42));
308 }
309}