git_branch_stash/
stack.rs

1pub use super::Snapshot;
2
3/// Manage branch snapshots on disk
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct Stack {
6    pub name: String,
7    root: std::path::PathBuf,
8    capacity: Option<usize>,
9}
10
11impl Stack {
12    pub const DEFAULT_STACK: &'static str = "recent";
13    const EXT: &'static str = "bak";
14
15    /// Create a named stack of snapshots
16    pub fn new(name: &str, repo: &crate::git::GitRepo) -> Self {
17        let root = stack_root(repo.raw().path(), name);
18        let name = name.to_owned();
19        Self {
20            name,
21            root,
22            capacity: None,
23        }
24    }
25
26    /// Discover all stacks of snapshots
27    pub fn all(repo: &crate::git::GitRepo) -> impl Iterator<Item = Self> {
28        let root = stacks_root(repo.raw().path());
29        let mut stacks: Vec<_> = std::fs::read_dir(root)
30            .into_iter()
31            .flatten()
32            .filter_map(|e| {
33                let e = e.ok()?;
34                let e = e.file_type().ok()?.is_dir().then_some(e)?;
35                let p = e.path();
36                let stack_name = p.file_name()?.to_str()?.to_owned();
37                let stack_root = stack_root(repo.raw().path(), &stack_name);
38                Some(Self {
39                    name: stack_name,
40                    root: stack_root,
41                    capacity: None,
42                })
43            })
44            .collect();
45        if !stacks.iter().any(|v| v.name == Self::DEFAULT_STACK) {
46            stacks.insert(0, Self::new(Self::DEFAULT_STACK, repo));
47        }
48        stacks.into_iter()
49    }
50
51    /// Change the capacity of the stack
52    pub fn capacity(&mut self, capacity: Option<usize>) {
53        self.capacity = capacity;
54    }
55
56    /// Discover snapshots within this stack
57    pub fn iter(&self) -> impl DoubleEndedIterator<Item = std::path::PathBuf> {
58        let mut elements: Vec<(usize, std::path::PathBuf)> = std::fs::read_dir(&self.root)
59            .into_iter()
60            .flatten()
61            .filter_map(|e| {
62                let e = e.ok()?;
63                let e = e.file_type().ok()?.is_file().then_some(e)?;
64                let p = e.path();
65                let p = (p.extension()? == Self::EXT).then_some(p)?;
66                let index = p.file_stem()?.to_str()?.parse::<usize>().ok()?;
67                Some((index, p))
68            })
69            .collect();
70        elements.sort_unstable();
71        elements.into_iter().map(|(_, p)| p)
72    }
73
74    /// Add a snapshot to this stack
75    pub fn push(&mut self, snapshot: Snapshot) -> Result<std::path::PathBuf, std::io::Error> {
76        let elems: Vec<_> = self.iter().collect();
77        let last_path = elems.iter().last();
78        let next_index = match last_path {
79            Some(last_path) => {
80                let current_index = last_path
81                    .file_stem()
82                    .unwrap()
83                    .to_str()
84                    .unwrap()
85                    .parse::<usize>()
86                    .unwrap();
87                current_index + 1
88            }
89            None => 0,
90        };
91        let last = last_path.and_then(|p| Snapshot::load(p).ok());
92        if last.as_ref() == Some(&snapshot) {
93            let last_path = last_path.unwrap().to_owned();
94            log::trace!("Reusing snapshot {}", last_path.display());
95            return Ok(last_path);
96        }
97
98        std::fs::create_dir_all(&self.root)?;
99        let new_path = self.root.join(format!("{}.{}", next_index, Self::EXT));
100        snapshot.save(&new_path)?;
101        log::trace!("Backed up as {}", new_path.display());
102
103        if let Some(capacity) = self.capacity {
104            let len = elems.len();
105            if capacity < len {
106                let remove = len - capacity;
107                log::debug!("Too many snapshots, clearing {} oldest", remove);
108                for snapshot_path in &elems[0..remove] {
109                    if let Err(err) = std::fs::remove_file(snapshot_path) {
110                        log::debug!("Failed to remove {}: {}", snapshot_path.display(), err);
111                    } else {
112                        log::trace!("Removed {}", snapshot_path.display());
113                    }
114                }
115            }
116        }
117
118        Ok(new_path)
119    }
120
121    /// Empty the snapshot stack
122    pub fn clear(&mut self) {
123        let _ = std::fs::remove_dir_all(&self.root);
124    }
125
126    /// Remove the most recent snapshot from the stack
127    pub fn pop(&mut self) -> Option<std::path::PathBuf> {
128        let mut elems: Vec<_> = self.iter().collect();
129        let last = elems.pop()?;
130        std::fs::remove_file(&last).ok()?;
131        Some(last)
132    }
133
134    /// View the most recent snapshot in the stack
135    pub fn peek(&mut self) -> Option<std::path::PathBuf> {
136        self.iter().last()
137    }
138}
139
140fn stacks_root(repo: &std::path::Path) -> std::path::PathBuf {
141    repo.join("branch-stash")
142}
143
144fn stack_root(repo: &std::path::Path, stack: &str) -> std::path::PathBuf {
145    repo.join("branch-stash").join(stack)
146}