1use 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#[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>, }
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
33pub 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 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 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 if self.config.include_stats {
70 content.push_str(&format!("Changes: {}\n\n", DiffFormatter::format_stats(result)));
71 }
72
73 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 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 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 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 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 pub fn create_patch_bundle<P: AsRef<Path>>(
150 &self,
151 events: &[FileEvent],
152 bundle_path: P,
153 ) -> Result<()> {
154 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 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 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 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
229impl DiffExporter {
231 pub fn git_patch() -> Self {
233 Self::with_format(DiffFormat::GitPatch)
234 }
235
236 pub fn unified() -> Self {
238 Self::with_format(DiffFormat::Unified)
239 }
240
241 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}