1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
pub use super::Snapshot;

/// Manage branch snapshots on disk
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Stack {
    pub name: String,
    root: std::path::PathBuf,
    capacity: Option<usize>,
}

impl Stack {
    pub const DEFAULT_STACK: &'static str = "recent";
    const EXT: &'static str = "bak";

    /// Create a named stack of snapshots
    pub fn new(name: &str, repo: &crate::git::GitRepo) -> Self {
        let root = stack_root(repo.raw().path(), name);
        let name = name.to_owned();
        Self {
            name,
            root,
            capacity: None,
        }
    }

    /// Discover all stacks of snapshots
    pub fn all(repo: &crate::git::GitRepo) -> impl Iterator<Item = Self> {
        let root = stacks_root(repo.raw().path());
        let mut stacks: Vec<_> = std::fs::read_dir(root)
            .into_iter()
            .flatten()
            .filter_map(|e| {
                let e = e.ok()?;
                let e = e.file_type().ok()?.is_dir().then_some(e)?;
                let p = e.path();
                let stack_name = p.file_name()?.to_str()?.to_owned();
                let stack_root = stack_root(repo.raw().path(), &stack_name);
                Some(Self {
                    name: stack_name,
                    root: stack_root,
                    capacity: None,
                })
            })
            .collect();
        if !stacks.iter().any(|v| v.name == Self::DEFAULT_STACK) {
            stacks.insert(0, Self::new(Self::DEFAULT_STACK, repo));
        }
        stacks.into_iter()
    }

    /// Change the capacity of the stack
    pub fn capacity(&mut self, capacity: Option<usize>) {
        self.capacity = capacity;
    }

    /// Discover snapshots within this stack
    pub fn iter(&self) -> impl DoubleEndedIterator<Item = std::path::PathBuf> {
        let mut elements: Vec<(usize, std::path::PathBuf)> = std::fs::read_dir(&self.root)
            .into_iter()
            .flatten()
            .filter_map(|e| {
                let e = e.ok()?;
                let e = e.file_type().ok()?.is_file().then_some(e)?;
                let p = e.path();
                let p = (p.extension()? == Self::EXT).then_some(p)?;
                let index = p.file_stem()?.to_str()?.parse::<usize>().ok()?;
                Some((index, p))
            })
            .collect();
        elements.sort_unstable();
        elements.into_iter().map(|(_, p)| p)
    }

    /// Add a snapshot to this stack
    pub fn push(&mut self, snapshot: Snapshot) -> Result<std::path::PathBuf, std::io::Error> {
        let elems: Vec<_> = self.iter().collect();
        let last_path = elems.iter().last();
        let next_index = match last_path {
            Some(last_path) => {
                let current_index = last_path
                    .file_stem()
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .parse::<usize>()
                    .unwrap();
                current_index + 1
            }
            None => 0,
        };
        let last = last_path.and_then(|p| Snapshot::load(p).ok());
        if last.as_ref() == Some(&snapshot) {
            let last_path = last_path.unwrap().to_owned();
            log::trace!("Reusing snapshot {}", last_path.display());
            return Ok(last_path);
        }

        std::fs::create_dir_all(&self.root)?;
        let new_path = self.root.join(format!("{}.{}", next_index, Self::EXT));
        snapshot.save(&new_path)?;
        log::trace!("Backed up as {}", new_path.display());

        if let Some(capacity) = self.capacity {
            let len = elems.len();
            if capacity < len {
                let remove = len - capacity;
                log::debug!("Too many snapshots, clearing {} oldest", remove);
                for snapshot_path in &elems[0..remove] {
                    if let Err(err) = std::fs::remove_file(snapshot_path) {
                        log::debug!("Failed to remove {}: {}", snapshot_path.display(), err);
                    } else {
                        log::trace!("Removed {}", snapshot_path.display());
                    }
                }
            }
        }

        Ok(new_path)
    }

    /// Empty the snapshot stack
    pub fn clear(&mut self) {
        let _ = std::fs::remove_dir_all(&self.root);
    }

    /// Remove the most recent snapshot from the stack
    pub fn pop(&mut self) -> Option<std::path::PathBuf> {
        let mut elems: Vec<_> = self.iter().collect();
        let last = elems.pop()?;
        std::fs::remove_file(&last).ok()?;
        Some(last)
    }

    /// View the most recent snapshot in the stack
    pub fn peek(&mut self) -> Option<std::path::PathBuf> {
        self.iter().last()
    }
}

fn stacks_root(repo: &std::path::Path) -> std::path::PathBuf {
    repo.join("branch-stash")
}

fn stack_root(repo: &std::path::Path, stack: &str) -> std::path::PathBuf {
    repo.join("branch-stash").join(stack)
}