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