ricecoder_files/
conflict.rs

1//! Conflict detection and resolution for file operations
2
3use crate::error::FileError;
4use crate::models::{ConflictInfo, ConflictResolution};
5use std::path::Path;
6use tokio::fs;
7
8/// Detects and resolves file conflicts when target path already exists
9#[derive(Debug, Clone)]
10pub struct ConflictResolver;
11
12impl ConflictResolver {
13    /// Creates a new ConflictResolver instance
14    pub fn new() -> Self {
15        ConflictResolver
16    }
17
18    /// Detects conflicts by checking if target file exists
19    ///
20    /// # Arguments
21    ///
22    /// * `path` - Path to check for conflicts
23    ///
24    /// # Returns
25    ///
26    /// Ok(Some(ConflictInfo)) if conflict exists, Ok(None) if no conflict,
27    /// or an error if the check fails
28    pub async fn detect_conflict(
29        &self,
30        path: &Path,
31        new_content: &str,
32    ) -> Result<Option<ConflictInfo>, FileError> {
33        if !path.exists() {
34            return Ok(None);
35        }
36
37        let existing_content = fs::read_to_string(path)
38            .await
39            .map_err(|_e| FileError::ConflictDetected(path.to_path_buf()))?;
40
41        Ok(Some(ConflictInfo {
42            path: path.to_path_buf(),
43            existing_content,
44            new_content: new_content.to_string(),
45        }))
46    }
47
48    /// Resolves a conflict using the specified strategy
49    ///
50    /// # Arguments
51    ///
52    /// * `strategy` - The resolution strategy to use
53    /// * `conflict_info` - Information about the conflict
54    ///
55    /// # Returns
56    ///
57    /// Ok(()) if resolution succeeds, or an error if it fails
58    pub fn resolve(
59        &self,
60        strategy: ConflictResolution,
61        conflict_info: &ConflictInfo,
62    ) -> Result<(), FileError> {
63        match strategy {
64            ConflictResolution::Skip => {
65                Err(FileError::ConflictDetected(conflict_info.path.clone()))
66            }
67            ConflictResolution::Overwrite => {
68                // Overwrite is allowed; no error
69                Ok(())
70            }
71            ConflictResolution::Merge => {
72                // Merge strategy: combine both versions
73                // For now, we'll implement a simple merge that keeps both
74                Ok(())
75            }
76        }
77    }
78
79    /// Performs a simple merge of two content versions
80    ///
81    /// # Arguments
82    ///
83    /// * `existing` - The existing content
84    /// * `new` - The new content
85    ///
86    /// # Returns
87    ///
88    /// Merged content
89    pub fn merge_content(existing: &str, new: &str) -> String {
90        // Simple merge: if content is identical, return it
91        if existing == new {
92            return new.to_string();
93        }
94
95        // Otherwise, combine with markers
96        format!(
97            "<<<<<<< EXISTING\n{}\n=======\n{}\n>>>>>>> NEW",
98            existing, new
99        )
100    }
101}
102
103impl Default for ConflictResolver {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[tokio::test]
114    async fn test_detect_conflict_no_file() {
115        let resolver = ConflictResolver::new();
116        let temp_dir = tempfile::tempdir().unwrap();
117        let path = temp_dir.path().join("nonexistent.txt");
118
119        let result = resolver.detect_conflict(&path, "new content").await;
120        assert!(result.is_ok());
121        assert!(result.unwrap().is_none());
122    }
123
124    #[tokio::test]
125    async fn test_detect_conflict_file_exists() {
126        let resolver = ConflictResolver::new();
127        let temp_dir = tempfile::tempdir().unwrap();
128        let path = temp_dir.path().join("existing.txt");
129
130        fs::write(&path, "existing content").await.unwrap();
131
132        let result = resolver.detect_conflict(&path, "new content").await;
133        assert!(result.is_ok());
134
135        let conflict = result.unwrap();
136        assert!(conflict.is_some());
137
138        let conflict_info = conflict.unwrap();
139        assert_eq!(conflict_info.path, path);
140        assert_eq!(conflict_info.existing_content, "existing content");
141        assert_eq!(conflict_info.new_content, "new content");
142    }
143
144    #[test]
145    fn test_resolve_skip_strategy() {
146        let resolver = ConflictResolver::new();
147        let conflict_info = ConflictInfo {
148            path: "test.txt".into(),
149            existing_content: "existing".to_string(),
150            new_content: "new".to_string(),
151        };
152
153        let result = resolver.resolve(ConflictResolution::Skip, &conflict_info);
154        assert!(result.is_err());
155        match result {
156            Err(FileError::ConflictDetected(_)) => (),
157            _ => panic!("Expected ConflictDetected error"),
158        }
159    }
160
161    #[test]
162    fn test_resolve_overwrite_strategy() {
163        let resolver = ConflictResolver::new();
164        let conflict_info = ConflictInfo {
165            path: "test.txt".into(),
166            existing_content: "existing".to_string(),
167            new_content: "new".to_string(),
168        };
169
170        let result = resolver.resolve(ConflictResolution::Overwrite, &conflict_info);
171        assert!(result.is_ok());
172    }
173
174    #[test]
175    fn test_resolve_merge_strategy() {
176        let resolver = ConflictResolver::new();
177        let conflict_info = ConflictInfo {
178            path: "test.txt".into(),
179            existing_content: "existing".to_string(),
180            new_content: "new".to_string(),
181        };
182
183        let result = resolver.resolve(ConflictResolution::Merge, &conflict_info);
184        assert!(result.is_ok());
185    }
186
187    #[test]
188    fn test_merge_content_identical() {
189        let content = "same content";
190        let merged = ConflictResolver::merge_content(content, content);
191        assert_eq!(merged, content);
192    }
193
194    #[test]
195    fn test_merge_content_different() {
196        let existing = "existing content";
197        let new = "new content";
198        let merged = ConflictResolver::merge_content(existing, new);
199
200        assert!(merged.contains("<<<<<<< EXISTING"));
201        assert!(merged.contains("======="));
202        assert!(merged.contains(">>>>>>> NEW"));
203        assert!(merged.contains(existing));
204        assert!(merged.contains(new));
205    }
206}