ricecoder_generation/
conflict_resolver.rs

1//! Conflict resolution strategies for generated files
2//!
3//! Implements multiple strategies for handling file conflicts:
4//! - Skip: Don't write conflicting files
5//! - Overwrite: Write and backup original
6//! - Merge: Attempt intelligent merge
7//! - Prompt: Ask user for each conflict
8//!
9//! Implements requirements:
10//! - Requirement 4.2: Skip strategy - don't write conflicting files
11//! - Requirement 4.3: Overwrite strategy - write and backup original
12//! - Requirement 4.4: Merge strategy - attempt intelligent merge
13//! - Requirement 4.5: Prompt strategy - ask user for each conflict
14
15use crate::conflict_detector::FileConflictInfo;
16use crate::error::GenerationError;
17use std::fs;
18use std::path::Path;
19
20/// Strategy for resolving file conflicts
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ConflictStrategy {
23    /// Skip the conflicting file (don't write)
24    Skip,
25    /// Overwrite the existing file (with backup)
26    Overwrite,
27    /// Attempt to merge changes
28    Merge,
29    /// Prompt user for each conflict
30    Prompt,
31}
32
33/// Result of applying a conflict resolution strategy
34#[derive(Debug, Clone)]
35pub struct ConflictResolutionResult {
36    /// Whether the file was written
37    pub written: bool,
38    /// Path to backup file if created
39    pub backup_path: Option<String>,
40    /// Resolution action taken
41    pub action: String,
42}
43
44/// Resolves file conflicts using specified strategy
45///
46/// Implements requirements:
47/// - Requirement 4.2: Skip strategy
48/// - Requirement 4.3: Overwrite strategy with backup
49/// - Requirement 4.4: Merge strategy
50/// - Requirement 4.5: Prompt strategy
51pub struct ConflictResolver;
52
53impl ConflictResolver {
54    /// Create a new conflict resolver
55    pub fn new() -> Self {
56        Self
57    }
58
59    /// Resolve a single conflict using the specified strategy
60    ///
61    /// # Arguments
62    /// * `conflict` - Conflict information
63    /// * `strategy` - Resolution strategy to apply
64    /// * `new_content` - New content to write
65    ///
66    /// # Returns
67    /// Resolution result
68    ///
69    /// # Requirements
70    /// - Requirement 4.2: Skip strategy
71    /// - Requirement 4.3: Overwrite strategy with backup
72    /// - Requirement 4.4: Merge strategy
73    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                // Prompt strategy is handled by the caller
85                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    /// Resolve conflict using skip strategy
95    ///
96    /// Does not write the file and continues with remaining files.
97    ///
98    /// # Requirements
99    /// - Requirement 4.2: Skip strategy - don't write conflicting files
100    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    /// Resolve conflict using overwrite strategy
112    ///
113    /// Writes the generated file and creates a backup of the original.
114    ///
115    /// # Arguments
116    /// * `conflict` - Conflict information
117    /// * `new_content` - New content to write
118    ///
119    /// # Returns
120    /// Resolution result with backup path
121    ///
122    /// # Requirements
123    /// - Requirement 4.3: Overwrite strategy - write and backup original
124    fn resolve_overwrite(
125        &self,
126        conflict: &FileConflictInfo,
127        new_content: &str,
128    ) -> Result<ConflictResolutionResult, GenerationError> {
129        // Create backup of original file
130        let backup_path = self.create_backup(&conflict.path)?;
131
132        // Write new content
133        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    /// Resolve conflict using merge strategy
151    ///
152    /// Attempts to merge changes intelligently. For now, this is a simple merge
153    /// that preserves both old and new content with markers.
154    ///
155    /// # Arguments
156    /// * `conflict` - Conflict information
157    /// * `new_content` - New content to merge
158    ///
159    /// # Returns
160    /// Resolution result
161    ///
162    /// # Requirements
163    /// - Requirement 4.4: Merge strategy - attempt intelligent merge
164    fn resolve_merge(
165        &self,
166        conflict: &FileConflictInfo,
167        new_content: &str,
168    ) -> Result<ConflictResolutionResult, GenerationError> {
169        // Create backup of original file
170        let backup_path = self.create_backup(&conflict.path)?;
171
172        // Perform simple merge: mark conflict regions
173        let merged_content = self.merge_contents(&conflict.old_content, new_content)?;
174
175        // Write merged content
176        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    /// Create a backup of a file
196    ///
197    /// Creates a backup with .bak extension.
198    ///
199    /// # Arguments
200    /// * `file_path` - Path to file to backup
201    ///
202    /// # Returns
203    /// Path to backup file
204    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        // Read original content
209        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        // Write backup
217        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    /// Merge two file contents
227    ///
228    /// Simple merge that marks conflict regions with markers.
229    ///
230    /// # Arguments
231    /// * `old_content` - Original content
232    /// * `new_content` - New content
233    ///
234    /// # Returns
235    /// Merged content
236    fn merge_contents(
237        &self,
238        old_content: &str,
239        new_content: &str,
240    ) -> Result<String, GenerationError> {
241        // Simple merge: if contents are identical, return as-is
242        if old_content == new_content {
243            return Ok(new_content.to_string());
244        }
245
246        // Otherwise, create a marked merge with both versions
247        let merged = format!(
248            "<<<<<<< ORIGINAL\n{}\n=======\n{}\n>>>>>>> GENERATED\n",
249            old_content, new_content
250        );
251
252        Ok(merged)
253    }
254
255    /// Check if a conflict can be auto-merged
256    ///
257    /// Returns true if the conflict can be automatically resolved without user intervention.
258    ///
259    /// # Arguments
260    /// * `conflict` - Conflict to check
261    ///
262    /// # Returns
263    /// True if auto-mergeable
264    pub fn is_auto_mergeable(&self, conflict: &FileConflictInfo) -> bool {
265        // A conflict is auto-mergeable if:
266        // 1. The files are identical (no actual conflict)
267        // 2. The new content is a superset of the old content (only additions)
268
269        if conflict.old_content == conflict.new_content {
270            return true;
271        }
272
273        // Check if new content contains all lines from old content
274        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    /// Get a human-readable description of a strategy
283    ///
284    /// # Arguments
285    /// * `strategy` - Strategy to describe
286    ///
287    /// # Returns
288    /// Description string
289    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        // Verify file was written
361        let content = fs::read_to_string(&file_path).unwrap();
362        assert_eq!(content, "new content");
363
364        // Verify backup was created
365        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        // Verify merged content contains conflict markers
388        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}