1use crate::config;
14use crate::queue;
15use crate::undo;
16use anyhow::Result;
17use clap::Args;
18
19#[derive(Args, Debug)]
20pub struct UndoArgs {
21 #[arg(long, short)]
23 pub id: Option<String>,
24
25 #[arg(long)]
27 pub list: bool,
28
29 #[arg(long)]
31 pub dry_run: bool,
32
33 #[arg(long, short)]
35 pub verbose: bool,
36}
37
38pub fn handle(args: UndoArgs, force: bool) -> Result<()> {
40 let resolved = config::resolve_from_cwd()?;
41
42 if args.list {
43 return handle_list(&resolved);
44 }
45
46 let _lock = queue::acquire_queue_lock(&resolved.repo_root, "undo", force)?;
47
48 let result = undo::restore_from_snapshot(&resolved, args.id.as_deref(), args.dry_run)?;
49
50 if args.dry_run {
51 println!("Dry run - would restore from snapshot:");
52 } else {
53 println!("Restored from snapshot:");
54 }
55
56 println!(" Operation: {}", result.operation);
57 println!(" Timestamp: {}", result.timestamp);
58 println!(" Tasks affected: {}", result.tasks_affected);
59
60 if args.verbose && !args.dry_run {
61 println!("\nRun `ralph queue list` to see the restored queue state.");
62 }
63
64 Ok(())
65}
66
67fn handle_list(resolved: &config::Resolved) -> Result<()> {
68 let list = undo::list_undo_snapshots(&resolved.repo_root)?;
69
70 if list.snapshots.is_empty() {
71 println!("No undo snapshots available.");
72 println!("\nSnapshots are created automatically before queue mutations such as:");
73 println!(" - ralph task done/reject");
74 println!(" - ralph queue archive");
75 println!(" - ralph queue prune");
76 println!(" - ralph task batch operations");
77 println!(" - ralph task edit");
78 return Ok(());
79 }
80
81 println!("Available undo snapshots (newest first):\n");
82
83 for (i, snap) in list.snapshots.iter().enumerate() {
84 let num = i + 1;
85 println!(" {}. {} [{}]", num, snap.operation, snap.timestamp);
86 println!(" ID: {}", snap.id);
87 }
88
89 println!("\nTo restore the most recent: ralph undo");
90 println!("To restore a specific one: ralph undo --id <ID>");
91 println!("To preview without applying: ralph undo --dry-run");
92
93 Ok(())
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::contracts::{QueueFile, Task, TaskStatus};
100 use std::collections::HashMap;
101 use tempfile::TempDir;
102
103 fn create_test_resolved(temp_dir: &TempDir) -> config::Resolved {
104 let repo_root = temp_dir.path();
105 let ralph_dir = repo_root.join(".ralph");
106 std::fs::create_dir_all(&ralph_dir).unwrap();
107
108 let queue_path = ralph_dir.join("queue.json");
109 let done_path = ralph_dir.join("done.json");
110
111 let queue = QueueFile {
113 version: 1,
114 tasks: vec![Task {
115 id: "RQ-0001".to_string(),
116 title: "Test task".to_string(),
117 status: TaskStatus::Todo,
118 description: None,
119 priority: Default::default(),
120 tags: vec!["test".to_string()],
121 scope: vec!["crates/ralph".to_string()],
122 evidence: vec!["observed".to_string()],
123 plan: vec!["do thing".to_string()],
124 notes: vec![],
125 request: Some("test request".to_string()),
126 agent: None,
127 created_at: Some("2026-01-18T00:00:00Z".to_string()),
128 updated_at: Some("2026-01-18T00:00:00Z".to_string()),
129 completed_at: None,
130 started_at: None,
131 scheduled_start: None,
132 estimated_minutes: None,
133 actual_minutes: None,
134 depends_on: vec![],
135 blocks: vec![],
136 relates_to: vec![],
137 duplicates: None,
138 custom_fields: HashMap::new(),
139 parent_id: None,
140 }],
141 };
142
143 queue::save_queue(&queue_path, &queue).unwrap();
144
145 config::Resolved {
146 config: crate::contracts::Config::default(),
147 repo_root: repo_root.to_path_buf(),
148 queue_path,
149 done_path,
150 id_prefix: "RQ".to_string(),
151 id_width: 4,
152 global_config_path: None,
153 project_config_path: None,
154 }
155 }
156
157 #[test]
158 fn handle_list_shows_snapshots() {
159 let temp = TempDir::new().unwrap();
160 let resolved = create_test_resolved(&temp);
161
162 undo::create_undo_snapshot(&resolved, "test operation").unwrap();
164
165 let result = handle_list(&resolved);
167 assert!(result.is_ok());
168 }
169
170 #[test]
171 fn handle_list_empty_shows_helpful_message() {
172 let temp = TempDir::new().unwrap();
173 let resolved = create_test_resolved(&temp);
174
175 let result = handle_list(&resolved);
177 assert!(result.is_ok());
178 }
179}