Skip to main content

git_broom/
keep_store.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fs;
3use std::io::ErrorKind;
4use std::path::{Path, PathBuf};
5use std::process::{Command, Stdio};
6
7use anyhow::{Context, Result, bail};
8use serde::{Deserialize, Serialize};
9
10use crate::app::CleanupMode;
11
12const KEEP_STORE_VERSION: u32 = 1;
13
14#[derive(Debug, Clone)]
15pub struct KeepStore {
16    path: PathBuf,
17    labels_by_mode: BTreeMap<String, BTreeSet<String>>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21struct KeepStoreFile {
22    version: u32,
23    labels_by_mode: BTreeMap<String, BTreeSet<String>>,
24}
25
26impl KeepStore {
27    pub fn load(repo: &Path) -> Result<Self> {
28        let path = keep_store_path(repo)?;
29        if !path.exists() {
30            return Ok(Self {
31                path,
32                labels_by_mode: BTreeMap::new(),
33            });
34        }
35
36        let contents = fs::read_to_string(&path)
37            .with_context(|| format!("failed to read keep-label store {}", path.display()))?;
38        let parsed = serde_json::from_str::<KeepStoreFile>(&contents)
39            .with_context(|| format!("failed to parse keep-label store {}", path.display()))?;
40
41        if parsed.version != KEEP_STORE_VERSION {
42            bail!(
43                "unsupported keep-label store version {} in {}",
44                parsed.version,
45                path.display()
46            );
47        }
48
49        Ok(Self {
50            path,
51            labels_by_mode: parsed.labels_by_mode,
52        })
53    }
54
55    pub fn path(&self) -> &Path {
56        &self.path
57    }
58
59    pub fn is_saved(&self, mode: CleanupMode, branch: &str) -> bool {
60        self.labels_by_mode
61            .get(mode.key())
62            .is_some_and(|branches| branches.contains(branch))
63    }
64
65    pub fn replace_mode<I, S>(&mut self, mode: CleanupMode, branches: I) -> Result<()>
66    where
67        I: IntoIterator<Item = S>,
68        S: Into<String>,
69    {
70        let saved = branches
71            .into_iter()
72            .map(Into::into)
73            .collect::<BTreeSet<_>>();
74        if saved.is_empty() {
75            self.labels_by_mode.remove(mode.key());
76        } else {
77            self.labels_by_mode.insert(mode.key().to_string(), saved);
78        }
79
80        self.persist()
81    }
82
83    fn persist(&self) -> Result<()> {
84        if self.labels_by_mode.is_empty() {
85            match fs::remove_file(&self.path) {
86                Ok(()) => {}
87                Err(error) if error.kind() == ErrorKind::NotFound => {}
88                Err(error) => {
89                    return Err(error).with_context(|| {
90                        format!("failed to remove keep-label store {}", self.path.display())
91                    });
92                }
93            }
94            return Ok(());
95        }
96
97        let parent = self
98            .path
99            .parent()
100            .context("keep-label store path missing parent directory")?;
101        fs::create_dir_all(parent)
102            .with_context(|| format!("failed to create keep-label dir {}", parent.display()))?;
103
104        let file = KeepStoreFile {
105            version: KEEP_STORE_VERSION,
106            labels_by_mode: self.labels_by_mode.clone(),
107        };
108        let contents =
109            serde_json::to_string_pretty(&file).context("failed to serialize keep-label store")?;
110        fs::write(&self.path, format!("{contents}\n"))
111            .with_context(|| format!("failed to write keep-label store {}", self.path.display()))
112    }
113}
114
115fn keep_store_path(repo: &Path) -> Result<PathBuf> {
116    let output = Command::new("git")
117        .args(["rev-parse", "--path-format=absolute", "--git-common-dir"])
118        .current_dir(repo)
119        .stdout(Stdio::piped())
120        .stderr(Stdio::piped())
121        .output()
122        .context("failed to resolve git common dir")?;
123
124    if !output.status.success() {
125        bail!(
126            "failed to resolve git common dir: {}",
127            String::from_utf8_lossy(&output.stderr).trim()
128        );
129    }
130
131    let common_dir =
132        String::from_utf8(output.stdout).context("git returned non-utf8 common dir output")?;
133    Ok(PathBuf::from(common_dir.trim()).join("git-broom/keep-labels.json"))
134}