Skip to main content

git_broom/
pr_cache.rs

1use std::collections::BTreeMap;
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
10const PR_CACHE_VERSION: u32 = 1;
11
12#[derive(Debug, Clone)]
13pub struct PrCache {
14    path: PathBuf,
15    entries_by_remote: BTreeMap<String, PrCacheRemoteEntry>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct PrCacheRemoteEntry {
20    pub remote_url: String,
21    pub refreshed_at: i64,
22    pub default_branch: String,
23    pub pull_requests_by_head: BTreeMap<String, CachedPullRequestRecord>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct CachedPullRequestRecord {
28    pub state: String,
29    pub url: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33struct PrCacheFile {
34    version: u32,
35    entries_by_remote: BTreeMap<String, PrCacheRemoteEntry>,
36}
37
38impl PrCache {
39    pub fn new(repo: &Path) -> Result<Self> {
40        Ok(Self {
41            path: pr_cache_path(repo)?,
42            entries_by_remote: BTreeMap::new(),
43        })
44    }
45
46    pub fn load(repo: &Path) -> Result<Self> {
47        let path = pr_cache_path(repo)?;
48        if !path.exists() {
49            return Ok(Self {
50                path,
51                entries_by_remote: BTreeMap::new(),
52            });
53        }
54
55        let contents = fs::read_to_string(&path)
56            .with_context(|| format!("failed to read PR cache {}", path.display()))?;
57        let parsed = serde_json::from_str::<PrCacheFile>(&contents)
58            .with_context(|| format!("failed to parse PR cache {}", path.display()))?;
59
60        if parsed.version != PR_CACHE_VERSION {
61            bail!(
62                "unsupported PR cache version {} in {}",
63                parsed.version,
64                path.display()
65            );
66        }
67
68        Ok(Self {
69            path,
70            entries_by_remote: parsed.entries_by_remote,
71        })
72    }
73
74    pub fn path(&self) -> &Path {
75        &self.path
76    }
77
78    pub fn remote_entry(&self, remote: &str, remote_url: &str) -> Option<&PrCacheRemoteEntry> {
79        self.entries_by_remote
80            .get(remote)
81            .filter(|entry| entry.remote_url == remote_url)
82    }
83
84    pub fn replace_remote(&mut self, remote: &str, entry: PrCacheRemoteEntry) -> Result<()> {
85        self.entries_by_remote.insert(remote.to_string(), entry);
86        self.persist()
87    }
88
89    fn persist(&self) -> Result<()> {
90        if self.entries_by_remote.is_empty() {
91            match fs::remove_file(&self.path) {
92                Ok(()) => {}
93                Err(error) if error.kind() == ErrorKind::NotFound => {}
94                Err(error) => {
95                    return Err(error).with_context(|| {
96                        format!("failed to remove PR cache {}", self.path.display())
97                    });
98                }
99            }
100            return Ok(());
101        }
102
103        let parent = self
104            .path
105            .parent()
106            .context("PR cache path missing parent directory")?;
107        fs::create_dir_all(parent)
108            .with_context(|| format!("failed to create PR cache dir {}", parent.display()))?;
109
110        let file = PrCacheFile {
111            version: PR_CACHE_VERSION,
112            entries_by_remote: self.entries_by_remote.clone(),
113        };
114        let contents =
115            serde_json::to_string_pretty(&file).context("failed to serialize PR cache")?;
116        fs::write(&self.path, format!("{contents}\n"))
117            .with_context(|| format!("failed to write PR cache {}", self.path.display()))
118    }
119}
120
121fn pr_cache_path(repo: &Path) -> Result<PathBuf> {
122    let output = Command::new("git")
123        .args(["rev-parse", "--path-format=absolute", "--git-common-dir"])
124        .current_dir(repo)
125        .stdout(Stdio::piped())
126        .stderr(Stdio::piped())
127        .output()
128        .context("failed to resolve git common dir")?;
129
130    if !output.status.success() {
131        bail!(
132            "failed to resolve git common dir: {}",
133            String::from_utf8_lossy(&output.stderr).trim()
134        );
135    }
136
137    let common_dir =
138        String::from_utf8(output.stdout).context("git returned non-utf8 common dir output")?;
139    Ok(PathBuf::from(common_dir.trim()).join("git-broom/pr-cache.json"))
140}