Skip to main content

review_queue/
paths.rs

1//! XDG path resolution for config and data directories.
2//!
3//! Layout:
4//! - config: `$XDG_CONFIG_HOME/review-queue/config.toml`
5//! - data: `$XDG_DATA_HOME/review-queue/` — state.json, repos.json, sync.lock, workdir.json (the
6//!   `crate::workdir` scan cache), `repos/` (every tool-managed canonical clone, keyed by its
7//!   normalized URL - discovered repos live wherever the user's workdir put them instead), and
8//!   `workspaces/{id}` (every review's workspace, independent of where its canonical repo lives;
9//!   keyed by the full canonical id - see `crate::state::ReviewKey::slug` - so ids can't collide
10//!   across sources sharing a repo)
11
12use std::path::{Path, PathBuf};
13
14use anyhow::{Context, Result};
15use directories::ProjectDirs;
16
17#[derive(Clone)]
18pub struct Paths {
19    config_dir: PathBuf,
20    data_dir: PathBuf,
21}
22
23impl Paths {
24    /// Resolve paths from `ProjectDirs`, honoring the `data_dir` override once it's read from
25    /// config (see [`Paths::with_overrides`]).
26    pub fn discover() -> Result<Self> {
27        let dirs = ProjectDirs::from("", "", "review-queue")
28            .context("could not determine home directory")?;
29        Ok(Self {
30            config_dir: dirs.config_dir().to_path_buf(),
31            data_dir: dirs.data_dir().to_path_buf(),
32        })
33    }
34
35    pub fn with_overrides(mut self, data_dir: Option<PathBuf>) -> Self {
36        if let Some(d) = data_dir {
37            self.data_dir = d;
38        }
39        self
40    }
41
42    pub fn config_file(&self) -> PathBuf {
43        self.config_dir.join("config.toml")
44    }
45
46    pub fn state_file(&self) -> PathBuf {
47        self.data_dir.join("state.json")
48    }
49
50    pub fn repos_file(&self) -> PathBuf {
51        self.data_dir.join("repos.json")
52    }
53
54    pub fn sync_lock_file(&self) -> PathBuf {
55        self.data_dir.join("sync.lock")
56    }
57
58    /// The `crate::workdir` scan cache.
59    pub fn workdir_cache_file(&self) -> PathBuf {
60        self.data_dir.join("workdir.json")
61    }
62
63    /// Root of every tool-managed canonical repo's directory, keyed by name (its normalized URL).
64    pub fn repos_dir(&self) -> PathBuf {
65        self.data_dir.join("repos")
66    }
67
68    /// Where a tool-managed canonical clone lives: `repos/{name}`.
69    pub fn repo_dir(&self, name: &str) -> PathBuf {
70        self.repos_dir().join(name)
71    }
72
73    /// Root of every review's workspace: `workspaces/`.
74    pub fn workspaces_dir(&self) -> PathBuf {
75        self.data_dir.join("workspaces")
76    }
77
78    /// Where a specific review's workspace lives: `workspaces/{slug}`. `slug` should be the
79    /// review's full canonical id (`ReviewKey::slug`, e.g. `gh/owner/repo/42`) so it can't collide
80    /// with another source's workspace built from the same canonical repo.
81    pub fn workspace_dir(&self, slug: &str) -> PathBuf {
82        self.workspaces_dir().join(slug)
83    }
84
85    pub fn data_dir(&self) -> &Path {
86        &self.data_dir
87    }
88
89    /// Create the data and config directories if they don't exist yet.
90    pub fn ensure_dirs(&self) -> Result<()> {
91        std::fs::create_dir_all(&self.data_dir)
92            .with_context(|| format!("creating data dir {}", self.data_dir.display()))?;
93        std::fs::create_dir_all(&self.config_dir)
94            .with_context(|| format!("creating config dir {}", self.config_dir.display()))?;
95        Ok(())
96    }
97}