devlog/
rollover.rs

1//! Rollover is an operation that copies incomplete
2//! tasks from the latest devlog entry file to a new devlog entry file.
3
4use crate::config::Config;
5use crate::error::Error;
6use crate::file::LogFile;
7use crate::hook::{execute_hook, HookType};
8use crate::path::LogPath;
9use crate::task::{Task, TaskStatus};
10use std::fs::OpenOptions;
11use std::io::Write;
12use std::path::Path;
13
14/// Copies incomplete tasks from the latest devlog entry file
15/// to a new devlog entry file with the next sequence number.
16/// If available, the before-rollover and after-rollover hooks are invoked.
17pub fn rollover<W: Write>(
18    w: &mut W,
19    config: &Config,
20    p: &LogPath,
21) -> Result<(LogPath, usize), Error> {
22    let path = p.path();
23    let next = p.next()?;
24    let next_path = next.path();
25
26    execute_hook(w, config, &HookType::BeforeRollover, &[path.as_os_str()])?;
27    let tasks = load_carryover_tasks(path)?;
28    create_new_logfile(&next_path, &tasks)?;
29    execute_hook(
30        w,
31        config,
32        &HookType::AfterRollover,
33        &[path.as_os_str(), next_path.as_os_str()],
34    )?;
35
36    Ok((next, tasks.len()))
37}
38
39fn load_carryover_tasks(path: &Path) -> Result<Vec<Task>, Error> {
40    let prev = LogFile::load(path)?;
41    let mut tasks = Vec::new();
42    prev.tasks().iter().for_each(|t| {
43        if let TaskStatus::ToDo | TaskStatus::Started | TaskStatus::Blocked = t.status() {
44            tasks.push(t.clone());
45        }
46    });
47    Ok(tasks)
48}
49
50fn create_new_logfile(next_path: &Path, tasks: &[Task]) -> Result<(), Error> {
51    let mut f = OpenOptions::new()
52        .write(true)
53        .create_new(true)
54        .open(next_path)?;
55
56    for t in tasks {
57        write!(f, "{}\n", t)?;
58    }
59
60    Ok(())
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::repository::LogRepository;
67    use tempfile::tempdir;
68
69    #[test]
70    fn test_rollover() {
71        let mut out = Vec::new();
72        let dir = tempdir().unwrap();
73        let repo = LogRepository::new(dir.path());
74        let config = Config::new(dir.path(), "");
75
76        // Initialize the repository, which creates a single
77        // logfile with three example tasks.
78        repo.init().unwrap();
79        let first_logpath = repo.latest().unwrap().unwrap();
80
81        // Rollover, then check that only todo/started/blocked tasks
82        // were imported into the new logfile
83        let (new_logpath, num_imported) = rollover(&mut out, &config, &first_logpath).unwrap();
84        assert_eq!(num_imported, 3);
85
86        // Check tasks in the new logfile
87        let logfile = LogFile::load(new_logpath.path()).unwrap();
88        let task_statuses: Vec<TaskStatus> = logfile.tasks().iter().map(|t| t.status()).collect();
89        assert_eq!(
90            task_statuses,
91            vec![TaskStatus::ToDo, TaskStatus::Started, TaskStatus::Blocked]
92        );
93
94        // Repo should contain two logfiles
95        let mut paths = repo.list().unwrap();
96        paths.sort();
97        assert_eq!(paths, vec![first_logpath, new_logpath]);
98    }
99}