1use crate::error::GenerationError;
9use crate::models::GeneratedFile;
10use serde::{Deserialize, Serialize};
11use std::fs;
12use std::path::{Path, PathBuf};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct FileConflictInfo {
17 pub path: PathBuf,
19 pub old_content: String,
21 pub new_content: String,
23 pub diff: FileDiff,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct FileDiff {
30 pub added_lines: Vec<DiffLine>,
32 pub removed_lines: Vec<DiffLine>,
34 pub modified_lines: Vec<(DiffLine, DiffLine)>,
36 pub total_changes: usize,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct DiffLine {
43 pub line_number: usize,
45 pub content: String,
47}
48
49pub struct ConflictDetector;
55
56impl ConflictDetector {
57 pub fn new() -> Self {
59 Self
60 }
61
62 pub fn detect(
77 &self,
78 files: &[GeneratedFile],
79 target_dir: &Path,
80 ) -> Result<Vec<FileConflictInfo>, GenerationError> {
81 let mut conflicts = Vec::new();
82
83 for file in files {
84 let file_path = target_dir.join(&file.path);
85
86 if file_path.exists() {
88 let old_content = fs::read_to_string(&file_path).map_err(|e| {
90 GenerationError::ValidationError {
91 file: file.path.clone(),
92 line: 0,
93 message: format!("Failed to read existing file: {}", e),
94 }
95 })?;
96
97 let diff = self.compute_diff(&old_content, &file.content)?;
99
100 conflicts.push(FileConflictInfo {
101 path: file_path,
102 old_content,
103 new_content: file.content.clone(),
104 diff,
105 });
106 }
107 }
108
109 Ok(conflicts)
110 }
111
112 pub fn detect_single(
121 &self,
122 file_path: &Path,
123 new_content: &str,
124 ) -> Result<Option<FileConflictInfo>, GenerationError> {
125 if !file_path.exists() {
126 return Ok(None);
127 }
128
129 let old_content =
130 fs::read_to_string(file_path).map_err(|e| GenerationError::ValidationError {
131 file: file_path.to_string_lossy().to_string(),
132 line: 0,
133 message: format!("Failed to read existing file: {}", e),
134 })?;
135
136 let diff = self.compute_diff(&old_content, new_content)?;
137
138 Ok(Some(FileConflictInfo {
139 path: file_path.to_path_buf(),
140 old_content,
141 new_content: new_content.to_string(),
142 diff,
143 }))
144 }
145
146 fn compute_diff(
157 &self,
158 old_content: &str,
159 new_content: &str,
160 ) -> Result<FileDiff, GenerationError> {
161 let old_lines: Vec<&str> = old_content.lines().collect();
162 let new_lines: Vec<&str> = new_content.lines().collect();
163
164 let mut added_lines = Vec::new();
165 let mut removed_lines = Vec::new();
166 let mut modified_lines = Vec::new();
167
168 let max_lines = old_lines.len().max(new_lines.len());
170
171 for i in 0..max_lines {
172 let old_line = old_lines.get(i).copied();
173 let new_line = new_lines.get(i).copied();
174
175 match (old_line, new_line) {
176 (Some(old), Some(new)) if old != new => {
177 modified_lines.push((
179 DiffLine {
180 line_number: i + 1,
181 content: old.to_string(),
182 },
183 DiffLine {
184 line_number: i + 1,
185 content: new.to_string(),
186 },
187 ));
188 }
189 (Some(old), None) => {
190 removed_lines.push(DiffLine {
192 line_number: i + 1,
193 content: old.to_string(),
194 });
195 }
196 (None, Some(new)) => {
197 added_lines.push(DiffLine {
199 line_number: i + 1,
200 content: new.to_string(),
201 });
202 }
203 (Some(_), Some(_)) => {
204 }
206 (None, None) => {
207 }
209 }
210 }
211
212 let total_changes = added_lines.len() + removed_lines.len() + modified_lines.len();
213
214 Ok(FileDiff {
215 added_lines,
216 removed_lines,
217 modified_lines,
218 total_changes,
219 })
220 }
221
222 pub fn is_identical(&self, old_content: &str, new_content: &str) -> bool {
231 old_content == new_content
232 }
233
234 pub fn summarize_diff(&self, diff: &FileDiff) -> String {
242 format!(
243 "{} added, {} removed, {} modified",
244 diff.added_lines.len(),
245 diff.removed_lines.len(),
246 diff.modified_lines.len()
247 )
248 }
249}
250
251impl Default for ConflictDetector {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use tempfile::TempDir;
261
262 #[test]
263 fn test_create_conflict_detector() {
264 let _detector = ConflictDetector::new();
265 }
266
267 #[test]
268 fn test_detect_no_conflicts() {
269 let temp_dir = TempDir::new().unwrap();
270 let detector = ConflictDetector::new();
271
272 let files = vec![GeneratedFile {
273 path: "src/main.rs".to_string(),
274 content: "fn main() {}".to_string(),
275 language: "rust".to_string(),
276 }];
277
278 let conflicts = detector.detect(&files, temp_dir.path()).unwrap();
279 assert_eq!(conflicts.len(), 0);
280 }
281
282 #[test]
283 fn test_detect_single_conflict() {
284 let temp_dir = TempDir::new().unwrap();
285 let detector = ConflictDetector::new();
286
287 let file_path = temp_dir.path().join("src").join("main.rs");
289 fs::create_dir_all(file_path.parent().unwrap()).unwrap();
290 fs::write(&file_path, "// old content").unwrap();
291
292 let files = vec![GeneratedFile {
293 path: "src/main.rs".to_string(),
294 content: "// new content".to_string(),
295 language: "rust".to_string(),
296 }];
297
298 let conflicts = detector.detect(&files, temp_dir.path()).unwrap();
299 assert_eq!(conflicts.len(), 1);
300 assert_eq!(conflicts[0].old_content, "// old content");
301 assert_eq!(conflicts[0].new_content, "// new content");
302 }
303
304 #[test]
305 fn test_detect_multiple_conflicts() {
306 let temp_dir = TempDir::new().unwrap();
307 let detector = ConflictDetector::new();
308
309 let file1_path = temp_dir.path().join("src").join("main.rs");
311 fs::create_dir_all(file1_path.parent().unwrap()).unwrap();
312 fs::write(&file1_path, "// old main").unwrap();
313
314 let file2_path = temp_dir.path().join("src").join("lib.rs");
315 fs::write(&file2_path, "// old lib").unwrap();
316
317 let files = vec![
318 GeneratedFile {
319 path: "src/main.rs".to_string(),
320 content: "// new main".to_string(),
321 language: "rust".to_string(),
322 },
323 GeneratedFile {
324 path: "src/lib.rs".to_string(),
325 content: "// new lib".to_string(),
326 language: "rust".to_string(),
327 },
328 ];
329
330 let conflicts = detector.detect(&files, temp_dir.path()).unwrap();
331 assert_eq!(conflicts.len(), 2);
332 }
333
334 #[test]
335 fn test_compute_diff_identical() {
336 let detector = ConflictDetector::new();
337 let old = "line 1\nline 2\nline 3";
338 let new = "line 1\nline 2\nline 3";
339
340 let diff = detector.compute_diff(old, new).unwrap();
341 assert_eq!(diff.added_lines.len(), 0);
342 assert_eq!(diff.removed_lines.len(), 0);
343 assert_eq!(diff.modified_lines.len(), 0);
344 }
345
346 #[test]
347 fn test_compute_diff_added_lines() {
348 let detector = ConflictDetector::new();
349 let old = "line 1\nline 2";
350 let new = "line 1\nline 2\nline 3\nline 4";
351
352 let diff = detector.compute_diff(old, new).unwrap();
353 assert_eq!(diff.added_lines.len(), 2);
354 assert_eq!(diff.removed_lines.len(), 0);
355 assert_eq!(diff.modified_lines.len(), 0);
356 }
357
358 #[test]
359 fn test_compute_diff_removed_lines() {
360 let detector = ConflictDetector::new();
361 let old = "line 1\nline 2\nline 3\nline 4";
362 let new = "line 1\nline 2";
363
364 let diff = detector.compute_diff(old, new).unwrap();
365 assert_eq!(diff.added_lines.len(), 0);
366 assert_eq!(diff.removed_lines.len(), 2);
367 assert_eq!(diff.modified_lines.len(), 0);
368 }
369
370 #[test]
371 fn test_compute_diff_modified_lines() {
372 let detector = ConflictDetector::new();
373 let old = "line 1\nline 2\nline 3";
374 let new = "line 1\nmodified line 2\nline 3";
375
376 let diff = detector.compute_diff(old, new).unwrap();
377 assert_eq!(diff.added_lines.len(), 0);
378 assert_eq!(diff.removed_lines.len(), 0);
379 assert_eq!(diff.modified_lines.len(), 1);
380 }
381
382 #[test]
383 fn test_is_identical_true() {
384 let detector = ConflictDetector::new();
385 let content = "line 1\nline 2\nline 3";
386 assert!(detector.is_identical(content, content));
387 }
388
389 #[test]
390 fn test_is_identical_false() {
391 let detector = ConflictDetector::new();
392 let old = "line 1\nline 2";
393 let new = "line 1\nline 2\nline 3";
394 assert!(!detector.is_identical(old, new));
395 }
396
397 #[test]
398 fn test_summarize_diff() {
399 let detector = ConflictDetector::new();
400 let diff = FileDiff {
401 added_lines: vec![DiffLine {
402 line_number: 1,
403 content: "added".to_string(),
404 }],
405 removed_lines: vec![DiffLine {
406 line_number: 2,
407 content: "removed".to_string(),
408 }],
409 modified_lines: vec![(
410 DiffLine {
411 line_number: 3,
412 content: "old".to_string(),
413 },
414 DiffLine {
415 line_number: 3,
416 content: "new".to_string(),
417 },
418 )],
419 total_changes: 3,
420 };
421
422 let summary = detector.summarize_diff(&diff);
423 assert!(summary.contains("1 added"));
424 assert!(summary.contains("1 removed"));
425 assert!(summary.contains("1 modified"));
426 }
427
428 #[test]
429 fn test_detect_single_no_conflict() {
430 let temp_dir = TempDir::new().unwrap();
431 let detector = ConflictDetector::new();
432
433 let file_path = temp_dir.path().join("nonexistent.rs");
434 let result = detector.detect_single(&file_path, "content").unwrap();
435 assert!(result.is_none());
436 }
437
438 #[test]
439 fn test_detect_single_with_conflict() {
440 let temp_dir = TempDir::new().unwrap();
441 let detector = ConflictDetector::new();
442
443 let file_path = temp_dir.path().join("existing.rs");
444 fs::write(&file_path, "old content").unwrap();
445
446 let result = detector.detect_single(&file_path, "new content").unwrap();
447 assert!(result.is_some());
448
449 let conflict = result.unwrap();
450 assert_eq!(conflict.old_content, "old content");
451 assert_eq!(conflict.new_content, "new content");
452 }
453}