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