1use 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 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 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}