Skip to main content

cli/cli/commands/
undo.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Undo and redo commands.
3
4use 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}