Skip to main content

grit_lib/
ref_storage.rs

1//! Typed ref storage backend selection (`files` vs `reftable`).
2//!
3//! Discovery reads **repository-local** config only (never global or system config),
4//! matching legacy reftable detection (see `repository_config_snapshot_tests`).
5
6use std::fmt;
7use std::path::Path;
8use std::str::FromStr;
9
10use crate::error::{Error, Result};
11use crate::repo::{read_repository_format_from_git_dir, validate_repository_format_parsed};
12
13/// On-disk ref storage backend named by `extensions.refStorage`.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum RefStorageFormat {
17    /// Traditional loose refs under `refs/` plus `packed-refs`.
18    #[default]
19    Files,
20    /// Git reftable backend (`extensions.refStorage = reftable`).
21    Reftable,
22}
23
24impl fmt::Display for RefStorageFormat {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        f.write_str(match self {
27            Self::Files => "files",
28            Self::Reftable => "reftable",
29        })
30    }
31}
32
33impl FromStr for RefStorageFormat {
34    type Err = Error;
35
36    fn from_str(s: &str) -> Result<Self> {
37        Self::parse_config_value(s)
38    }
39}
40
41impl RefStorageFormat {
42    /// Parse an `extensions.refStorage` config value (optional `name:payload` suffix).
43    ///
44    /// # Errors
45    ///
46    /// Returns [`Error::InvalidRefStorageFormat`] when the name is not `files` or `reftable`.
47    pub fn parse_config_value(raw: &str) -> Result<Self> {
48        let trimmed = raw.trim();
49        let lower = trimmed.to_ascii_lowercase();
50        let name = lower
51            .split_once(':')
52            .map(|(prefix, _)| prefix)
53            .unwrap_or(lower.as_str());
54        match name {
55            "files" => Ok(Self::Files),
56            "reftable" => Ok(Self::Reftable),
57            _ => Err(Error::InvalidRefStorageFormat {
58                value: trimmed.to_owned(),
59            }),
60        }
61    }
62
63    /// Detect ref storage from repository-local `config` (linked worktree common dir when needed).
64    ///
65    /// When `extensions.refStorage` is absent, returns [`RefStorageFormat::Files`].
66    ///
67    /// # Errors
68    ///
69    /// Propagates I/O and repository format errors when local config is invalid.
70    pub fn detect(git_dir: &Path) -> Result<Self> {
71        let parsed = read_repository_format_from_git_dir(git_dir)?;
72        Self::from_repository_format(&parsed)
73    }
74
75    /// Resolve ref storage from an already-parsed repository-local format block.
76    ///
77    /// # Errors
78    ///
79    /// Propagates repository format validation and invalid `extensions.refStorage` values.
80    pub(crate) fn from_repository_format(parsed: &crate::repo::RepositoryFormat) -> Result<Self> {
81        validate_repository_format_parsed(parsed)?;
82        match parsed.ref_storage.as_deref() {
83            None => Ok(Self::Files),
84            Some(raw) => Self::parse_config_value(raw),
85        }
86    }
87
88    /// Validate an `extensions.refStorage` value from repository config.
89    ///
90    /// # Errors
91    ///
92    /// Returns [`Error::InvalidRefStorageFormat`] when the value is not recognized.
93    pub fn validate_config_extension(raw: &str) -> Result<()> {
94        Self::parse_config_value(raw).map(|_| ())
95    }
96
97    /// Whether this format uses the reftable backend.
98    #[must_use]
99    pub const fn is_reftable(self) -> bool {
100        matches!(self, Self::Reftable)
101    }
102}
103
104/// Detect ref storage format for `git_dir` (see [`RefStorageFormat::detect`]).
105pub fn detect_format(git_dir: &Path) -> Result<RefStorageFormat> {
106    RefStorageFormat::detect(git_dir)
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use std::fs;
113    use tempfile::TempDir;
114
115    #[test]
116    fn detect_reflects_rewritten_ref_storage_without_repository() {
117        let tmp = TempDir::new().expect("tempdir");
118        let git_dir = tmp.path().join(".git");
119        fs::create_dir_all(&git_dir).expect("git dir");
120        fs::write(
121            git_dir.join("config"),
122            "[core]\n\trepositoryformatversion = 0\n",
123        )
124        .expect("config");
125        assert_eq!(
126            RefStorageFormat::detect(&git_dir).expect("detect"),
127            RefStorageFormat::Files
128        );
129
130        fs::write(
131            git_dir.join("config"),
132            "[core]\n\trepositoryformatversion = 1\n[extensions]\n\trefStorage = reftable\n",
133        )
134        .expect("rewrite");
135        assert_eq!(
136            RefStorageFormat::detect(&git_dir).expect("detect again"),
137            RefStorageFormat::Reftable
138        );
139    }
140}