Skip to main content

lit/commands/
undo.rs

1use crate::core::find_repo_root;
2use crate::errors::LitError;
3use crate::response::CommandResponse;
4use serde::{Deserialize, Serialize};
5
6/// Operation log entry for undo timeline
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct OpLogEntry {
9    pub id: u64,
10    pub timestamp: i64,
11    pub operation: String,
12    pub description: String,
13    /// Snapshot hash before the operation (for reverting)
14    pub before_snapshot: String,
15    /// Snapshot hash after the operation
16    pub after_snapshot: String,
17    /// Whether this entry has been undone
18    pub undone: bool,
19}
20
21#[derive(Debug, Serialize, Deserialize)]
22pub struct OpLogResponse {
23    pub action: String,
24    pub message: String,
25    pub entries: Option<Vec<OpLogEntry>>,
26    pub undone_entry: Option<OpLogEntry>,
27}
28
29impl CommandResponse for OpLogResponse {
30    fn command_name(&self) -> &'static str {
31        "undo"
32    }
33    fn human_readable(&self) -> String {
34        let mut out = format!("{}\n", self.message);
35        if let Some(ref entries) = self.entries {
36            for entry in entries {
37                let status = if entry.undone { " (undone)" } else { "" };
38                let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(entry.timestamp, 0)
39                    .map(|d| d.format("%Y-%m-%d %H:%M:%S").to_string())
40                    .unwrap_or_else(|| "unknown".to_string());
41                out.push_str(&format!(
42                    "  {:>4}  {}  {}{}\n",
43                    entry.id, dt, entry.description, status
44                ));
45            }
46        }
47        if let Some(ref entry) = self.undone_entry {
48            out.push_str(&format!("  Reverted: {}\n", entry.description));
49        }
50        out
51    }
52}
53
54/// Path to the operation log file
55fn oplog_path(repo_root: &std::path::Path) -> std::path::PathBuf {
56    repo_root.join(".lit").join("oplog.json")
57}
58
59/// Load the operation log
60fn load_oplog(repo_root: &std::path::Path) -> Vec<OpLogEntry> {
61    let path = oplog_path(repo_root);
62    if path.exists() {
63        match std::fs::read_to_string(&path) {
64            Ok(data) => serde_json::from_str(&data).unwrap_or_default(),
65            Err(_) => Vec::new(),
66        }
67    } else {
68        Vec::new()
69    }
70}
71
72/// Save the operation log
73fn save_oplog(repo_root: &std::path::Path, entries: &[OpLogEntry]) -> Result<(), LitError> {
74    let path = oplog_path(repo_root);
75    let data = serde_json::to_string_pretty(entries)
76        .map_err(|e| LitError::general(format!("Failed to serialize oplog: {}", e)))?;
77    std::fs::write(path, data)
78        .map_err(|e| LitError::io(format!("Failed to write oplog: {}", e)))?;
79    Ok(())
80}
81
82/// Record an operation in the log (called by other commands)
83pub fn record_operation(
84    repo_root: &std::path::Path,
85    operation: &str,
86    description: &str,
87    before_snapshot: &str,
88    after_snapshot: &str,
89) -> Result<(), LitError> {
90    let mut entries = load_oplog(repo_root);
91    let id = entries.last().map(|e| e.id + 1).unwrap_or(1);
92    entries.push(OpLogEntry {
93        id,
94        timestamp: chrono::Utc::now().timestamp(),
95        operation: operation.to_string(),
96        description: description.to_string(),
97        before_snapshot: before_snapshot.to_string(),
98        after_snapshot: after_snapshot.to_string(),
99        undone: false,
100    });
101    save_oplog(repo_root, &entries)?;
102    Ok(())
103}
104
105/// List the operation log
106pub fn execute_list(count: usize) -> Result<OpLogResponse, LitError> {
107    let repo_root = find_repo_root()?;
108    let entries = load_oplog(&repo_root);
109    let shown: Vec<OpLogEntry> = entries.into_iter().rev().take(count).collect();
110
111    Ok(OpLogResponse {
112        action: "list".into(),
113        message: format!("Showing {} operation(s)", shown.len()),
114        entries: Some(shown),
115        undone_entry: None,
116    })
117}
118
119/// Undo the last operation (or a specific operation by ID)
120pub fn execute_undo(target_id: Option<u64>) -> Result<OpLogResponse, LitError> {
121    let repo_root = find_repo_root()?;
122    let mut entries = load_oplog(&repo_root);
123
124    if entries.is_empty() {
125        return Err(LitError::general("No operations to undo"));
126    }
127
128    let target = if let Some(id) = target_id {
129        entries
130            .iter_mut()
131            .find(|e| e.id == id && !e.undone)
132            .ok_or_else(|| {
133                LitError::general(format!("Operation {} not found or already undone", id))
134            })?
135    } else {
136        entries
137            .iter_mut()
138            .rev()
139            .find(|e| !e.undone)
140            .ok_or_else(|| LitError::general("No operations to undo"))?
141    };
142
143    // Restore the before-snapshot state
144    let before = target.before_snapshot.clone();
145    let description = target.description.clone();
146    target.undone = true;
147    let undone_entry = target.clone();
148
149    // Restore HEAD to the before-snapshot state
150    let branch = crate::core::get_current_branch(&repo_root).unwrap_or_else(|_| "main".to_string());
151    if !before.is_empty() {
152        crate::core::write_ref(&repo_root, &format!("heads/{}", branch), &before)?;
153    }
154
155    save_oplog(&repo_root, &entries)?;
156
157    Ok(OpLogResponse {
158        action: "undo".into(),
159        message: format!("Undone: {}", description),
160        entries: None,
161        undone_entry: Some(undone_entry),
162    })
163}
164
165/// Redo a previously undone operation
166pub fn execute_redo(target_id: Option<u64>) -> Result<OpLogResponse, LitError> {
167    let repo_root = find_repo_root()?;
168    let mut entries = load_oplog(&repo_root);
169
170    let target = if let Some(id) = target_id {
171        entries
172            .iter_mut()
173            .find(|e| e.id == id && e.undone)
174            .ok_or_else(|| LitError::general(format!("Operation {} not found or not undone", id)))?
175    } else {
176        entries
177            .iter_mut()
178            .rev()
179            .find(|e| e.undone)
180            .ok_or_else(|| LitError::general("No undone operations to redo"))?
181    };
182
183    let after = target.after_snapshot.clone();
184    let description = target.description.clone();
185    target.undone = false;
186    let redone_entry = target.clone();
187
188    let branch = crate::core::get_current_branch(&repo_root).unwrap_or_else(|_| "main".to_string());
189    if !after.is_empty() {
190        crate::core::write_ref(&repo_root, &format!("heads/{}", branch), &after)?;
191    }
192
193    save_oplog(&repo_root, &entries)?;
194
195    Ok(OpLogResponse {
196        action: "redo".into(),
197        message: format!("Redone: {}", description),
198        entries: None,
199        undone_entry: Some(redone_entry),
200    })
201}