1use anyhow::{Result, anyhow};
5use oplog::OpBatch;
6use repo::Repository;
7use serde::Serialize;
8
9use super::{
10 undo_apply::{apply_redo_batch, apply_undo_batch},
11 worktree_safety::ensure_worktree_clean,
12};
13use crate::cli::{Cli, should_output_json};
14
15#[derive(Serialize)]
16struct OpListOutput {
17 batches: Vec<OpBatchOutput>,
18}
19
20#[derive(Serialize)]
21struct OpBatchOutput {
22 batch_id: u64,
23 timestamp: String,
24 undone: bool,
25 partial: bool,
26 operations: Vec<OpListEntry>,
27}
28
29#[derive(Serialize)]
30struct OpListEntry {
31 id: u64,
32 description: String,
33 timestamp: String,
34 undone: bool,
35}
36
37#[derive(Serialize)]
38struct UndoRedoOutput {
39 action: String,
40 message: String,
41 batches: Vec<OpBatchOutput>,
42}
43
44pub fn cmd_undo(cli: &Cli, steps: usize, list: bool, depth: usize, preview: bool) -> Result<()> {
45 let repo = Repository::open(cli.repo.as_ref().unwrap_or(&std::env::current_dir()?))?;
46
47 if list && preview {
48 return Err(anyhow!("Use either --list or --preview, not both"));
49 }
50
51 if list {
52 let scope = repo.op_scope();
53 let batches = repo.oplog().recent_batches_scoped(depth, Some(&scope))?;
54 let output = OpListOutput {
55 batches: batches.iter().map(build_batch_output).collect(),
56 };
57
58 if should_output_json(cli, Some(repo.config())) {
59 println!("{}", serde_json::to_string(&output)?);
60 } else {
61 println!("Recent operation batches (showing up to {}):", depth);
62 if output.batches.is_empty() {
63 println!(" No operations");
64 } else {
65 print_batches(&output.batches);
66 }
67 }
68
69 return Ok(());
70 }
71
72 let scope = repo.op_scope();
73 let batches = repo.oplog().undo_batches_scoped(steps, Some(&scope))?;
74
75 if batches.is_empty() {
76 return Err(anyhow!("Nothing to undo"));
77 }
78
79 if preview {
80 let output = UndoRedoOutput {
81 action: "undo".to_string(),
82 message: format!(
83 "Would undo {} batch{}",
84 batches.len(),
85 if batches.len() == 1 { "" } else { "es" }
86 ),
87 batches: batches.iter().map(build_batch_output).collect(),
88 };
89
90 if should_output_json(cli, Some(repo.config())) {
91 println!("{}", serde_json::to_string(&output)?);
92 } else {
93 println!("{}", output.message);
94 print_batches(&output.batches);
95 }
96
97 return Ok(());
98 }
99
100 ensure_worktree_clean(&repo, "undo")?;
101
102 let mut updated_batches = Vec::with_capacity(batches.len());
103 for batch in batches {
104 apply_undo_batch(&repo, &batch)?;
105 updated_batches.push(repo.oplog().mark_batch_undone(&batch)?);
106 }
107
108 let output = UndoRedoOutput {
109 action: "undo".to_string(),
110 message: format!(
111 "Undone {} batch{}",
112 updated_batches.len(),
113 if updated_batches.len() == 1 { "" } else { "es" }
114 ),
115 batches: updated_batches.iter().map(build_batch_output).collect(),
116 };
117
118 if should_output_json(cli, Some(repo.config())) {
119 println!("{}", serde_json::to_string(&output)?);
120 } else {
121 println!("{}", output.message);
122 print_batches(&output.batches);
123 print_head(&repo)?;
124 }
125
126 Ok(())
127}
128
129pub fn cmd_redo(cli: &Cli, steps: usize, preview: bool) -> Result<()> {
130 let repo = Repository::open(cli.repo.as_ref().unwrap_or(&std::env::current_dir()?))?;
131
132 let scope = repo.op_scope();
133 let batches = repo.oplog().redo_batches_scoped(steps, Some(&scope))?;
134
135 if batches.is_empty() {
136 return Err(anyhow!("Nothing to redo"));
137 }
138
139 if preview {
140 let output = UndoRedoOutput {
141 action: "redo".to_string(),
142 message: format!(
143 "Would redo {} batch{}",
144 batches.len(),
145 if batches.len() == 1 { "" } else { "es" }
146 ),
147 batches: batches.iter().map(build_batch_output).collect(),
148 };
149
150 if should_output_json(cli, Some(repo.config())) {
151 println!("{}", serde_json::to_string(&output)?);
152 } else {
153 println!("{}", output.message);
154 print_batches(&output.batches);
155 }
156
157 return Ok(());
158 }
159
160 ensure_worktree_clean(&repo, "redo")?;
161
162 let mut updated_batches = Vec::with_capacity(batches.len());
163 for batch in batches {
164 apply_redo_batch(&repo, &batch)?;
165 updated_batches.push(repo.oplog().mark_batch_redone(&batch)?);
166 }
167
168 let output = UndoRedoOutput {
169 action: "redo".to_string(),
170 message: format!(
171 "Redone {} batch{}",
172 updated_batches.len(),
173 if updated_batches.len() == 1 { "" } else { "es" }
174 ),
175 batches: updated_batches.iter().map(build_batch_output).collect(),
176 };
177
178 if should_output_json(cli, Some(repo.config())) {
179 println!("{}", serde_json::to_string(&output)?);
180 } else {
181 println!("{}", output.message);
182 print_batches(&output.batches);
183 print_head(&repo)?;
184 }
185
186 Ok(())
187}
188
189fn build_batch_output(batch: &OpBatch) -> OpBatchOutput {
190 let (undone, partial) = batch_status(batch);
191 let timestamp = batch
192 .entries
193 .iter()
194 .map(|entry| entry.timestamp)
195 .max()
196 .map(format_timestamp)
197 .unwrap_or_else(|| "unknown".to_string());
198
199 OpBatchOutput {
200 batch_id: batch.id,
201 timestamp,
202 undone,
203 partial,
204 operations: batch
205 .entries
206 .iter()
207 .map(|entry| OpListEntry {
208 id: entry.id,
209 description: entry.operation.description(),
210 timestamp: format_timestamp(entry.timestamp),
211 undone: entry.undone,
212 })
213 .collect(),
214 }
215}
216
217fn batch_status(batch: &OpBatch) -> (bool, bool) {
218 let any_undone = batch.entries.iter().any(|entry| entry.undone);
219 let all_undone = batch.entries.iter().all(|entry| entry.undone);
220 (all_undone, any_undone && !all_undone)
221}
222
223fn format_timestamp(timestamp: chrono::DateTime<chrono::Utc>) -> String {
224 timestamp.format("%Y-%m-%d %H:%M:%S").to_string()
225}
226
227fn print_batches(batches: &[OpBatchOutput]) {
228 for batch in batches {
229 let status = if batch.undone {
230 " (undone)"
231 } else if batch.partial {
232 " (partial)"
233 } else {
234 ""
235 };
236 let op_count = batch.operations.len();
237 println!(
238 " Batch {}{} {} op{}",
239 batch.batch_id,
240 status,
241 op_count,
242 if op_count == 1 { "" } else { "s" }
243 );
244 for entry in &batch.operations {
245 let entry_status = if entry.undone { " (undone)" } else { "" };
246 println!(
247 " {} {} {}{}",
248 entry.id, entry.timestamp, entry.description, entry_status
249 );
250 }
251 }
252}
253
254fn print_head(repo: &Repository) -> Result<()> {
255 if let Some(id) = repo.head()? {
256 println!("Now at: {}", id.short());
257 }
258 Ok(())
259}