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