claude_utils/turbo/
rollback.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::VecDeque;
4use std::path::PathBuf;
5use tokio::fs;
6use tokio::process::Command;
7use tracing::info;
8use rand::Rng;
9
10use super::checkpoint_store::CheckpointStore;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Checkpoint {
14 pub id: String,
15 pub timestamp: DateTime<Utc>,
16 pub description: String,
17 pub git_stash: Option<String>,
18 pub operations: Vec<Operation>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Operation {
23 pub op_type: OperationType,
24 pub target: String,
25 pub backup_path: Option<PathBuf>,
26 pub original_content: Option<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub enum OperationType {
31 FileEdit,
32 FileCreate,
33 FileDelete,
34 CommandExecute,
35}
36
37pub struct RollbackManager {
38 checkpoints: tokio::sync::RwLock<VecDeque<Checkpoint>>,
39 max_checkpoints: usize,
40 store: CheckpointStore,
41}
42
43impl RollbackManager {
44 pub async fn new() -> crate::Result<Self> {
45 let store = CheckpointStore::new();
46 let checkpoints = store.load_checkpoints().await.unwrap_or_else(|e| {
47 info!("Failed to load checkpoints: {}, starting fresh", e);
48 VecDeque::new()
49 });
50
51 Ok(Self {
52 checkpoints: tokio::sync::RwLock::new(checkpoints),
53 max_checkpoints: 10,
54 store,
55 })
56 }
57
58 pub async fn create_checkpoint(&self, description: String) -> crate::Result<String> {
60 let checkpoint_id = format!("{:x}", rand::thread_rng().gen::<u128>());
61
62 let git_stash = match Command::new("git")
64 .args(["stash", "push", "-m", &format!("turbo-checkpoint-{checkpoint_id}")])
65 .output()
66 .await
67 {
68 Ok(output) if output.status.success() => {
69 let stash_ref = String::from_utf8_lossy(&output.stdout);
70 Some(stash_ref.trim().to_string())
71 }
72 _ => {
73 info!("Git stash failed, using file backups only");
74 None
75 }
76 };
77
78 let checkpoint = Checkpoint {
79 id: checkpoint_id.clone(),
80 timestamp: Utc::now(),
81 description,
82 git_stash,
83 operations: Vec::new(),
84 };
85
86 let mut checkpoints = self.checkpoints.write().await;
87 checkpoints.push_front(checkpoint);
88
89 while checkpoints.len() > self.max_checkpoints {
91 checkpoints.pop_back();
92 }
93
94 if let Err(e) = self.store.save_checkpoints(&checkpoints).await {
96 info!("Failed to save checkpoints to disk: {}", e);
97 }
98
99 info!("✅ Created checkpoint: {}", checkpoint_id);
100 Ok(checkpoint_id)
101 }
102
103 pub async fn record_operation(&self, operation: Operation) -> crate::Result<()> {
105 let mut checkpoints = self.checkpoints.write().await;
106 if let Some(current) = checkpoints.front_mut() {
107 current.operations.push(operation);
108 }
109 Ok(())
110 }
111
112 pub async fn rollback_last(&self) -> crate::Result<()> {
114 let checkpoint = {
115 let mut checkpoints = self.checkpoints.write().await;
116 let cp = checkpoints.pop_front();
117
118 if cp.is_some() {
120 if let Err(e) = self.store.save_checkpoints(&checkpoints).await {
121 info!("Failed to save checkpoints after rollback: {}", e);
122 }
123 }
124
125 cp
126 };
127
128 if let Some(checkpoint) = checkpoint {
129 self.rollback_to_checkpoint(&checkpoint).await?;
130 info!("✅ Rolled back to checkpoint: {}", checkpoint.id);
131 } else {
132 return Err(crate::ClaudeUtilsError::Turbo("No checkpoints available".into()));
133 }
134
135 Ok(())
136 }
137
138 async fn rollback_to_checkpoint(&self, checkpoint: &Checkpoint) -> crate::Result<()> {
140 info!("🔄 Rolling back {} operations...", checkpoint.operations.len());
141
142 if let Some(ref stash_ref) = checkpoint.git_stash {
144 match Command::new("git")
145 .args(["stash", "pop", stash_ref])
146 .output()
147 .await
148 {
149 Ok(output) if output.status.success() => {
150 info!("✅ Git stash restored successfully");
151 return Ok(());
152 }
153 _ => {
154 info!("Git stash restore failed, falling back to manual rollback");
155 }
156 }
157 }
158
159 for operation in checkpoint.operations.iter().rev() {
161 match operation.op_type {
162 OperationType::FileEdit => {
163 if let Some(ref content) = operation.original_content {
164 fs::write(&operation.target, content).await?;
165 info!("Restored: {}", operation.target);
166 }
167 }
168 OperationType::FileCreate => {
169 let _ = fs::remove_file(&operation.target).await;
171 info!("Removed: {}", operation.target);
172 }
173 OperationType::FileDelete => {
174 if let Some(ref backup_path) = operation.backup_path {
175 fs::copy(backup_path, &operation.target).await?;
176 info!("Restored: {}", operation.target);
177 }
178 }
179 OperationType::CommandExecute => {
180 info!("⚠️ Cannot rollback command: {}", operation.target);
182 }
183 }
184 }
185
186 Ok(())
187 }
188
189 pub async fn list_checkpoints(&self) -> Vec<(String, String, DateTime<Utc>)> {
191 self.checkpoints
192 .read()
193 .await
194 .iter()
195 .map(|cp| (cp.id.clone(), cp.description.clone(), cp.timestamp))
196 .collect()
197 }
198}