1use std::{
2 fs,
3 path::{Path, PathBuf},
4};
5
6use chrono::Local;
7use doing_error::{Error, Result};
8
9use crate::backup::{self, backup_prefix};
10
11pub fn redo(source: &Path, backup_dir: &Path, count: usize) -> Result<()> {
17 let undone = backup::list_undone(source, backup_dir)?;
18 let count = if count == 0 { 1 } else { count };
19
20 if count > undone.len() {
21 return Err(Error::HistoryLimit("end of redo history".into()));
22 }
23
24 fs::copy(&undone[count - 1], source)?;
25 unconsume_all(source, backup_dir)?;
26 Ok(())
27}
28
29pub fn undo(source: &Path, backup_dir: &Path, count: usize) -> Result<()> {
37 let backups = backup::list_backups(source, backup_dir)?;
38 if count == 0 || count > backups.len() {
39 return Err(Error::HistoryLimit("end of undo history".into()));
40 }
41
42 create_undone(source, backup_dir)?;
43 fs::copy(&backups[count - 1], source)?;
44
45 for backup in &backups[..count] {
46 consume(backup)?;
47 }
48
49 Ok(())
50}
51
52fn consume(path: &Path) -> Result<()> {
54 let undone = path.with_extension("undone");
55 fs::rename(path, undone)?;
56 Ok(())
57}
58
59fn create_undone(source: &Path, backup_dir: &Path) -> Result<PathBuf> {
64 fs::create_dir_all(backup_dir)?;
65
66 let prefix = backup_prefix(source);
67 let timestamp = Local::now().format("%Y%m%d_%H%M%S_%6f");
68 let name = format!("{prefix}{timestamp}.undone");
69 let path = backup_dir.join(name);
70
71 fs::copy(source, &path)?;
72 Ok(path)
73}
74
75fn unconsume_all(source: &Path, backup_dir: &Path) -> Result<()> {
77 for undone in backup::list_undone(source, backup_dir)? {
78 let bak = undone.with_extension("bak");
79 fs::rename(undone, bak)?;
80 }
81 Ok(())
82}
83
84#[cfg(test)]
85mod test {
86 use std::fs;
87
88 use super::*;
89
90 mod redo {
91 use pretty_assertions::assert_eq;
92
93 use super::*;
94
95 #[test]
96 fn it_converts_all_undone_files_back_to_bak() {
97 let dir = tempfile::tempdir().unwrap();
98 let source = dir.path().join("doing.md");
99 let backup_dir = dir.path().join("backups");
100 fs::create_dir_all(&backup_dir).unwrap();
101 fs::write(&source, "current").unwrap();
102
103 let prefix = backup_prefix(&source);
104 fs::write(backup_dir.join(format!("{prefix}20240101_000002.undone")), "newer").unwrap();
105 fs::write(backup_dir.join(format!("{prefix}20240101_000001.undone")), "older").unwrap();
106
107 redo(&source, &backup_dir, 1).unwrap();
108
109 let undone = backup::list_undone(&source, &backup_dir).unwrap();
110 assert!(undone.is_empty());
111
112 let bak = backup::list_backups(&source, &backup_dir).unwrap();
113 assert_eq!(bak.len(), 2);
114 }
115
116 #[test]
117 fn it_restores_from_newest_undone_file() {
118 let dir = tempfile::tempdir().unwrap();
119 let source = dir.path().join("doing.md");
120 let backup_dir = dir.path().join("backups");
121 fs::create_dir_all(&backup_dir).unwrap();
122 fs::write(&source, "current").unwrap();
123
124 let prefix = backup_prefix(&source);
125 fs::write(
126 backup_dir.join(format!("{prefix}20240101_000001.undone")),
127 "older undone",
128 )
129 .unwrap();
130 fs::write(
131 backup_dir.join(format!("{prefix}20240101_000002.undone")),
132 "newest undone",
133 )
134 .unwrap();
135
136 redo(&source, &backup_dir, 1).unwrap();
137
138 assert_eq!(fs::read_to_string(&source).unwrap(), "newest undone");
139 }
140
141 #[test]
142 fn it_returns_error_when_no_undone_files() {
143 let dir = tempfile::tempdir().unwrap();
144 let source = dir.path().join("doing.md");
145 let backup_dir = dir.path().join("backups");
146 fs::create_dir_all(&backup_dir).unwrap();
147
148 let result = redo(&source, &backup_dir, 1);
149
150 assert!(result.is_err());
151 assert!(result.unwrap_err().to_string().contains("redo history"));
152 }
153 }
154
155 mod undo {
156 use pretty_assertions::assert_eq;
157
158 use super::*;
159
160 #[test]
161 fn it_consumes_backup_after_restoring() {
162 let dir = tempfile::tempdir().unwrap();
163 let source = dir.path().join("doing.md");
164 let backup_dir = dir.path().join("backups");
165 fs::create_dir_all(&backup_dir).unwrap();
166 fs::write(&source, "current state").unwrap();
167
168 let prefix = backup_prefix(&source);
169 fs::write(backup_dir.join(format!("{prefix}20240101_000001.bak")), "backup1").unwrap();
170
171 undo(&source, &backup_dir, 1).unwrap();
172
173 let remaining_bak = backup::list_backups(&source, &backup_dir).unwrap();
174 assert!(remaining_bak.is_empty());
175
176 let undone = backup::list_undone(&source, &backup_dir).unwrap();
177 assert_eq!(undone.len(), 2);
178 }
179
180 #[test]
181 fn it_creates_undone_snapshot_of_current_state() {
182 let dir = tempfile::tempdir().unwrap();
183 let source = dir.path().join("doing.md");
184 let backup_dir = dir.path().join("backups");
185 fs::create_dir_all(&backup_dir).unwrap();
186 fs::write(&source, "current state").unwrap();
187
188 let prefix = backup_prefix(&source);
189 fs::write(backup_dir.join(format!("{prefix}20240101_000001.bak")), "backup1").unwrap();
190
191 undo(&source, &backup_dir, 1).unwrap();
192
193 let undone = backup::list_undone(&source, &backup_dir).unwrap();
194 let newest_undone = &undone[0];
195 assert_eq!(fs::read_to_string(newest_undone).unwrap(), "current state");
196 }
197
198 #[test]
199 fn it_restores_from_most_recent_by_default() {
200 let dir = tempfile::tempdir().unwrap();
201 let source = dir.path().join("doing.md");
202 let backup_dir = dir.path().join("backups");
203 fs::create_dir_all(&backup_dir).unwrap();
204 fs::write(&source, "current").unwrap();
205
206 let prefix = backup_prefix(&source);
207 fs::write(backup_dir.join(format!("{prefix}20240101_000001.bak")), "oldest").unwrap();
208 fs::write(backup_dir.join(format!("{prefix}20240101_000002.bak")), "newest").unwrap();
209
210 undo(&source, &backup_dir, 1).unwrap();
211
212 assert_eq!(fs::read_to_string(&source).unwrap(), "newest");
213 }
214
215 #[test]
216 fn it_restores_from_nth_backup() {
217 let dir = tempfile::tempdir().unwrap();
218 let source = dir.path().join("doing.md");
219 let backup_dir = dir.path().join("backups");
220 fs::create_dir_all(&backup_dir).unwrap();
221 fs::write(&source, "current").unwrap();
222
223 let prefix = backup_prefix(&source);
224 fs::write(backup_dir.join(format!("{prefix}20240101_000001.bak")), "oldest").unwrap();
225 fs::write(backup_dir.join(format!("{prefix}20240101_000002.bak")), "middle").unwrap();
226 fs::write(backup_dir.join(format!("{prefix}20240101_000003.bak")), "newest").unwrap();
227
228 undo(&source, &backup_dir, 2).unwrap();
229
230 assert_eq!(fs::read_to_string(&source).unwrap(), "middle");
231 }
232
233 #[test]
234 fn it_returns_error_when_count_exceeds_history() {
235 let dir = tempfile::tempdir().unwrap();
236 let source = dir.path().join("doing.md");
237 let backup_dir = dir.path().join("backups");
238 fs::create_dir_all(&backup_dir).unwrap();
239 fs::write(&source, "current").unwrap();
240
241 let prefix = backup_prefix(&source);
242 fs::write(backup_dir.join(format!("{prefix}20240101_000001.bak")), "backup").unwrap();
243
244 let result = undo(&source, &backup_dir, 5);
245
246 assert!(result.is_err());
247 assert!(result.unwrap_err().to_string().contains("undo history"));
248 }
249
250 #[test]
251 fn it_returns_error_when_count_is_zero() {
252 let dir = tempfile::tempdir().unwrap();
253 let source = dir.path().join("doing.md");
254 let backup_dir = dir.path().join("backups");
255 fs::create_dir_all(&backup_dir).unwrap();
256 fs::write(&source, "current").unwrap();
257
258 let prefix = backup_prefix(&source);
259 fs::write(backup_dir.join(format!("{prefix}20240101_000001.bak")), "backup").unwrap();
260
261 let result = undo(&source, &backup_dir, 0);
262
263 assert!(result.is_err());
264 }
265
266 #[test]
267 fn it_walks_backwards_on_sequential_calls() {
268 let dir = tempfile::tempdir().unwrap();
269 let source = dir.path().join("doing.md");
270 let backup_dir = dir.path().join("backups");
271 fs::create_dir_all(&backup_dir).unwrap();
272 fs::write(&source, "current").unwrap();
273
274 let prefix = backup_prefix(&source);
275 fs::write(backup_dir.join(format!("{prefix}20240101_000001.bak")), "oldest").unwrap();
276 fs::write(backup_dir.join(format!("{prefix}20240101_000002.bak")), "middle").unwrap();
277 fs::write(backup_dir.join(format!("{prefix}20240101_000003.bak")), "newest").unwrap();
278
279 undo(&source, &backup_dir, 1).unwrap();
280 assert_eq!(fs::read_to_string(&source).unwrap(), "newest");
281
282 undo(&source, &backup_dir, 1).unwrap();
283 assert_eq!(fs::read_to_string(&source).unwrap(), "middle");
284
285 undo(&source, &backup_dir, 1).unwrap();
286 assert_eq!(fs::read_to_string(&source).unwrap(), "oldest");
287
288 let result = undo(&source, &backup_dir, 1);
289 assert!(result.is_err());
290 }
291 }
292}