rust_diff_analyzer/git/
hunk.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use serde::{Deserialize, Serialize};
5
6/// Type of line in a diff hunk
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub enum LineType {
9    /// Line was added
10    Added,
11    /// Line was removed
12    Removed,
13    /// Context line (unchanged)
14    Context,
15}
16
17/// A single line in a diff hunk
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct HunkLine {
20    /// Type of the line
21    pub line_type: LineType,
22    /// Line number in original file (for removed/context)
23    pub old_line: Option<usize>,
24    /// Line number in new file (for added/context)
25    pub new_line: Option<usize>,
26    /// Content of the line
27    pub content: String,
28}
29
30impl HunkLine {
31    /// Creates a new added line
32    ///
33    /// # Arguments
34    ///
35    /// * `new_line` - Line number in new file
36    /// * `content` - Content of the line
37    ///
38    /// # Returns
39    ///
40    /// A new HunkLine for an added line
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use rust_diff_analyzer::git::HunkLine;
46    ///
47    /// let line = HunkLine::added(10, "let x = 5;".to_string());
48    /// assert_eq!(line.new_line, Some(10));
49    /// ```
50    pub fn added(new_line: usize, content: String) -> Self {
51        Self {
52            line_type: LineType::Added,
53            old_line: None,
54            new_line: Some(new_line),
55            content,
56        }
57    }
58
59    /// Creates a new removed line
60    ///
61    /// # Arguments
62    ///
63    /// * `old_line` - Line number in original file
64    /// * `content` - Content of the line
65    ///
66    /// # Returns
67    ///
68    /// A new HunkLine for a removed line
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use rust_diff_analyzer::git::HunkLine;
74    ///
75    /// let line = HunkLine::removed(5, "let y = 10;".to_string());
76    /// assert_eq!(line.old_line, Some(5));
77    /// ```
78    pub fn removed(old_line: usize, content: String) -> Self {
79        Self {
80            line_type: LineType::Removed,
81            old_line: Some(old_line),
82            new_line: None,
83            content,
84        }
85    }
86
87    /// Creates a new context line
88    ///
89    /// # Arguments
90    ///
91    /// * `old_line` - Line number in original file
92    /// * `new_line` - Line number in new file
93    /// * `content` - Content of the line
94    ///
95    /// # Returns
96    ///
97    /// A new HunkLine for a context line
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use rust_diff_analyzer::git::HunkLine;
103    ///
104    /// let line = HunkLine::context(5, 6, "fn main() {".to_string());
105    /// assert_eq!(line.old_line, Some(5));
106    /// assert_eq!(line.new_line, Some(6));
107    /// ```
108    pub fn context(old_line: usize, new_line: usize, content: String) -> Self {
109        Self {
110            line_type: LineType::Context,
111            old_line: Some(old_line),
112            new_line: Some(new_line),
113            content,
114        }
115    }
116
117    /// Checks if this is an added line
118    ///
119    /// # Returns
120    ///
121    /// `true` if line was added
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use rust_diff_analyzer::git::HunkLine;
127    ///
128    /// let line = HunkLine::added(10, "code".to_string());
129    /// assert!(line.is_added());
130    /// ```
131    pub fn is_added(&self) -> bool {
132        matches!(self.line_type, LineType::Added)
133    }
134
135    /// Checks if this is a removed line
136    ///
137    /// # Returns
138    ///
139    /// `true` if line was removed
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use rust_diff_analyzer::git::HunkLine;
145    ///
146    /// let line = HunkLine::removed(5, "code".to_string());
147    /// assert!(line.is_removed());
148    /// ```
149    pub fn is_removed(&self) -> bool {
150        matches!(self.line_type, LineType::Removed)
151    }
152}
153
154/// A hunk in a unified diff
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct Hunk {
157    /// Starting line in original file
158    pub old_start: usize,
159    /// Number of lines in original file
160    pub old_count: usize,
161    /// Starting line in new file
162    pub new_start: usize,
163    /// Number of lines in new file
164    pub new_count: usize,
165    /// Lines in the hunk
166    pub lines: Vec<HunkLine>,
167}
168
169impl Hunk {
170    /// Creates a new hunk
171    ///
172    /// # Arguments
173    ///
174    /// * `old_start` - Starting line in original file
175    /// * `old_count` - Number of lines in original file
176    /// * `new_start` - Starting line in new file
177    /// * `new_count` - Number of lines in new file
178    ///
179    /// # Returns
180    ///
181    /// A new Hunk with empty lines
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// use rust_diff_analyzer::git::Hunk;
187    ///
188    /// let hunk = Hunk::new(10, 5, 10, 7);
189    /// assert_eq!(hunk.old_start, 10);
190    /// assert!(hunk.lines.is_empty());
191    /// ```
192    pub fn new(old_start: usize, old_count: usize, new_start: usize, new_count: usize) -> Self {
193        Self {
194            old_start,
195            old_count,
196            new_start,
197            new_count,
198            lines: Vec::new(),
199        }
200    }
201
202    /// Returns count of added lines
203    ///
204    /// # Returns
205    ///
206    /// Number of added lines in hunk
207    ///
208    /// # Examples
209    ///
210    /// ```
211    /// use rust_diff_analyzer::git::{Hunk, HunkLine};
212    ///
213    /// let mut hunk = Hunk::new(1, 1, 1, 2);
214    /// hunk.lines.push(HunkLine::added(1, "new line".to_string()));
215    /// assert_eq!(hunk.added_count(), 1);
216    /// ```
217    pub fn added_count(&self) -> usize {
218        self.lines.iter().filter(|l| l.is_added()).count()
219    }
220
221    /// Returns count of removed lines
222    ///
223    /// # Returns
224    ///
225    /// Number of removed lines in hunk
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use rust_diff_analyzer::git::{Hunk, HunkLine};
231    ///
232    /// let mut hunk = Hunk::new(1, 2, 1, 1);
233    /// hunk.lines
234    ///     .push(HunkLine::removed(1, "old line".to_string()));
235    /// assert_eq!(hunk.removed_count(), 1);
236    /// ```
237    pub fn removed_count(&self) -> usize {
238        self.lines.iter().filter(|l| l.is_removed()).count()
239    }
240
241    /// Returns all added line numbers in new file
242    ///
243    /// # Returns
244    ///
245    /// Vector of line numbers that were added
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use rust_diff_analyzer::git::{Hunk, HunkLine};
251    ///
252    /// let mut hunk = Hunk::new(1, 1, 1, 2);
253    /// hunk.lines.push(HunkLine::added(5, "new".to_string()));
254    /// hunk.lines.push(HunkLine::added(6, "lines".to_string()));
255    /// assert_eq!(hunk.added_lines(), vec![5, 6]);
256    /// ```
257    pub fn added_lines(&self) -> Vec<usize> {
258        self.lines
259            .iter()
260            .filter_map(|l| if l.is_added() { l.new_line } else { None })
261            .collect()
262    }
263
264    /// Returns all removed line numbers in old file
265    ///
266    /// # Returns
267    ///
268    /// Vector of line numbers that were removed
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// use rust_diff_analyzer::git::{Hunk, HunkLine};
274    ///
275    /// let mut hunk = Hunk::new(1, 2, 1, 1);
276    /// hunk.lines.push(HunkLine::removed(3, "old".to_string()));
277    /// assert_eq!(hunk.removed_lines(), vec![3]);
278    /// ```
279    pub fn removed_lines(&self) -> Vec<usize> {
280        self.lines
281            .iter()
282            .filter_map(|l| if l.is_removed() { l.old_line } else { None })
283            .collect()
284    }
285}