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