1use crate::core::find_repo_root;
2use crate::errors::LitError;
3use crate::response::CommandResponse;
4use serde::{Deserialize, Serialize};
5
6#[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 pub before_snapshot: String,
15 pub after_snapshot: String,
17 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
54fn oplog_path(repo_root: &std::path::Path) -> std::path::PathBuf {
56 repo_root.join(".lit").join("oplog.json")
57}
58
59fn 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
72fn 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
82pub 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
105pub 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
119pub 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 let before = target.before_snapshot.clone();
145 let description = target.description.clone();
146 target.undone = true;
147 let undone_entry = target.clone();
148
149 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
165pub 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}