Skip to main content

open_loops/
ignores.rs

1//! Loops discarded by the user ("not worth continuing").
2//! Persisted at <base>/ignores.toml, keys in "repo/branch" format.
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeSet;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Default, Serialize, Deserialize)]
9struct IgnoreFile {
10    #[serde(default)]
11    ignored: BTreeSet<String>,
12}
13
14pub struct Ignores {
15    path: PathBuf,
16    set: BTreeSet<String>,
17}
18
19impl Ignores {
20    /// Loads the ignore list from `<base>/ignores.toml`.
21    ///
22    /// A missing file is treated as an empty list.
23    ///
24    /// # Errors
25    ///
26    /// Returns an error if the file exists but cannot be read or is not
27    /// valid TOML in the expected format.
28    pub fn load(base: &Path) -> Result<Self> {
29        let path = base.join("ignores.toml");
30        let set = match std::fs::read_to_string(&path) {
31            Ok(raw) => {
32                toml::from_str::<IgnoreFile>(&raw)
33                    .with_context(|| format!("invalid ignores.toml at {}", path.display()))?
34                    .ignored
35            }
36            Err(e) if e.kind() == std::io::ErrorKind::NotFound => BTreeSet::new(),
37            Err(e) => return Err(e).context(format!("reading {}", path.display())),
38        };
39        Ok(Self { path, set })
40    }
41
42    /// Adds `key` to the ignore list and immediately persists it to disk.
43    ///
44    /// # Errors
45    ///
46    /// Returns an error if the base directory cannot be created, if the path
47    /// has no parent directory, or if writing the file fails.
48    pub fn add(&mut self, key: &str) -> Result<()> {
49        self.set.insert(key.to_string());
50        let parent = self.path.parent().ok_or_else(|| {
51            anyhow::anyhow!("path has no parent directory: {}", self.path.display())
52        })?;
53        std::fs::create_dir_all(parent)?;
54        let file = IgnoreFile {
55            ignored: self.set.clone(),
56        };
57        std::fs::write(&self.path, toml::to_string_pretty(&file)?)?;
58        Ok(())
59    }
60
61    pub fn contains(&self, key: &str) -> bool {
62        self.set.contains(key)
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn empty_when_file_does_not_exist() {
72        let tmp = tempfile::tempdir().unwrap();
73        let ig = Ignores::load(tmp.path()).unwrap();
74        assert!(!ig.contains("repo/branch"));
75    }
76
77    #[test]
78    fn add_persists_and_contains_finds() {
79        let tmp = tempfile::tempdir().unwrap();
80        let mut ig = Ignores::load(tmp.path()).unwrap();
81        ig.add("app/feat/x").unwrap();
82        let reloaded = Ignores::load(tmp.path()).unwrap();
83        assert!(reloaded.contains("app/feat/x"));
84        assert!(!reloaded.contains("app/feat/y"));
85    }
86}