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_prose_language_hint(hint: Option<&str>) -> bool {
41 matches!(hint, Some("md" | "markdown" | "txt" | "text" | "rst" | "adoc" | "textile"))
42}
43
44pub fn is_diff_addition_line(line: &str) -> bool {
46 line.starts_with('+') && !line.starts_with("+++")
47}
48
49pub fn is_diff_deletion_line(line: &str) -> bool {
51 line.starts_with('-') && !line.starts_with("---")
52}
53
54pub(crate) fn is_diff_old_file_marker_line(line: &str) -> bool {
56 line.starts_with("--- ")
57}
58
59pub fn is_diff_new_file_marker_line(line: &str) -> bool {
61 line.starts_with("+++ ")
62}
63
64pub(crate) fn is_apply_patch_header_line(line: &str) -> bool {
66 line.starts_with("*** Begin Patch")
67 || line.starts_with("*** Update File:")
68 || line.starts_with("*** Add File:")
69 || line.starts_with("*** Delete File:")
70}
71
72pub fn is_diff_header_line(line: &str) -> bool {
74 line.starts_with("diff --git ")
75 || line.starts_with("@@")
76 || line.starts_with("index ")
77 || line.starts_with("new file mode ")
78 || line.starts_with("deleted file mode ")
79 || line.starts_with("rename from ")
80 || line.starts_with("rename to ")
81 || line.starts_with("copy from ")
82 || line.starts_with("copy to ")
83 || line.starts_with("similarity index ")
84 || line.starts_with("dissimilarity index ")
85 || line.starts_with("old mode ")
86 || line.starts_with("new mode ")
87 || line.starts_with("Binary files ")
88 || line.starts_with("\\ No newline at end of file")
89 || is_diff_new_file_marker_line(line)
90 || is_diff_old_file_marker_line(line)
91 || is_apply_patch_header_line(line)
92}
93
94pub fn looks_like_diff_content(content: &str) -> bool {
99 let mut has_git_header = false;
100 let mut has_hunk = false;
101 let mut has_old_marker = false;
102 let mut has_new_marker = false;
103 let mut has_add = false;
104 let mut has_del = false;
105 let mut has_binary_or_mode_header = false;
106 let mut has_apply_patch = false;
107
108 for raw in content.lines() {
109 let line = raw.trim_start();
110 if line.is_empty() {
111 continue;
112 }
113
114 if line.starts_with("diff --git ") {
115 has_git_header = true;
116 continue;
117 }
118 if line.starts_with("@@") {
119 has_hunk = true;
120 continue;
121 }
122 if is_diff_old_file_marker_line(line) {
123 has_old_marker = true;
124 continue;
125 }
126 if is_diff_new_file_marker_line(line) {
127 has_new_marker = true;
128 continue;
129 }
130 if is_apply_patch_header_line(line) {
131 has_apply_patch = true;
132 continue;
133 }
134 if line.starts_with("new file mode ")
135 || line.starts_with("deleted file mode ")
136 || line.starts_with("rename from ")
137 || line.starts_with("rename to ")
138 || line.starts_with("copy from ")
139 || line.starts_with("copy to ")
140 || line.starts_with("similarity index ")
141 || line.starts_with("dissimilarity index ")
142 || line.starts_with("old mode ")
143 || line.starts_with("new mode ")
144 || line.starts_with("Binary files ")
145 || line.starts_with("index ")
146 || line.starts_with("\\ No newline at end of file")
147 {
148 has_binary_or_mode_header = true;
149 continue;
150 }
151
152 if is_diff_addition_line(line) {
153 has_add = true;
154 continue;
155 }
156 if is_diff_deletion_line(line) {
157 has_del = true;
158 }
159 }
160
161 if has_apply_patch {
162 return true;
163 }
164 if has_git_header && (has_hunk || has_old_marker || has_new_marker || has_binary_or_mode_header) {
165 return true;
166 }
167 if has_hunk && (has_old_marker || has_new_marker || has_add || has_del) {
168 return true;
169 }
170 if has_old_marker && has_new_marker && (has_add || has_del) {
171 return true;
172 }
173
174 false
175}
176
177pub(crate) fn parse_hunk_starts(line: &str) -> Option<(usize, usize)> {
179 let trimmed = line.trim_end();
180 let rest = trimmed.strip_prefix("@@ -")?;
181 let mut parts = rest.split_whitespace();
182 let old_part = parts.next()?;
183 let new_part = parts.next()?;
184 if !new_part.starts_with('+') {
185 return None;
186 }
187
188 let old_start = old_part.split(',').next()?.parse::<usize>().ok()?;
189 let new_start = new_part.trim_start_matches('+').split(',').next()?.parse::<usize>().ok()?;
190 Some((old_start, new_start))
191}
192
193pub fn format_start_only_hunk_header(line: &str) -> Option<String> {
195 let (old_start, new_start) = parse_hunk_starts(line)?;
196 Some(format!("@@ -{old_start} +{new_start} @@"))
197}
198
199#[cfg(test)]
200mod tests {
201 use super::{
202 format_start_only_hunk_header, is_apply_patch_header_line, is_diff_addition_line, is_diff_deletion_line,
203 is_diff_header_line, is_diff_new_file_marker_line, is_diff_old_file_marker_line, is_prose_language_hint,
204 language_hint_from_path, looks_like_diff_content, parse_diff_git_path, parse_diff_marker_path,
205 parse_hunk_starts,
206 };
207
208 #[test]
209 fn parses_git_diff_path() {
210 let line = "diff --git a/src/lib.rs b/src/lib.rs";
211 assert_eq!(parse_diff_git_path(line).as_deref(), Some("src/lib.rs"));
212 }
213
214 #[test]
215 fn parses_marker_path() {
216 assert_eq!(parse_diff_marker_path("+++ b/src/main.rs").as_deref(), Some("src/main.rs"));
217 assert_eq!(parse_diff_marker_path("--- /dev/null"), None);
218 }
219
220 #[test]
221 fn infers_language_hint_from_extension() {
222 assert_eq!(language_hint_from_path("src/main.RS").as_deref(), Some("rs"));
223 assert_eq!(language_hint_from_path("Makefile"), None);
224 }
225
226 #[test]
227 fn parses_hunk_starts() {
228 assert_eq!(parse_hunk_starts("@@ -536,4 +540,5 @@"), Some((536, 540)));
229 assert_eq!(parse_hunk_starts("not a hunk"), None);
230 }
231
232 #[test]
233 fn formats_start_only_hunk_header() {
234 assert_eq!(format_start_only_hunk_header("@@ -536,4 +540,5 @@"), Some("@@ -536 +540 @@".to_string()));
235 }
236
237 #[test]
238 fn detects_diff_add_remove_lines() {
239 assert!(is_diff_addition_line("+added"));
240 assert!(!is_diff_addition_line("+++ b/file.rs"));
241 assert!(is_diff_deletion_line("-removed"));
242 assert!(!is_diff_deletion_line("--- a/file.rs"));
243 }
244
245 #[test]
246 fn detects_diff_header_lines() {
247 assert!(is_diff_header_line("diff --git a/a b/a"));
248 assert!(is_diff_header_line("@@ -1 +1 @@"));
249 assert!(is_diff_header_line("+++ b/src/main.rs"));
250 assert!(!is_diff_header_line("println!(\"diff --git\");"));
251 }
252
253 #[test]
254 fn detects_marker_and_apply_patch_header_lines() {
255 assert!(is_diff_old_file_marker_line("--- a/src/lib.rs"));
256 assert!(is_diff_new_file_marker_line("+++ b/src/lib.rs"));
257 assert!(is_apply_patch_header_line("*** Update File: src/lib.rs"));
258 assert!(!is_apply_patch_header_line("*** End Patch"));
259 }
260
261 #[test]
262 fn classifies_git_diff_content() {
263 let diff = "diff --git a/a.rs b/a.rs\n@@ -1 +1 @@\n-old\n+new\n";
264 assert!(looks_like_diff_content(diff));
265 }
266
267 #[test]
268 fn classifies_apply_patch_content() {
269 let patch = "*** Begin Patch\n*** Update File: a.rs\n@@\n-old\n+new\n*** End Patch\n";
270 assert!(looks_like_diff_content(patch));
271 }
272
273 #[test]
274 fn avoids_false_positive_for_regular_code() {
275 let code = "fn delta(x: i32) -> i32 {\n let y = x + 1;\n let z = x - 1;\n y + z\n}\n";
276 assert!(!looks_like_diff_content(code));
277 }
278
279 #[test]
280 fn avoids_false_positive_for_plus_minus_logs() {
281 let log = "+ started service\n- previous pid cleaned\n";
282 assert!(!looks_like_diff_content(log));
283 }
284
285 #[test]
286 fn detects_prose_language_hints() {
287 for hint in ["md", "markdown", "txt", "text", "rst", "adoc"] {
288 assert!(is_prose_language_hint(Some(hint)), "{hint} should be prose");
289 }
290 assert!(!is_prose_language_hint(Some("rs")));
291 assert!(!is_prose_language_hint(Some("toml")));
292 assert!(!is_prose_language_hint(None));
293 }
294}