Skip to main content

claude_utils/turbo/
rollback.rs

1use 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    /// Create a new checkpoint
59    pub async fn create_checkpoint(&self, description: String) -> crate::Result<String> {
60        let checkpoint_id = format!("{:x}", rand::thread_rng().gen::<u128>());
61        
62        // Try to create git stash
63        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        // Keep only max_checkpoints
90        while checkpoints.len() > self.max_checkpoints {
91            checkpoints.pop_back();
92        }
93        
94        // Save to disk
95        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    /// Record an operation for rollback
104    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    /// Rollback to the last checkpoint
113    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            // Save updated list to disk
119            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    /// Rollback to a specific checkpoint
139    async fn rollback_to_checkpoint(&self, checkpoint: &Checkpoint) -> crate::Result<()> {
140        info!("🔄 Rolling back {} operations...", checkpoint.operations.len());
141
142        // First try git stash pop if available
143        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        // Manual rollback of operations
160        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                    // Delete the created file
170                    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                    // Commands can't be rolled back directly
181                    info!("⚠️  Cannot rollback command: {}", operation.target);
182                }
183            }
184        }
185
186        Ok(())
187    }
188
189    /// Get list of available checkpoints
190    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}