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}