vtcode_commons/
diff_paths.rs1use std::path::Path;
2
3pub fn parse_diff_git_path(line: &str) -> Option<String> {
5 let mut parts = line.split_whitespace();
6 if parts.next()? != "diff" || parts.next()? != "--git" {
7 return None;
8 }
9 let _old = parts.next()?;
10 let new_path = parts.next()?;
11 Some(new_path.trim_start_matches("b/").to_string())
12}
13
14pub fn parse_diff_marker_path(line: &str) -> Option<String> {
16 let trimmed = line.trim_start();
17 if !(is_diff_old_file_marker_line(trimmed) || is_diff_new_file_marker_line(trimmed)) {
18 return None;
19 }
20 let path = trimmed.split_whitespace().nth(1)?;
21 if path == "/dev/null" {
22 return None;
23 }
24 Some(path.trim_start_matches("a/").trim_start_matches("b/").to_string())
25}
26
27pub fn language_hint_from_path(path: &str) -> Option<String> {
29 Path::new(path)
30 .extension()
31 .and_then(|ext| ext.to_str())
32 .filter(|ext| !ext.is_empty())
33 .map(|ext| ext.to_ascii_lowercase())
34}
35
36pub fn is_diff_addition_line(line: &str) -> bool {
38 line.starts_with('+') && !line.starts_with("+++")
39}
40
41pub fn is_diff_deletion_line(line: &str) -> bool {
43 line.starts_with('-') && !line.starts_with("---")
44}
45
46pub fn is_diff_old_file_marker_line(line: &str) -> bool {
48 line.starts_with("--- ")
49}
50
51pub fn is_diff_new_file_marker_line(line: &str) -> bool {
53 line.starts_with("+++ ")
54}
55
56pub fn is_apply_patch_header_line(line: &str) -> bool {
58 line.starts_with("*** Begin Patch")
59 || line.starts_with("*** Update File:")
60 || line.starts_with("*** Add File:")
61 || line.starts_with("*** Delete File:")
62}
63
64pub fn is_diff_header_line(line: &str) -> bool {
66 line.starts_with("diff --git ")
67 || line.starts_with("@@")
68 || line.starts_with("index ")
69 || line.starts_with("new file mode ")
70 || line.starts_with("deleted file mode ")
71 || line.starts_with("rename from ")
72 || line.starts_with("rename to ")
73 || line.starts_with("copy from ")
74 || line.starts_with("copy to ")
75 || line.starts_with("similarity index ")
76 || line.starts_with("dissimilarity index ")
77 || line.starts_with("old mode ")
78 || line.starts_with("new mode ")
79 || line.starts_with("Binary files ")
80 || line.starts_with("\\ No newline at end of file")
81 || is_diff_new_file_marker_line(line)
82 || is_diff_old_file_marker_line(line)
83 || is_apply_patch_header_line(line)
84}
85
86pub fn looks_like_diff_content(content: &str) -> bool {
91 let mut has_git_header = false;
92 let mut has_hunk = false;
93 let mut has_old_marker = false;
94 let mut has_new_marker = false;
95 let mut has_add = false;
96 let mut has_del = false;
97 let mut has_binary_or_mode_header = false;
98 let mut has_apply_patch = false;
99
100 for raw in content.lines() {
101 let line = raw.trim_start();
102 if line.is_empty() {
103 continue;
104 }
105
106 if line.starts_with("diff --git ") {
107 has_git_header = true;
108 continue;
109 }
110 if line.starts_with("@@") {
111 has_hunk = true;
112 continue;
113 }
114 if is_diff_old_file_marker_line(line) {
115 has_old_marker = true;
116 continue;
117 }
118 if is_diff_new_file_marker_line(line) {
119 has_new_marker = true;
120 continue;
121 }
122 if is_apply_patch_header_line(line) {
123 has_apply_patch = true;
124 continue;
125 }
126 if line.starts_with("new file mode ")
127 || line.starts_with("deleted file mode ")
128 || line.starts_with("rename from ")
129 || line.starts_with("rename to ")
130 || line.starts_with("copy from ")
131 || line.starts_with("copy to ")
132 || line.starts_with("similarity index ")
133 || line.starts_with("dissimilarity index ")
134 || line.starts_with("old mode ")
135 || line.starts_with("new mode ")
136 || line.starts_with("Binary files ")
137 || line.starts_with("index ")
138 || line.starts_with("\\ No newline at end of file")
139 {
140 has_binary_or_mode_header = true;
141 continue;
142 }
143
144 if is_diff_addition_line(line) {
145 has_add = true;
146 continue;
147 }
148 if is_diff_deletion_line(line) {
149 has_del = true;
150 }
151 }
152
153 if has_apply_patch {
154 return true;
155 }
156 if has_git_header && (has_hunk || has_old_marker || has_new_marker || has_binary_or_mode_header) {
157 return true;
158 }
159 if has_hunk && (has_old_marker || has_new_marker || has_add || has_del) {
160 return true;
161 }
162 if has_old_marker && has_new_marker && (has_add || has_del) {
163 return true;
164 }
165
166 false
167}
168
169pub fn parse_hunk_starts(line: &str) -> Option<(usize, usize)> {
171 let trimmed = line.trim_end();
172 let rest = trimmed.strip_prefix("@@ -")?;
173 let mut parts = rest.split_whitespace();
174 let old_part = parts.next()?;
175 let new_part = parts.next()?;
176 if !new_part.starts_with('+') {
177 return None;
178 }
179
180 let old_start = old_part.split(',').next()?.parse::<usize>().ok()?;
181 let new_start = new_part.trim_start_matches('+').split(',').next()?.parse::<usize>().ok()?;
182 Some((old_start, new_start))
183}
184
185pub fn format_start_only_hunk_header(line: &str) -> Option<String> {
187 let (old_start, new_start) = parse_hunk_starts(line)?;
188 Some(format!("@@ -{old_start} +{new_start} @@"))
189}
190
191#[cfg(test)]
192mod tests {
193 use super::{
194 format_start_only_hunk_header, is_apply_patch_header_line, is_diff_addition_line, is_diff_deletion_line,
195 is_diff_header_line, is_diff_new_file_marker_line, is_diff_old_file_marker_line, language_hint_from_path,
196 looks_like_diff_content, parse_diff_git_path, parse_diff_marker_path, parse_hunk_starts,
197 };
198
199 #[test]
200 fn parses_git_diff_path() {
201 let line = "diff --git a/src/lib.rs b/src/lib.rs";
202 assert_eq!(parse_diff_git_path(line).as_deref(), Some("src/lib.rs"));
203 }
204
205 #[test]
206 fn parses_marker_path() {
207 assert_eq!(parse_diff_marker_path("+++ b/src/main.rs").as_deref(), Some("src/main.rs"));
208 assert_eq!(parse_diff_marker_path("--- /dev/null"), None);
209 }
210
211 #[test]
212 fn infers_language_hint_from_extension() {
213 assert_eq!(language_hint_from_path("src/main.RS").as_deref(), Some("rs"));
214 assert_eq!(language_hint_from_path("Makefile"), None);
215 }
216
217 #[test]
218 fn parses_hunk_starts() {
219 assert_eq!(parse_hunk_starts("@@ -536,4 +540,5 @@"), Some((536, 540)));
220 assert_eq!(parse_hunk_starts("not a hunk"), None);
221 }
222
223 #[test]
224 fn formats_start_only_hunk_header() {
225 assert_eq!(format_start_only_hunk_header("@@ -536,4 +540,5 @@"), Some("@@ -536 +540 @@".to_string()));
226 }
227
228 #[test]
229 fn detects_diff_add_remove_lines() {
230 assert!(is_diff_addition_line("+added"));
231 assert!(!is_diff_addition_line("+++ b/file.rs"));
232 assert!(is_diff_deletion_line("-removed"));
233 assert!(!is_diff_deletion_line("--- a/file.rs"));
234 }
235
236 #[test]
237 fn detects_diff_header_lines() {
238 assert!(is_diff_header_line("diff --git a/a b/a"));
239 assert!(is_diff_header_line("@@ -1 +1 @@"));
240 assert!(is_diff_header_line("+++ b/src/main.rs"));
241 assert!(!is_diff_header_line("println!(\"diff --git\");"));
242 }
243
244 #[test]
245 fn detects_marker_and_apply_patch_header_lines() {
246 assert!(is_diff_old_file_marker_line("--- a/src/lib.rs"));
247 assert!(is_diff_new_file_marker_line("+++ b/src/lib.rs"));
248 assert!(is_apply_patch_header_line("*** Update File: src/lib.rs"));
249 assert!(!is_apply_patch_header_line("*** End Patch"));
250 }
251
252 #[test]
253 fn classifies_git_diff_content() {
254 let diff = "diff --git a/a.rs b/a.rs\n@@ -1 +1 @@\n-old\n+new\n";
255 assert!(looks_like_diff_content(diff));
256 }
257
258 #[test]
259 fn classifies_apply_patch_content() {
260 let patch = "*** Begin Patch\n*** Update File: a.rs\n@@\n-old\n+new\n*** End Patch\n";
261 assert!(looks_like_diff_content(patch));
262 }
263
264 #[test]
265 fn avoids_false_positive_for_regular_code() {
266 let code = "fn delta(x: i32) -> i32 {\n let y = x + 1;\n let z = x - 1;\n y + z\n}\n";
267 assert!(!looks_like_diff_content(code));
268 }
269
270 #[test]
271 fn avoids_false_positive_for_plus_minus_logs() {
272 let log = "+ started service\n- previous pid cleaned\n";
273 assert!(!looks_like_diff_content(log));
274 }
275}