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 {
158 return true;
159 }
160 if has_hunk && (has_old_marker || has_new_marker || has_add || has_del) {
161 return true;
162 }
163 if has_old_marker && has_new_marker && (has_add || has_del) {
164 return true;
165 }
166
167 false
168}
169
170pub fn parse_hunk_starts(line: &str) -> Option<(usize, usize)> {
172 let trimmed = line.trim_end();
173 let rest = trimmed.strip_prefix("@@ -")?;
174 let mut parts = rest.split_whitespace();
175 let old_part = parts.next()?;
176 let new_part = parts.next()?;
177 if !new_part.starts_with('+') {
178 return None;
179 }
180
181 let old_start = old_part.split(',').next()?.parse::<usize>().ok()?;
182 let new_start = new_part.trim_start_matches('+').split(',').next()?.parse::<usize>().ok()?;
183 Some((old_start, new_start))
184}
185
186pub fn format_start_only_hunk_header(line: &str) -> Option<String> {
188 let (old_start, new_start) = parse_hunk_starts(line)?;
189 Some(format!("@@ -{old_start} +{new_start} @@"))
190}
191
192#[cfg(test)]
193mod tests {
194 use super::{
195 format_start_only_hunk_header, is_apply_patch_header_line, is_diff_addition_line,
196 is_diff_deletion_line, is_diff_header_line, is_diff_new_file_marker_line,
197 is_diff_old_file_marker_line, language_hint_from_path, looks_like_diff_content,
198 parse_diff_git_path, parse_diff_marker_path, parse_hunk_starts,
199 };
200
201 #[test]
202 fn parses_git_diff_path() {
203 let line = "diff --git a/src/lib.rs b/src/lib.rs";
204 assert_eq!(parse_diff_git_path(line).as_deref(), Some("src/lib.rs"));
205 }
206
207 #[test]
208 fn parses_marker_path() {
209 assert_eq!(parse_diff_marker_path("+++ b/src/main.rs").as_deref(), Some("src/main.rs"));
210 assert_eq!(parse_diff_marker_path("--- /dev/null"), None);
211 }
212
213 #[test]
214 fn infers_language_hint_from_extension() {
215 assert_eq!(language_hint_from_path("src/main.RS").as_deref(), Some("rs"));
216 assert_eq!(language_hint_from_path("Makefile"), None);
217 }
218
219 #[test]
220 fn parses_hunk_starts() {
221 assert_eq!(parse_hunk_starts("@@ -536,4 +540,5 @@"), Some((536, 540)));
222 assert_eq!(parse_hunk_starts("not a hunk"), None);
223 }
224
225 #[test]
226 fn formats_start_only_hunk_header() {
227 assert_eq!(
228 format_start_only_hunk_header("@@ -536,4 +540,5 @@"),
229 Some("@@ -536 +540 @@".to_string())
230 );
231 }
232
233 #[test]
234 fn detects_diff_add_remove_lines() {
235 assert!(is_diff_addition_line("+added"));
236 assert!(!is_diff_addition_line("+++ b/file.rs"));
237 assert!(is_diff_deletion_line("-removed"));
238 assert!(!is_diff_deletion_line("--- a/file.rs"));
239 }
240
241 #[test]
242 fn detects_diff_header_lines() {
243 assert!(is_diff_header_line("diff --git a/a b/a"));
244 assert!(is_diff_header_line("@@ -1 +1 @@"));
245 assert!(is_diff_header_line("+++ b/src/main.rs"));
246 assert!(!is_diff_header_line("println!(\"diff --git\");"));
247 }
248
249 #[test]
250 fn detects_marker_and_apply_patch_header_lines() {
251 assert!(is_diff_old_file_marker_line("--- a/src/lib.rs"));
252 assert!(is_diff_new_file_marker_line("+++ b/src/lib.rs"));
253 assert!(is_apply_patch_header_line("*** Update File: src/lib.rs"));
254 assert!(!is_apply_patch_header_line("*** End Patch"));
255 }
256
257 #[test]
258 fn classifies_git_diff_content() {
259 let diff = "diff --git a/a.rs b/a.rs\n@@ -1 +1 @@\n-old\n+new\n";
260 assert!(looks_like_diff_content(diff));
261 }
262
263 #[test]
264 fn classifies_apply_patch_content() {
265 let patch = "*** Begin Patch\n*** Update File: a.rs\n@@\n-old\n+new\n*** End Patch\n";
266 assert!(looks_like_diff_content(patch));
267 }
268
269 #[test]
270 fn avoids_false_positive_for_regular_code() {
271 let code =
272 "fn delta(x: i32) -> i32 {\n let y = x + 1;\n let z = x - 1;\n y + z\n}\n";
273 assert!(!looks_like_diff_content(code));
274 }
275
276 #[test]
277 fn avoids_false_positive_for_plus_minus_logs() {
278 let log = "+ started service\n- previous pid cleaned\n";
279 assert!(!looks_like_diff_content(log));
280 }
281}