1use crate::conflict_detector::FileConflictInfo;
16use crate::error::GenerationError;
17use std::fs;
18use std::path::Path;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ConflictStrategy {
23 Skip,
25 Overwrite,
27 Merge,
29 Prompt,
31}
32
33#[derive(Debug, Clone)]
35pub struct ConflictResolutionResult {
36 pub written: bool,
38 pub backup_path: Option<String>,
40 pub action: String,
42}
43
44pub struct ConflictResolver;
52
53impl ConflictResolver {
54 pub fn new() -> Self {
56 Self
57 }
58
59 pub fn resolve(
74 &self,
75 conflict: &FileConflictInfo,
76 strategy: ConflictStrategy,
77 new_content: &str,
78 ) -> Result<ConflictResolutionResult, GenerationError> {
79 match strategy {
80 ConflictStrategy::Skip => self.resolve_skip(conflict),
81 ConflictStrategy::Overwrite => self.resolve_overwrite(conflict, new_content),
82 ConflictStrategy::Merge => self.resolve_merge(conflict, new_content),
83 ConflictStrategy::Prompt => {
84 Err(GenerationError::ValidationError {
86 file: conflict.path.to_string_lossy().to_string(),
87 line: 0,
88 message: "Prompt strategy requires user interaction".to_string(),
89 })
90 }
91 }
92 }
93
94 fn resolve_skip(
101 &self,
102 conflict: &FileConflictInfo,
103 ) -> Result<ConflictResolutionResult, GenerationError> {
104 Ok(ConflictResolutionResult {
105 written: false,
106 backup_path: None,
107 action: format!("Skipped: {}", conflict.path.display()),
108 })
109 }
110
111 fn resolve_overwrite(
125 &self,
126 conflict: &FileConflictInfo,
127 new_content: &str,
128 ) -> Result<ConflictResolutionResult, GenerationError> {
129 let backup_path = self.create_backup(&conflict.path)?;
131
132 fs::write(&conflict.path, new_content).map_err(|e| GenerationError::ValidationError {
134 file: conflict.path.to_string_lossy().to_string(),
135 line: 0,
136 message: format!("Failed to write file: {}", e),
137 })?;
138
139 Ok(ConflictResolutionResult {
140 written: true,
141 backup_path: backup_path.clone(),
142 action: format!(
143 "Overwritten: {} (backup: {})",
144 conflict.path.display(),
145 backup_path.unwrap_or_default()
146 ),
147 })
148 }
149
150 fn resolve_merge(
165 &self,
166 conflict: &FileConflictInfo,
167 new_content: &str,
168 ) -> Result<ConflictResolutionResult, GenerationError> {
169 let backup_path = self.create_backup(&conflict.path)?;
171
172 let merged_content = self.merge_contents(&conflict.old_content, new_content)?;
174
175 fs::write(&conflict.path, &merged_content).map_err(|e| {
177 GenerationError::ValidationError {
178 file: conflict.path.to_string_lossy().to_string(),
179 line: 0,
180 message: format!("Failed to write merged file: {}", e),
181 }
182 })?;
183
184 Ok(ConflictResolutionResult {
185 written: true,
186 backup_path: backup_path.clone(),
187 action: format!(
188 "Merged: {} (backup: {}, conflicts marked)",
189 conflict.path.display(),
190 backup_path.unwrap_or_default()
191 ),
192 })
193 }
194
195 fn create_backup(&self, file_path: &Path) -> Result<Option<String>, GenerationError> {
205 let backup_path = format!("{}.bak", file_path.display());
206 let backup_path_obj = Path::new(&backup_path);
207
208 let content =
210 fs::read_to_string(file_path).map_err(|e| GenerationError::ValidationError {
211 file: file_path.to_string_lossy().to_string(),
212 line: 0,
213 message: format!("Failed to read file for backup: {}", e),
214 })?;
215
216 fs::write(backup_path_obj, content).map_err(|e| GenerationError::ValidationError {
218 file: file_path.to_string_lossy().to_string(),
219 line: 0,
220 message: format!("Failed to create backup: {}", e),
221 })?;
222
223 Ok(Some(backup_path))
224 }
225
226 fn merge_contents(
237 &self,
238 old_content: &str,
239 new_content: &str,
240 ) -> Result<String, GenerationError> {
241 if old_content == new_content {
243 return Ok(new_content.to_string());
244 }
245
246 let merged = format!(
248 "<<<<<<< ORIGINAL\n{}\n=======\n{}\n>>>>>>> GENERATED\n",
249 old_content, new_content
250 );
251
252 Ok(merged)
253 }
254
255 pub fn is_auto_mergeable(&self, conflict: &FileConflictInfo) -> bool {
265 if conflict.old_content == conflict.new_content {
270 return true;
271 }
272
273 let old_lines: Vec<&str> = conflict.old_content.lines().collect();
275 let new_content_lines = conflict.new_content.lines().collect::<Vec<_>>();
276
277 old_lines
278 .iter()
279 .all(|line| new_content_lines.contains(line))
280 }
281
282 pub fn describe_strategy(&self, strategy: ConflictStrategy) -> &'static str {
290 match strategy {
291 ConflictStrategy::Skip => "Skip conflicting files (don't write)",
292 ConflictStrategy::Overwrite => "Overwrite existing files (with backup)",
293 ConflictStrategy::Merge => "Merge changes (mark conflicts)",
294 ConflictStrategy::Prompt => "Prompt for each conflict",
295 }
296 }
297}
298
299impl Default for ConflictResolver {
300 fn default() -> Self {
301 Self::new()
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use std::path::PathBuf;
309 use tempfile::TempDir;
310
311 fn create_test_conflict(old_content: &str, new_content: &str) -> FileConflictInfo {
312 FileConflictInfo {
313 path: PathBuf::from("test.rs"),
314 old_content: old_content.to_string(),
315 new_content: new_content.to_string(),
316 diff: crate::conflict_detector::FileDiff {
317 added_lines: vec![],
318 removed_lines: vec![],
319 modified_lines: vec![],
320 total_changes: 0,
321 },
322 }
323 }
324
325 #[test]
326 fn test_create_resolver() {
327 let _resolver = ConflictResolver::new();
328 }
329
330 #[test]
331 fn test_resolve_skip() {
332 let resolver = ConflictResolver::new();
333 let conflict = create_test_conflict("old", "new");
334
335 let result = resolver
336 .resolve(&conflict, ConflictStrategy::Skip, "new")
337 .unwrap();
338 assert!(!result.written);
339 assert!(result.backup_path.is_none());
340 }
341
342 #[test]
343 fn test_resolve_overwrite() {
344 let temp_dir = TempDir::new().unwrap();
345 let resolver = ConflictResolver::new();
346
347 let file_path = temp_dir.path().join("test.rs");
348 fs::write(&file_path, "old content").unwrap();
349
350 let mut conflict = create_test_conflict("old content", "new content");
351 conflict.path = file_path.clone();
352
353 let result = resolver
354 .resolve(&conflict, ConflictStrategy::Overwrite, "new content")
355 .unwrap();
356
357 assert!(result.written);
358 assert!(result.backup_path.is_some());
359
360 let content = fs::read_to_string(&file_path).unwrap();
362 assert_eq!(content, "new content");
363
364 let backup_path = format!("{}.bak", file_path.display());
366 assert!(Path::new(&backup_path).exists());
367 }
368
369 #[test]
370 fn test_resolve_merge() {
371 let temp_dir = TempDir::new().unwrap();
372 let resolver = ConflictResolver::new();
373
374 let file_path = temp_dir.path().join("test.rs");
375 fs::write(&file_path, "old content").unwrap();
376
377 let mut conflict = create_test_conflict("old content", "new content");
378 conflict.path = file_path.clone();
379
380 let result = resolver
381 .resolve(&conflict, ConflictStrategy::Merge, "new content")
382 .unwrap();
383
384 assert!(result.written);
385 assert!(result.backup_path.is_some());
386
387 let content = fs::read_to_string(&file_path).unwrap();
389 assert!(content.contains("<<<<<<< ORIGINAL"));
390 assert!(content.contains("======="));
391 assert!(content.contains(">>>>>>> GENERATED"));
392 }
393
394 #[test]
395 fn test_is_auto_mergeable_identical() {
396 let resolver = ConflictResolver::new();
397 let conflict = create_test_conflict("content", "content");
398
399 assert!(resolver.is_auto_mergeable(&conflict));
400 }
401
402 #[test]
403 fn test_is_auto_mergeable_superset() {
404 let resolver = ConflictResolver::new();
405 let conflict = create_test_conflict("line 1\nline 2", "line 1\nline 2\nline 3");
406
407 assert!(resolver.is_auto_mergeable(&conflict));
408 }
409
410 #[test]
411 fn test_is_auto_mergeable_not_mergeable() {
412 let resolver = ConflictResolver::new();
413 let conflict = create_test_conflict("line 1\nline 2", "line 1\nmodified line 2");
414
415 assert!(!resolver.is_auto_mergeable(&conflict));
416 }
417
418 #[test]
419 fn test_describe_strategy_skip() {
420 let resolver = ConflictResolver::new();
421 let desc = resolver.describe_strategy(ConflictStrategy::Skip);
422 assert!(desc.contains("Skip"));
423 }
424
425 #[test]
426 fn test_describe_strategy_overwrite() {
427 let resolver = ConflictResolver::new();
428 let desc = resolver.describe_strategy(ConflictStrategy::Overwrite);
429 assert!(desc.contains("Overwrite"));
430 }
431
432 #[test]
433 fn test_describe_strategy_merge() {
434 let resolver = ConflictResolver::new();
435 let desc = resolver.describe_strategy(ConflictStrategy::Merge);
436 assert!(desc.contains("Merge"));
437 }
438
439 #[test]
440 fn test_describe_strategy_prompt() {
441 let resolver = ConflictResolver::new();
442 let desc = resolver.describe_strategy(ConflictStrategy::Prompt);
443 assert!(desc.contains("Prompt"));
444 }
445
446 #[test]
447 fn test_merge_contents_identical() {
448 let resolver = ConflictResolver::new();
449 let merged = resolver.merge_contents("content", "content").unwrap();
450 assert_eq!(merged, "content");
451 }
452
453 #[test]
454 fn test_merge_contents_different() {
455 let resolver = ConflictResolver::new();
456 let merged = resolver.merge_contents("old", "new").unwrap();
457 assert!(merged.contains("<<<<<<< ORIGINAL"));
458 assert!(merged.contains("old"));
459 assert!(merged.contains("======="));
460 assert!(merged.contains("new"));
461 assert!(merged.contains(">>>>>>> GENERATED"));
462 }
463
464 #[test]
465 fn test_create_backup() {
466 let temp_dir = TempDir::new().unwrap();
467 let resolver = ConflictResolver::new();
468
469 let file_path = temp_dir.path().join("test.rs");
470 fs::write(&file_path, "original content").unwrap();
471
472 let backup_path = resolver.create_backup(&file_path).unwrap();
473 assert!(backup_path.is_some());
474
475 let backup_path_str = backup_path.unwrap();
476 let backup_file = Path::new(&backup_path_str);
477 assert!(backup_file.exists());
478
479 let backup_content = fs::read_to_string(backup_file).unwrap();
480 assert_eq!(backup_content, "original content");
481 }
482}