codemod_core/transform/
rollback.rs1use std::fs;
8use std::path::{Path, PathBuf};
9
10use serde::{Deserialize, Serialize};
11
12use crate::error::CodemodError;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct RollbackEntry {
21 pub path: PathBuf,
23 pub timestamp: String,
25 pub file_count: usize,
27 pub description: String,
29}
30
31#[derive(Debug, Serialize, Deserialize)]
33struct RollbackData {
34 timestamp: String,
35 description: String,
36 files: Vec<RollbackFile>,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40struct RollbackFile {
41 path: PathBuf,
42 original_content: String,
43}
44
45pub struct RollbackManager {
53 rollback_dir: PathBuf,
54}
55
56impl RollbackManager {
57 pub fn new(project_root: &Path) -> crate::Result<Self> {
62 let rollback_dir = project_root.join(".codemod-pilot").join("rollback");
63 Ok(Self { rollback_dir })
64 }
65
66 pub fn save_rollback(&self, results: &[super::TransformResult]) -> crate::Result<PathBuf> {
70 fs::create_dir_all(&self.rollback_dir)?;
72
73 let now = chrono::Utc::now();
74 let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
75 let iso_timestamp = now.to_rfc3339();
76
77 let data = RollbackData {
78 timestamp: iso_timestamp.clone(),
79 description: format!(
80 "Rollback for {} file(s) transformed at {}",
81 results.len(),
82 iso_timestamp
83 ),
84 files: results
85 .iter()
86 .map(|r| RollbackFile {
87 path: r.file_path.clone(),
88 original_content: r.original_content.clone(),
89 })
90 .collect(),
91 };
92
93 let filename = format!("rollback_{timestamp}.json");
94 let file_path = self.rollback_dir.join(&filename);
95
96 let json = serde_json::to_string_pretty(&data).map_err(|e| {
97 CodemodError::Transform(format!("Failed to serialize rollback data: {e}"))
98 })?;
99
100 fs::write(&file_path, json)?;
101
102 log::info!("Saved rollback to {}", file_path.display());
103 Ok(file_path)
104 }
105
106 pub fn apply_rollback(&self, patch_path: &Path) -> crate::Result<usize> {
111 let content = fs::read_to_string(patch_path)?;
112 let data: RollbackData = serde_json::from_str(&content)
113 .map_err(|e| CodemodError::Transform(format!("Failed to parse rollback file: {e}")))?;
114
115 let mut restored = 0usize;
116 for file in &data.files {
117 if file.path.exists() {
118 fs::write(&file.path, &file.original_content)?;
119 restored += 1;
120 log::info!("Restored {}", file.path.display());
121 } else {
122 log::warn!("Skipping {} — file does not exist", file.path.display());
123 }
124 }
125
126 Ok(restored)
127 }
128
129 pub fn list_rollbacks(&self) -> crate::Result<Vec<RollbackEntry>> {
131 if !self.rollback_dir.exists() {
132 return Ok(Vec::new());
133 }
134
135 let mut entries = Vec::new();
136 for entry in fs::read_dir(&self.rollback_dir)? {
137 let entry = entry?;
138 let path = entry.path();
139
140 if path.extension().and_then(|e| e.to_str()) != Some("json") {
141 continue;
142 }
143
144 match self.read_rollback_entry(&path) {
145 Ok(re) => entries.push(re),
146 Err(e) => {
147 log::warn!("Skipping malformed rollback file {}: {e}", path.display());
148 }
149 }
150 }
151
152 entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
154
155 Ok(entries)
156 }
157
158 fn read_rollback_entry(&self, path: &Path) -> crate::Result<RollbackEntry> {
164 let content = fs::read_to_string(path)?;
165 let data: RollbackData = serde_json::from_str(&content)
166 .map_err(|e| CodemodError::Transform(format!("Failed to parse rollback file: {e}")))?;
167
168 Ok(RollbackEntry {
169 path: path.to_path_buf(),
170 timestamp: data.timestamp,
171 file_count: data.files.len(),
172 description: data.description,
173 })
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::transform::TransformResult;
181
182 #[test]
183 fn test_rollback_roundtrip() {
184 let tmp = std::env::temp_dir().join("codemod_test_rollback");
185 let _ = fs::remove_dir_all(&tmp);
186 fs::create_dir_all(&tmp).unwrap();
187
188 let test_file = tmp.join("test.rs");
190 fs::write(&test_file, "original content").unwrap();
191
192 let manager = RollbackManager::new(&tmp).unwrap();
193
194 let results = vec![TransformResult {
195 file_path: test_file.clone(),
196 match_count: 1,
197 applied_count: 1,
198 diff: String::new(),
199 original_content: "original content".into(),
200 new_content: "new content".into(),
201 }];
202
203 fs::write(&test_file, "new content").unwrap();
205
206 let patch_path = manager.save_rollback(&results).unwrap();
208 assert!(patch_path.exists());
209
210 let entries = manager.list_rollbacks().unwrap();
212 assert_eq!(entries.len(), 1);
213 assert_eq!(entries[0].file_count, 1);
214
215 let restored = manager.apply_rollback(&patch_path).unwrap();
217 assert_eq!(restored, 1);
218 assert_eq!(fs::read_to_string(&test_file).unwrap(), "original content");
219
220 let _ = fs::remove_dir_all(&tmp);
222 }
223}