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)> {
136 let (start_line, end_line) = if hunk.old_lines > 0 {
137 let start = hunk.old_start;
139 (start, start + hunk.old_lines)
140 } else {
141 let start = hunk.new_start;
143 (start, start + 1)
145 };
146 let inclusive_end = end_line - 1;
147 for range in &self.diff_hunks {
148 if range.contains(&start_line) && range.contains(&inclusive_end) {
149 return Some((start_line, end_line));
150 }
151 }
152 None
153 }
154
155 pub fn is_line_in_diff(&self, line: &u32) -> bool {
158 for range in &self.diff_hunks {
159 if range.contains(line) {
160 return true;
161 }
162 }
163 false
164 }
165}
166
167#[cfg(feature = "pyo3")]
168#[pymethods]
169impl FileDiffLines {
170 #[new]
175 #[pyo3(
176 signature = (added_lines, added_ranges, diff_hunks),
177 text_signature = "(added_lines: list[int], added_ranges: list[tuple[int, int]], diff_hunks: list[tuple[int, int]])"
178 )]
179 pub fn new_py(
180 added_lines: Vec<u32>,
181 added_ranges: Vec<(u32, u32)>,
182 diff_hunks: Vec<(u32, u32)>,
183 ) -> Self {
184 Self {
185 added_lines,
186 added_ranges: added_ranges
187 .into_iter()
188 .map(|(start, end)| start..end)
189 .collect(),
190 diff_hunks: diff_hunks
191 .into_iter()
192 .map(|(start, end)| start..end)
193 .collect(),
194 }
195 }
196
197 #[staticmethod]
202 #[pyo3(
203 signature = (added_lines, diff_hunks),
204 text_signature = "(added_lines: list[int], diff_hunks: list[tuple[int, int]]) -> FileDiffLines"
205 )]
206 pub fn from_info(added_lines: Vec<u32>, diff_hunks: Vec<(u32, u32)>) -> Self {
207 Self::with_info(
208 added_lines,
209 diff_hunks
210 .into_iter()
211 .map(|(start, end)| start..end)
212 .collect(),
213 )
214 }
215
216 #[getter]
221 pub fn get_added_ranges(&self) -> Vec<(u32, u32)> {
222 self.added_ranges
223 .iter()
224 .map(|range| (range.start, range.end))
225 .collect()
226 }
227
228 #[getter]
230 pub fn get_added_lines(&self) -> Vec<u32> {
231 self.added_lines.clone()
232 }
233
234 #[getter]
239 pub fn get_diff_hunks(&self) -> Vec<(u32, u32)> {
240 self.diff_hunks
241 .iter()
242 .map(|range| (range.start, range.end))
243 .collect()
244 }
245
246 #[pyo3(
248 name = "is_hunk_in_diff",
249 signature = (hunk),
250 text_signature = "(hunk: DiffHunkHeader) -> tuple[int, int] | None"
251 )]
252 pub fn is_hunk_in_diff_py(&self, hunk: &DiffHunkHeader) -> Option<(u32, u32)> {
253 self.is_hunk_in_diff(hunk)
254 }
255
256 #[pyo3(
258 name = "is_line_in_diff",
259 signature = (line),
260 text_signature = "(line: int) -> bool"
261 )]
262 pub fn is_line_in_diff_py(&self, line: u32) -> bool {
263 self.is_line_in_diff(&line)
264 }
265}
266
267#[cfg(test)]
268mod test {
269 #![allow(clippy::unwrap_used)]
270
271 use super::{FileDiffLines, LinesChangedOnly};
272
273 #[test]
274 fn display_lines_changed_only() {
275 assert_eq!(LinesChangedOnly::Off.to_string(), "false");
276 assert_eq!(LinesChangedOnly::Diff.to_string(), "diff");
277 assert_eq!(LinesChangedOnly::On.to_string(), "true");
278 }
279
280 #[test]
281 fn get_ranges_none() {
282 let file_obj = FileDiffLines::default();
283 let ranges = file_obj.get_ranges(&LinesChangedOnly::Off);
284 assert!(ranges.is_none());
285 }
286
287 #[test]
288 fn get_ranges_diff() {
289 #[allow(clippy::single_range_in_vec_init)]
290 let diff_chunks = vec![1..11];
291 let added_lines = vec![4, 5, 9];
292 let file_obj = FileDiffLines::with_info(added_lines, diff_chunks.clone());
293 let ranges = file_obj.get_ranges(&LinesChangedOnly::Diff);
294 assert_eq!(ranges.unwrap(), diff_chunks);
295 }
296
297 #[test]
298 fn get_ranges_added() {
299 #[allow(clippy::single_range_in_vec_init)]
300 let diff_chunks = vec![1..11];
301 let added_lines = vec![4, 5, 9];
302 let file_obj = FileDiffLines::with_info(added_lines, diff_chunks);
303 let ranges = file_obj.get_ranges(&LinesChangedOnly::On);
304 assert_eq!(ranges.unwrap(), vec![4..6, 9..10]);
305 }
306
307 #[test]
308 fn line_not_in_diff() {
309 let file_obj = FileDiffLines::default();
310 assert!(!file_obj.is_line_in_diff(&42));
311 }
312}