watchdiff_tui/export/
mod.rs

1//! Export functionality for saving diffs and patches
2//!
3//! This module provides functionality to export diffs in various formats
4//! to files or other outputs.
5
6use std::fs;
7use std::io::Write;
8use std::path::Path;
9use anyhow::Result;
10use crate::diff::{DiffResult, DiffFormatter, DiffFormat};
11use crate::core::FileEvent;
12
13/// Export configuration
14#[derive(Debug, Clone)]
15pub struct ExportConfig {
16    pub format: DiffFormat,
17    pub include_stats: bool,
18    pub include_metadata: bool,
19    pub width: Option<usize>, // For side-by-side format
20}
21
22impl Default for ExportConfig {
23    fn default() -> Self {
24        Self {
25            format: DiffFormat::Unified,
26            include_stats: true,
27            include_metadata: true,
28            width: Some(120),
29        }
30    }
31}
32
33/// Handles exporting diffs to various formats and destinations
34pub struct DiffExporter {
35    config: ExportConfig,
36}
37
38impl DiffExporter {
39    pub fn new(config: ExportConfig) -> Self {
40        Self { config }
41    }
42    
43    pub fn with_format(format: DiffFormat) -> Self {
44        Self {
45            config: ExportConfig {
46                format,
47                ..Default::default()
48            }
49        }
50    }
51    
52    /// Export a single diff result to a file
53    pub fn export_diff<P: AsRef<Path>>(
54        &self,
55        result: &DiffResult,
56        old_path: &Path,
57        new_path: &Path,
58        output_path: P,
59    ) -> Result<()> {
60        let mut content = String::new();
61        
62        // Add metadata if requested
63        if self.config.include_metadata {
64            content.push_str(&self.format_metadata(old_path, new_path));
65            content.push_str("\n\n");
66        }
67        
68        // Add stats if requested  
69        if self.config.include_stats {
70            content.push_str(&format!("Changes: {}\n\n", DiffFormatter::format_stats(result)));
71        }
72        
73        // Add the diff content
74        content.push_str(&DiffFormatter::format(
75            result,
76            self.config.format,
77            old_path,
78            new_path,
79            self.config.width,
80        ));
81        
82        fs::write(output_path.as_ref(), content)?;
83        Ok(())
84    }
85    
86    /// Export multiple file events as a single patch
87    pub fn export_multifile_patch<P: AsRef<Path>>(
88        &self,
89        events: &[FileEvent],
90        output_path: P,
91    ) -> Result<()> {
92        let mut content = String::new();
93        
94        // Add header
95        if self.config.include_metadata {
96            content.push_str(&format!(
97                "Multi-file patch containing {} files\n",
98                events.len()
99            ));
100            content.push_str(&format!(
101                "Generated at: {}\n\n",
102                chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
103            ));
104        }
105        
106        // Process each file event
107        for (i, event) in events.iter().enumerate() {
108            if i > 0 {
109                content.push_str("\n\n");
110            }
111            
112            content.push_str(&self.format_file_event(event));
113        }
114        
115        fs::write(output_path.as_ref(), content)?;
116        Ok(())
117    }
118    
119    /// Export to a writer (for streaming or custom outputs)
120    pub fn export_diff_to_writer<W: Write>(
121        &self,
122        result: &DiffResult,
123        old_path: &Path,
124        new_path: &Path,
125        writer: &mut W,
126    ) -> Result<()> {
127        if self.config.include_metadata {
128            writeln!(writer, "{}", self.format_metadata(old_path, new_path))?;
129            writeln!(writer)?;
130        }
131        
132        if self.config.include_stats {
133            writeln!(writer, "Changes: {}", DiffFormatter::format_stats(result))?;
134            writeln!(writer)?;
135        }
136        
137        write!(writer, "{}", DiffFormatter::format(
138            result,
139            self.config.format,
140            old_path,
141            new_path,
142            self.config.width,
143        ))?;
144        
145        Ok(())
146    }
147    
148    /// Create a patch bundle (tar/zip) with multiple patches
149    pub fn create_patch_bundle<P: AsRef<Path>>(
150        &self,
151        events: &[FileEvent],
152        bundle_path: P,
153    ) -> Result<()> {
154        // For now, just create a directory with individual patch files
155        let bundle_dir = bundle_path.as_ref();
156        fs::create_dir_all(bundle_dir)?;
157        
158        for (i, event) in events.iter().enumerate() {
159            let filename = format!("{:03}_{}.patch", 
160                i + 1, 
161                event.path.file_name()
162                    .and_then(|s| s.to_str())
163                    .unwrap_or("unknown")
164            );
165            
166            let patch_path = bundle_dir.join(filename);
167            let patch_content = self.format_file_event(event);
168            fs::write(patch_path, patch_content)?;
169        }
170        
171        // Write a manifest file
172        let manifest_content = self.create_manifest(events);
173        fs::write(bundle_dir.join("manifest.txt"), manifest_content)?;
174        
175        Ok(())
176    }
177    
178    fn format_metadata(&self, old_path: &Path, new_path: &Path) -> String {
179        format!(
180            "Diff between {} and {}\nGenerated at: {}",
181            old_path.display(),
182            new_path.display(),
183            chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
184        )
185    }
186    
187    fn format_file_event(&self, event: &FileEvent) -> String {
188        let mut content = String::new();
189        
190        // Add event metadata
191        content.push_str(&format!("File: {}\n", event.path.display()));
192        content.push_str(&format!("Event: {:?}\n", event.kind));
193        content.push_str(&format!("Timestamp: {}\n", 
194            chrono::DateTime::<chrono::Utc>::from(event.timestamp)
195                .format("%Y-%m-%d %H:%M:%S UTC")
196        ));
197        
198        // Add diff if available
199        if let Some(ref diff) = event.diff {
200            content.push_str("\n");
201            content.push_str(diff);
202        }
203        
204        content
205    }
206    
207    fn create_manifest(&self, events: &[FileEvent]) -> String {
208        let mut content = String::new();
209        
210        content.push_str(&format!("Patch Bundle Manifest\n"));
211        content.push_str(&format!("Generated at: {}\n", 
212            chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
213        ));
214        content.push_str(&format!("Total files: {}\n\n", events.len()));
215        
216        for (i, event) in events.iter().enumerate() {
217            content.push_str(&format!(
218                "{:03}. {} ({:?})\n",
219                i + 1,
220                event.path.display(),
221                event.kind
222            ));
223        }
224        
225        content
226    }
227}
228
229/// Predefined export presets
230impl DiffExporter {
231    /// Create an exporter for Git-style patches
232    pub fn git_patch() -> Self {
233        Self::with_format(DiffFormat::GitPatch)
234    }
235    
236    /// Create an exporter for unified diffs
237    pub fn unified() -> Self {
238        Self::with_format(DiffFormat::Unified)
239    }
240    
241    /// Create an exporter for side-by-side comparison
242    pub fn side_by_side(width: usize) -> Self {
243        Self::new(ExportConfig {
244            format: DiffFormat::SideBySide,
245            width: Some(width),
246            ..Default::default()
247        })
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use tempfile::TempDir;
255    use crate::diff::{DiffGenerator, DiffAlgorithmType};
256    use crate::core::events::FileEventKind;
257    use std::time::SystemTime;
258
259    #[test]
260    fn test_export_diff() {
261        let temp_dir = TempDir::new().unwrap();
262        let output_path = temp_dir.path().join("test.patch");
263        
264        let generator = DiffGenerator::new(DiffAlgorithmType::Myers);
265        let result = generator.generate("old\nline", "new\nline");
266        
267        let exporter = DiffExporter::unified();
268        exporter.export_diff(&result, 
269            Path::new("old.txt"), 
270            Path::new("new.txt"), 
271            &output_path
272        ).unwrap();
273        
274        let content = fs::read_to_string(output_path).unwrap();
275        assert!(content.contains("--- old.txt"));
276        assert!(content.contains("+++ new.txt"));
277    }
278    
279    #[test] 
280    fn test_export_multifile_patch() {
281        let temp_dir = TempDir::new().unwrap();
282        let output_path = temp_dir.path().join("multi.patch");
283        
284        let event = FileEvent {
285            path: Path::new("test.txt").to_path_buf(),
286            kind: FileEventKind::Modified,
287            timestamp: SystemTime::now(),
288            diff: Some("--- a\n+++ b\n@@ -1 +1 @@\n-old\n+new".to_string()),
289            content_preview: None,
290        };
291        
292        let exporter = DiffExporter::unified();
293        exporter.export_multifile_patch(&[event], &output_path).unwrap();
294        
295        let content = fs::read_to_string(output_path).unwrap();
296        assert!(content.contains("Multi-file patch"));
297        assert!(content.contains("test.txt"));
298    }
299}