use std::collections::BTreeSet;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::atomic::{write_atomic, write_create_new};
use crate::hash::{HASH_LEN, HEX_LEN, Hash, to_hex};
use crate::layout::RepoLayout;
pub const REFS_DIR: &str = "refs";
pub const HEADS_DIR: &str = "refs/heads";
pub const TAGS_DIR: &str = "refs/tags";
pub const REMOTES_DIR: &str = "refs/remotes";
pub const HEAD_FILE: &str = "HEAD";
pub const SHALLOW_FILE: &str = "shallow";
const HEAD_REF_PREFIX: &str = "ref: refs/heads/";
const HEAD_MAX_BYTES: u64 = 4 * 1024;
const REF_FILE_MAX_BYTES: u64 = 128;
const SHALLOW_MAX_BYTES: u64 = 1024 * 1024;
#[derive(Debug, thiserror::Error)]
pub enum RefError {
#[error("invalid ref name '{0}'")]
InvalidRefName(String),
#[error("invalid ref content for '{0}'")]
InvalidRef(String),
#[error("HEAD is not a valid symbolic-ref or detached-hash file")]
InvalidHead,
#[error("HEAD is not present")]
NoHead,
#[error("ref '{0}' did not satisfy CAS condition")]
Conflict(String),
#[error("ref '{0}' not found")]
NotFound(String),
#[error("cannot delete the current branch '{0}'")]
CurrentBranch(String),
#[error(transparent)]
Io(#[from] io::Error),
}
pub type RefResult<T> = Result<T, RefError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefWriteCondition {
Any,
Missing,
Match(Hash),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Head {
Branch(String),
Detached(Hash),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ref {
pub name: String,
pub hash: Option<Hash>,
}
#[must_use]
pub fn validate_ref_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
if name.starts_with('/') {
return false;
}
let mut last_part: &str = "";
for part in name.split('/') {
if part.is_empty() {
return false;
}
if part.starts_with('.') {
return false;
}
let bytes = part.as_bytes();
if bytes.len() >= 5 && &bytes[bytes.len() - 5..] == b".lock" {
return false;
}
for &c in part.as_bytes() {
if c == 0 || c == b'\\' {
return false;
}
let allowed = c.is_ascii_alphanumeric() || c == b'.' || c == b'_' || c == b'-';
if !allowed {
return false;
}
}
last_part = part;
}
if last_part == "HEAD" {
return false;
}
true
}
pub(crate) fn sanitize_ref_name(name: &str) -> String {
let mut out = String::with_capacity(name.len());
for &b in name.as_bytes() {
let allowed = b.is_ascii_alphanumeric() || b == b'-';
if allowed {
out.push(b as char);
} else {
use core::fmt::Write as _;
let _ = write!(&mut out, "_{b:02x}");
}
}
out
}
#[cfg(feature = "history-mmr")]
pub(crate) fn history_lock_name(branch: &str) -> String {
format!("refs-history-{}.lock", sanitize_ref_name(branch))
}
pub(crate) fn cas_lock_name(common_dir: &Path, path: &Path) -> String {
let ref_key = path
.strip_prefix(common_dir)
.map_or_else(|_| path.to_string_lossy(), |p| p.to_string_lossy());
format!("refs-{}.lock", sanitize_ref_name(&ref_key))
}
#[must_use]
pub fn validate_ref_prefix(prefix: &str) -> bool {
if prefix.is_empty() {
return true;
}
let trimmed = prefix.trim_end_matches('/');
if trimmed.is_empty() {
return false;
}
validate_ref_name(trimmed)
}
#[must_use]
pub fn encode_ref_wire(h: &Hash) -> [u8; 65] {
let hex = to_hex(h);
let bytes = hex.as_bytes();
let mut out = [0u8; 65];
out[..HEX_LEN].copy_from_slice(bytes);
out[HEX_LEN] = b'\n';
out
}
#[must_use]
pub fn decode_ref_wire(data: &[u8]) -> Option<Hash> {
let s = core::str::from_utf8(data).ok()?;
let trimmed = s.trim_end_matches(['\n', '\r', ' ', '\t']);
if trimmed.len() != HEX_LEN {
return None;
}
parse_lowercase_hash(trimmed.as_bytes())
}
#[must_use]
pub fn parse_lowercase_hash(bytes: &[u8]) -> Option<Hash> {
if bytes.len() != HEX_LEN {
return None;
}
let mut out = [0u8; HASH_LEN];
for i in 0..HASH_LEN {
let hi = lowercase_nibble(bytes[i * 2])?;
let lo = lowercase_nibble(bytes[i * 2 + 1])?;
out[i] = (hi << 4) | lo;
}
Some(out)
}
fn lowercase_nibble(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(10 + (b - b'a')),
_ => None,
}
}
pub fn init(layout: &RepoLayout) -> RefResult<()> {
fs::create_dir_all(layout.refs_dir())?;
fs::create_dir_all(layout.heads_dir())?;
fs::create_dir_all(layout.tags_dir())?;
fs::create_dir_all(layout.remotes_dir())?;
let head_path = layout.head_file();
if !head_path.exists() {
let body = format!("{HEAD_REF_PREFIX}main\n");
write_atomic(&head_path, body.as_bytes(), false)?;
}
Ok(())
}
pub fn read_head(layout: &RepoLayout) -> RefResult<Head> {
let path = layout.head_file();
let meta = match fs::metadata(&path) {
Ok(m) => m,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(RefError::NoHead),
Err(e) => return Err(RefError::Io(e)),
};
if meta.len() > HEAD_MAX_BYTES {
return Err(RefError::InvalidHead);
}
let raw = fs::read(&path)?;
let s = core::str::from_utf8(&raw).map_err(|_| RefError::InvalidHead)?;
let trimmed = s.trim_end_matches(['\n', '\r', ' ', '\t']);
if let Some(branch) = trimmed.strip_prefix(HEAD_REF_PREFIX) {
if !validate_ref_name(branch) {
return Err(RefError::InvalidHead);
}
return Ok(Head::Branch(branch.to_string()));
}
if trimmed.len() == HEX_LEN {
let h = parse_lowercase_hash(trimmed.as_bytes()).ok_or(RefError::InvalidHead)?;
return Ok(Head::Detached(h));
}
Err(RefError::InvalidHead)
}
pub fn write_head_branch(layout: &RepoLayout, branch: &str) -> RefResult<()> {
if !validate_ref_name(branch) {
return Err(RefError::InvalidRefName(branch.to_string()));
}
let body = format!("{HEAD_REF_PREFIX}{branch}\n");
write_atomic(&layout.head_file(), body.as_bytes(), false)?;
Ok(())
}
pub fn write_head_detached(layout: &RepoLayout, h: &Hash) -> RefResult<()> {
let wire = encode_ref_wire(h);
write_atomic(&layout.head_file(), &wire, false)?;
Ok(())
}
pub fn resolve_head(layout: &RepoLayout) -> RefResult<Option<Hash>> {
let head = match read_head(layout) {
Ok(h) => h,
Err(RefError::NoHead) => return Ok(None),
Err(e) => return Err(e),
};
match head {
Head::Branch(name) => read_ref(layout, &name),
Head::Detached(h) => Ok(Some(h)),
}
}
pub fn update_head(layout: &RepoLayout, commit_hash: &Hash) -> RefResult<()> {
let head = read_head(layout)?;
match head {
Head::Branch(name) => write_ref(layout, &name, commit_hash),
Head::Detached(_) => write_head_detached(layout, commit_hash),
}
}
pub fn read_ref(layout: &RepoLayout, branch: &str) -> RefResult<Option<Hash>> {
if !validate_ref_name(branch) {
return Err(RefError::InvalidRefName(branch.to_string()));
}
read_ref_under(layout.common_dir(), HEADS_DIR, branch)
}
pub fn write_ref(layout: &RepoLayout, branch: &str, h: &Hash) -> RefResult<()> {
update_ref(layout, branch, RefWriteCondition::Any, h)
}
pub fn update_ref(
layout: &RepoLayout,
branch: &str,
condition: RefWriteCondition,
h: &Hash,
) -> RefResult<()> {
if !validate_ref_name(branch) {
return Err(RefError::InvalidRefName(branch.to_string()));
}
let path = ref_path(layout.common_dir(), HEADS_DIR, branch);
let wire = encode_ref_wire(h);
cas_write(layout.common_dir(), &path, &wire, branch, condition)
}
#[cfg(feature = "history-mmr")]
pub fn update_ref_with_history<X: crate::protocol::async_shim::Executor + 'static>(
layout: &RepoLayout,
branch: &str,
condition: RefWriteCondition,
hash: &Hash,
history: &mut crate::history::CommitHistory<X>,
) -> RefResult<()> {
update_ref_with_history_locked(layout, branch, condition, hash, history, |_, _| Ok(()))
}
#[cfg(feature = "history-mmr")]
pub fn update_ref_with_history_and_backfill<X, F, E>(
layout: &RepoLayout,
branch: &str,
condition: RefWriteCondition,
hash: &Hash,
history: &mut crate::history::CommitHistory<X>,
mut parent_of: F,
) -> RefResult<()>
where
X: crate::protocol::async_shim::Executor + 'static,
F: FnMut(&Hash) -> Result<Option<Hash>, E>,
E: core::fmt::Display,
{
update_ref_with_history_locked(
layout,
branch,
condition,
hash,
history,
|history, current| {
crate::history::rebuild_from_chain(history, current, &mut parent_of)
.map(|_| ())
.map_err(|e| e.to_string())
},
)
}
#[cfg(feature = "history-mmr")]
fn update_ref_with_history_locked<X, G>(
layout: &RepoLayout,
branch: &str,
condition: RefWriteCondition,
hash: &Hash,
history: &mut crate::history::CommitHistory<X>,
mut on_empty_journal: G,
) -> RefResult<()>
where
X: crate::protocol::async_shim::Executor + 'static,
G: FnMut(&mut crate::history::CommitHistory<X>, Hash) -> Result<(), String>,
{
let Some(history_dir) = history.common_dir() else {
return Err(RefError::InvalidRef(format!(
"{branch}: update_ref_with_history requires a journaled CommitHistory (open_at)"
)));
};
if history_dir != layout.common_dir() {
return Err(RefError::InvalidRef(format!(
"{branch}: CommitHistory's common dir does not match the ref's common dir"
)));
}
if history.branch() != Some(branch) {
return Err(RefError::InvalidRef(format!(
"{branch}: CommitHistory was opened for a different branch ({:?})",
history.branch()
)));
}
let lock_name = history_lock_name(branch);
let _lock = crate::repo_lock::acquire_default(layout.common_dir(), &lock_name).map_err(
|e| match e {
crate::repo_lock::LockError::Io(io) => RefError::Io(io),
other => RefError::InvalidRef(format!("{branch}: lock acquisition: {other}")),
},
)?;
history
.reopen()
.map_err(|e| RefError::InvalidRef(format!("{branch}: history reopen: {e}")))?;
if let Some(current) = read_ref(layout, branch)? {
if history.is_empty() {
on_empty_journal(history, current)
.map_err(|e| RefError::InvalidRef(format!("{branch}: history backfill: {e}")))?;
} else {
heal_one_ahead_gap(history, ¤t)
.map_err(|e| RefError::InvalidRef(format!("{branch}: history recovery: {e}")))?;
}
}
update_ref(layout, branch, condition, hash)?;
history
.append(hash)
.map_err(|e| RefError::InvalidRef(format!("{branch}: history append: {e}")))?;
Ok(())
}
#[cfg(feature = "history-mmr")]
fn heal_one_ahead_gap<X: crate::protocol::async_shim::Executor + 'static>(
history: &mut crate::history::CommitHistory<X>,
current_ref_value: &Hash,
) -> Result<(), crate::history::HistoryError> {
let len = history.len();
let Some(last) = len.checked_sub(1) else {
return Ok(());
};
let in_sync = history
.prove(crate::history::Position(last))
.is_ok_and(|proof| {
crate::history::verify_inclusion(
current_ref_value,
crate::history::Position(last),
&proof,
&history.root(),
)
});
if in_sync {
return Ok(());
}
history.append(current_ref_value)?;
Ok(())
}
pub fn delete_ref(layout: &RepoLayout, branch: &str) -> RefResult<()> {
if !validate_ref_name(branch) {
return Err(RefError::InvalidRefName(branch.to_string()));
}
let path = ref_path(layout.common_dir(), HEADS_DIR, branch);
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
Err(RefError::NotFound(branch.to_string()))
}
Err(e) => Err(RefError::Io(e)),
}
}
pub fn delete_ref_safe(layout: &RepoLayout, branch: &str) -> RefResult<()> {
match read_head(layout) {
Ok(Head::Branch(current)) if current == branch => {
Err(RefError::CurrentBranch(branch.to_string()))
}
_ => delete_ref(layout, branch),
}
}
pub fn delete_ref_if_matches(layout: &RepoLayout, branch: &str, expected: Hash) -> RefResult<()> {
if !validate_ref_name(branch) {
return Err(RefError::InvalidRefName(branch.to_string()));
}
let common_dir = layout.common_dir();
let path = ref_path(common_dir, HEADS_DIR, branch);
let lock_name = cas_lock_name(common_dir, &path);
let _lock = crate::repo_lock::acquire_default(common_dir, &lock_name).map_err(|e| match e {
crate::repo_lock::LockError::Io(io) => RefError::Io(io),
other => RefError::InvalidRef(format!("{branch}: refs.lock acquisition: {other}")),
})?;
let current = match fs::read(&path) {
Ok(b) => Some(decode_ref_wire(&b).ok_or_else(|| RefError::InvalidRef(branch.to_string()))?),
Err(e) if e.kind() == io::ErrorKind::NotFound => None,
Err(e) => return Err(RefError::Io(e)),
};
if current != Some(expected) {
return Err(RefError::Conflict(branch.to_string()));
}
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
Err(RefError::Conflict(branch.to_string()))
}
Err(e) => Err(RefError::Io(e)),
}
}
#[cfg(feature = "history-mmr")]
pub fn delete_ref_with_history<X: crate::protocol::async_shim::Executor + 'static>(
layout: &RepoLayout,
branch: &str,
executor: std::sync::Arc<X>,
) -> RefResult<()> {
let lock_name = history_lock_name(branch);
let _lock = crate::repo_lock::acquire_default(layout.common_dir(), &lock_name).map_err(
|e| match e {
crate::repo_lock::LockError::Io(io) => RefError::Io(io),
other => RefError::InvalidRef(format!("{branch}: lock acquisition: {other}")),
},
)?;
if read_ref(layout, branch)?.is_none() {
return Err(RefError::NotFound(branch.to_string()));
}
let history = crate::history::CommitHistory::open_at(executor, layout, branch)
.map_err(|e| RefError::InvalidRef(format!("{branch}: open history journal: {e}")))?;
history
.destroy()
.map_err(|e| RefError::InvalidRef(format!("{branch}: destroy history journal: {e}")))?;
delete_ref(layout, branch)
}
#[cfg(feature = "history-mmr")]
pub fn delete_ref_with_history_if_matches<X: crate::protocol::async_shim::Executor + 'static>(
layout: &RepoLayout,
branch: &str,
expected: Hash,
executor: std::sync::Arc<X>,
) -> RefResult<()> {
let lock_name = history_lock_name(branch);
let _lock = crate::repo_lock::acquire_default(layout.common_dir(), &lock_name).map_err(
|e| match e {
crate::repo_lock::LockError::Io(io) => RefError::Io(io),
other => RefError::InvalidRef(format!("{branch}: lock acquisition: {other}")),
},
)?;
match read_ref(layout, branch)? {
Some(current) if current == expected => {}
_ => return Err(RefError::Conflict(branch.to_string())),
}
let history = crate::history::CommitHistory::open_at(executor, layout, branch)
.map_err(|e| RefError::InvalidRef(format!("{branch}: open history journal: {e}")))?;
history
.destroy()
.map_err(|e| RefError::InvalidRef(format!("{branch}: destroy history journal: {e}")))?;
delete_ref_if_matches(layout, branch, expected)
}
#[cfg(feature = "history-mmr")]
pub fn delete_ref_safe_with_history<X: crate::protocol::async_shim::Executor + 'static>(
layout: &RepoLayout,
branch: &str,
executor: std::sync::Arc<X>,
) -> RefResult<()> {
match read_head(layout) {
Ok(Head::Branch(current)) if current == branch => {
Err(RefError::CurrentBranch(branch.to_string()))
}
_ => delete_ref_with_history(layout, branch, executor),
}
}
pub fn list_refs(layout: &RepoLayout) -> RefResult<Vec<Ref>> {
list_refs_under(layout.common_dir(), HEADS_DIR)
}
pub fn read_remote_ref(layout: &RepoLayout, remote: &str, branch: &str) -> RefResult<Option<Hash>> {
validate_remote_and_branch(remote, branch)?;
read_ref_under(layout.common_dir(), &remote_ref_dir(remote), branch)
}
pub fn write_remote_ref(
layout: &RepoLayout,
remote: &str,
branch: &str,
h: &Hash,
) -> RefResult<()> {
validate_remote_and_branch(remote, branch)?;
let path = ref_path(layout.common_dir(), &remote_ref_dir(remote), branch);
let wire = encode_ref_wire(h);
cas_write(
layout.common_dir(),
&path,
&wire,
branch,
RefWriteCondition::Any,
)
}
#[derive(Debug)]
pub struct RemoteRefBatch<'a> {
layout: &'a RepoLayout,
sub_dir: String,
touched_dirs: BTreeSet<PathBuf>,
}
impl<'a> RemoteRefBatch<'a> {
pub fn new(layout: &'a RepoLayout, remote: &str) -> RefResult<Self> {
if !validate_ref_name(remote) {
return Err(RefError::InvalidRefName(remote.to_string()));
}
Ok(Self {
layout,
sub_dir: remote_ref_dir(remote),
touched_dirs: BTreeSet::new(),
})
}
pub fn write(&mut self, branch: &str, h: &Hash) -> RefResult<()> {
if !validate_ref_name(branch) {
return Err(RefError::InvalidRefName(branch.to_string()));
}
let path = ref_path(self.layout.common_dir(), &self.sub_dir, branch);
let parent = path
.parent()
.expect("remote-tracking ref path always has a parent")
.to_path_buf();
fs::create_dir_all(&parent)?;
let wire = encode_ref_wire(h);
crate::atomic::write_content_synced(&path, &wire)?;
self.touched_dirs.insert(parent);
Ok(())
}
pub fn commit(self) -> RefResult<()> {
for dir in &self.touched_dirs {
crate::atomic::sync_dir(dir)?;
}
Ok(())
}
}
pub fn delete_remote_ref(layout: &RepoLayout, remote: &str, branch: &str) -> RefResult<()> {
validate_remote_and_branch(remote, branch)?;
let path = ref_path(layout.common_dir(), &remote_ref_dir(remote), branch);
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
Err(RefError::NotFound(format!("{remote}/{branch}")))
}
Err(e) => Err(RefError::Io(e)),
}
}
pub fn list_remote_refs(layout: &RepoLayout, remote: &str) -> RefResult<Vec<Ref>> {
if !validate_ref_name(remote) {
return Err(RefError::InvalidRefName(remote.to_string()));
}
list_refs_under(layout.common_dir(), &remote_ref_dir(remote))
}
pub fn list_remote_names(layout: &RepoLayout) -> RefResult<Vec<String>> {
let dir = layout.remotes_dir();
let entries = match fs::read_dir(&dir) {
Ok(e) => e,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(RefError::Io(e)),
};
let mut names = Vec::new();
for entry in entries {
let entry = entry.map_err(RefError::Io)?;
if !entry.file_type().map_err(RefError::Io)?.is_dir() {
continue;
}
if let Some(name) = entry.file_name().to_str()
&& validate_ref_name(name)
{
names.push(name.to_owned());
}
}
names.sort();
Ok(names)
}
pub fn read_tag(layout: &RepoLayout, name: &str) -> RefResult<Option<Hash>> {
if !validate_ref_name(name) {
return Err(RefError::InvalidRefName(name.to_string()));
}
read_ref_under(layout.common_dir(), TAGS_DIR, name)
}
pub fn write_tag(layout: &RepoLayout, name: &str, h: &Hash) -> RefResult<()> {
update_tag(layout, name, RefWriteCondition::Any, h)
}
pub fn update_tag(
layout: &RepoLayout,
name: &str,
condition: RefWriteCondition,
h: &Hash,
) -> RefResult<()> {
if !validate_ref_name(name) {
return Err(RefError::InvalidRefName(name.to_string()));
}
let path = ref_path(layout.common_dir(), TAGS_DIR, name);
let wire = encode_ref_wire(h);
cas_write(layout.common_dir(), &path, &wire, name, condition)
}
pub fn delete_tag(layout: &RepoLayout, name: &str) -> RefResult<()> {
if !validate_ref_name(name) {
return Err(RefError::InvalidRefName(name.to_string()));
}
let path = ref_path(layout.common_dir(), TAGS_DIR, name);
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Err(RefError::NotFound(name.to_string())),
Err(e) => Err(RefError::Io(e)),
}
}
pub fn list_tags(layout: &RepoLayout) -> RefResult<Vec<Ref>> {
list_refs_under(layout.common_dir(), TAGS_DIR)
}
pub fn load_shallow_boundaries(layout: &RepoLayout) -> RefResult<Option<Vec<Hash>>> {
let path = layout.shallow_file();
let meta = match fs::metadata(&path) {
Ok(m) => m,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(RefError::Io(e)),
};
if meta.len() == 0 {
return Ok(None);
}
if meta.len() > SHALLOW_MAX_BYTES {
return Err(RefError::InvalidRef("shallow file too large".to_string()));
}
let bytes = fs::read(&path)?;
let s = core::str::from_utf8(&bytes).map_err(|_| RefError::InvalidHead)?;
let mut out = Vec::new();
for line in s.split('\n') {
let trimmed = line.trim_end_matches(['\r', ' ', '\t']);
if trimmed.len() != HEX_LEN {
continue;
}
if let Some(h) = parse_lowercase_hash(trimmed.as_bytes()) {
out.push(h);
}
}
if out.is_empty() {
return Ok(None);
}
Ok(Some(out))
}
pub fn write_shallow_boundaries(layout: &RepoLayout, boundaries: &[Hash]) -> RefResult<()> {
let path = layout.shallow_file();
if boundaries.is_empty() {
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(RefError::Io(e)),
}
} else {
let mut out = Vec::with_capacity(boundaries.len() * 65);
for h in boundaries {
out.extend_from_slice(&encode_ref_wire(h));
}
write_atomic(&path, &out, true)?;
Ok(())
}
}
fn ref_path(common_dir: &Path, sub_dir: &str, name: &str) -> PathBuf {
let mut path = common_dir.join(sub_dir);
for segment in name.split('/') {
path.push(segment);
}
path
}
fn remote_ref_dir(remote: &str) -> String {
format!("{REMOTES_DIR}/{remote}")
}
fn validate_remote_and_branch(remote: &str, branch: &str) -> RefResult<()> {
if !validate_ref_name(remote) {
return Err(RefError::InvalidRefName(remote.to_string()));
}
if !validate_ref_name(branch) {
return Err(RefError::InvalidRefName(branch.to_string()));
}
Ok(())
}
fn read_ref_under(common_dir: &Path, sub_dir: &str, name: &str) -> RefResult<Option<Hash>> {
let path = ref_path(common_dir, sub_dir, name);
let meta = match fs::metadata(&path) {
Ok(m) => m,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(RefError::Io(e)),
};
if meta.len() > REF_FILE_MAX_BYTES {
return Err(RefError::InvalidRef(name.to_string()));
}
let bytes = fs::read(&path)?;
let h = decode_ref_wire(&bytes).ok_or_else(|| RefError::InvalidRef(name.to_string()))?;
Ok(Some(h))
}
fn cas_write(
common_dir: &Path,
path: &Path,
wire: &[u8; 65],
name_for_err: &str,
condition: RefWriteCondition,
) -> RefResult<()> {
match condition {
RefWriteCondition::Any => {
write_atomic(path, wire, true)?;
Ok(())
}
RefWriteCondition::Missing => {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let created = write_create_new(path, wire, true)?;
if !created {
return Err(RefError::Conflict(name_for_err.to_string()));
}
Ok(())
}
RefWriteCondition::Match(expected) => {
let lock_name = cas_lock_name(common_dir, path);
let _lock =
crate::repo_lock::acquire_default(common_dir, &lock_name).map_err(|e| match e {
crate::repo_lock::LockError::Io(io) => RefError::Io(io),
other => RefError::InvalidRef(format!(
"{name_for_err}: refs.lock acquisition: {other}"
)),
})?;
let current = match fs::read(path) {
Ok(b) => Some(
decode_ref_wire(&b)
.ok_or_else(|| RefError::InvalidRef(name_for_err.to_string()))?,
),
Err(e) if e.kind() == io::ErrorKind::NotFound => None,
Err(e) => return Err(RefError::Io(e)),
};
if current != Some(expected) {
return Err(RefError::Conflict(name_for_err.to_string()));
}
write_atomic(path, wire, true)?;
Ok(())
}
}
}
fn list_refs_under(common_dir: &Path, sub_dir: &str) -> RefResult<Vec<Ref>> {
let root = common_dir.join(sub_dir);
let mut out = Vec::new();
if !root.is_dir() {
return Ok(out);
}
collect_refs(&root, "", &mut out, 0)?;
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
}
const MAX_REF_DEPTH: usize = 32;
fn collect_refs(root: &Path, prefix: &str, out: &mut Vec<Ref>, depth: usize) -> RefResult<()> {
if depth > MAX_REF_DEPTH {
return Ok(());
}
let dir_path = if prefix.is_empty() {
root.to_path_buf()
} else {
root.join(prefix)
};
let iter = match fs::read_dir(&dir_path) {
Ok(i) => i,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(RefError::Io(e)),
};
for entry in iter {
let entry = entry?;
let file_name = match entry.file_name().to_str() {
Some(s) => s.to_string(),
None => continue, };
let child_name = if prefix.is_empty() {
file_name.clone()
} else {
format!("{prefix}/{file_name}")
};
let ft = entry.file_type()?;
if ft.is_dir() {
collect_refs(root, &child_name, out, depth + 1)?;
continue;
}
if !ft.is_file() {
continue;
}
if !validate_ref_name(&child_name) {
continue;
}
let Ok(bytes) = fs::read(entry.path()) else {
continue;
};
let hash = decode_ref_wire(&bytes);
out.push(Ref {
name: child_name,
hash,
});
}
Ok(())
}
#[doc(hidden)]
#[must_use]
pub fn _hash_from_lowercase_hex_for_tests(s: &str) -> Option<Hash> {
parse_lowercase_hash(s.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash;
use std::sync::Barrier;
use tempfile::TempDir;
fn fresh_repo() -> (TempDir, RepoLayout) {
let dir = TempDir::new().unwrap();
let layout = RepoLayout::single(dir.path());
fs::create_dir_all(layout.common_dir()).unwrap();
init(&layout).unwrap();
(dir, layout)
}
fn h(seed: &str) -> Hash {
hash::hash(seed.as_bytes())
}
#[test]
fn validate_accepts_simple_names() {
assert!(validate_ref_name("main"));
assert!(validate_ref_name("feat/v1.0-beta"));
assert!(validate_ref_name("release/2024_09"));
}
#[test]
fn validate_rejects_empty() {
assert!(!validate_ref_name(""));
}
#[test]
fn validate_rejects_leading_slash() {
assert!(!validate_ref_name("/main"));
}
#[test]
fn validate_rejects_dotdot_segment() {
assert!(!validate_ref_name("feat/.."));
assert!(!validate_ref_name("../escape"));
assert!(!validate_ref_name("feat/./topic"));
}
#[test]
fn validate_rejects_dot_leading_segment() {
assert!(!validate_ref_name(".hidden"));
assert!(!validate_ref_name("refs/.hidden/main"));
assert!(!validate_ref_name(".rename.tmp.12345.0"));
assert!(!validate_ref_name("refs/remotes/.rename.tmp.12345.0/main"));
}
#[test]
fn validate_rejects_double_slash() {
assert!(!validate_ref_name("refs//heads/main"));
assert!(!validate_ref_name("main/"));
}
#[test]
fn validate_rejects_disallowed_bytes() {
assert!(!validate_ref_name("main@v1"));
assert!(!validate_ref_name("feat\\branch"));
assert!(!validate_ref_name("with space"));
}
#[test]
fn validate_rejects_lock_suffix() {
assert!(!validate_ref_name("refs/heads/main.lock"));
}
#[test]
fn validate_rejects_head_final_segment() {
assert!(!validate_ref_name("refs/heads/HEAD"));
assert!(!validate_ref_name("HEAD"));
}
#[test]
fn validate_accepts_main_regression() {
assert!(validate_ref_name("refs/heads/main"));
}
#[test]
fn validate_accepts_non_lock_suffix_regression() {
assert!(validate_ref_name("refs/heads/lockfile"));
}
#[test]
fn validate_accepts_headless_regression() {
assert!(validate_ref_name("refs/heads/HEADless"));
}
#[test]
fn validate_prefix() {
assert!(validate_ref_prefix(""));
assert!(validate_ref_prefix("refs/heads/"));
assert!(validate_ref_prefix("refs/heads"));
assert!(!validate_ref_prefix("refs//heads/"));
assert!(!validate_ref_prefix("/"));
}
#[test]
fn wire_round_trip() {
let original = h("test-ref");
let wire = encode_ref_wire(&original);
assert_eq!(wire.len(), 65);
assert_eq!(wire[64], b'\n');
let parsed = decode_ref_wire(&wire).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn wire_rejects_uppercase() {
let original = h("test-ref");
let mut wire = encode_ref_wire(&original);
let mut flipped = false;
for b in &mut wire[..HEX_LEN] {
if (b'a'..=b'f').contains(b) {
*b -= b'a' - b'A';
flipped = true;
break;
}
}
assert!(flipped, "test fixture should contain at least one a-f");
assert!(decode_ref_wire(&wire).is_none());
}
#[test]
fn wire_rejects_short_input() {
let bad = b"deadbeef\n";
assert!(decode_ref_wire(bad).is_none());
}
#[test]
fn wire_rejects_non_hex() {
let mut wire = encode_ref_wire(&h("x"));
wire[1] = b'g';
assert!(decode_ref_wire(&wire).is_none());
}
#[test]
fn wire_tolerates_trailing_cr() {
let original = h("eol");
let mut buf = encode_ref_wire(&original).to_vec();
buf.insert(64, b'\r');
let parsed = decode_ref_wire(&buf).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn init_writes_default_head() {
let (_dir, mkit) = fresh_repo();
let head = read_head(&mkit).unwrap();
assert_eq!(head, Head::Branch("main".to_string()));
}
#[test]
fn write_and_read_branch_ref() {
let (_dir, mkit) = fresh_repo();
let commit = h("commit1");
write_ref(&mkit, "main", &commit).unwrap();
let read = read_ref(&mkit, "main").unwrap();
assert_eq!(read, Some(commit));
}
#[test]
fn resolve_head_with_no_commits_returns_none() {
let (_dir, mkit) = fresh_repo();
assert_eq!(resolve_head(&mkit).unwrap(), None);
}
#[test]
fn resolve_head_after_commit() {
let (_dir, mkit) = fresh_repo();
let commit = h("commit1");
write_ref(&mkit, "main", &commit).unwrap();
assert_eq!(resolve_head(&mkit).unwrap(), Some(commit));
}
#[test]
fn update_head_updates_current_branch() {
let (_dir, mkit) = fresh_repo();
let h1 = h("c1");
update_head(&mkit, &h1).unwrap();
assert_eq!(resolve_head(&mkit).unwrap(), Some(h1));
let h2 = h("c2");
update_head(&mkit, &h2).unwrap();
assert_eq!(resolve_head(&mkit).unwrap(), Some(h2));
}
#[test]
fn detached_head_round_trip() {
let dir = TempDir::new().unwrap();
let mkit = RepoLayout::single(dir.path());
fs::create_dir_all(mkit.common_dir()).unwrap();
let commit = h("detached");
write_head_detached(&mkit, &commit).unwrap();
match read_head(&mkit).unwrap() {
Head::Detached(got) => assert_eq!(got, commit),
other @ Head::Branch(_) => panic!("expected detached, got {other:?}"),
}
assert_eq!(resolve_head(&mkit).unwrap(), Some(commit));
}
#[test]
fn read_head_rejects_oversize_file() {
let dir = TempDir::new().unwrap();
let mkit = RepoLayout::single(dir.path());
fs::create_dir_all(mkit.common_dir()).unwrap();
fs::write(
mkit.head_file(),
vec![b'a'; usize::try_from(HEAD_MAX_BYTES).unwrap() + 1],
)
.unwrap();
let err = read_head(&mkit).unwrap_err();
assert!(matches!(err, RefError::InvalidHead));
}
#[test]
fn nonexistent_branch_returns_none() {
let (_dir, mkit) = fresh_repo();
assert_eq!(read_ref(&mkit, "nonexistent").unwrap(), None);
}
#[test]
fn read_ref_rejects_oversize_file() {
let (_dir, mkit) = fresh_repo();
let path = ref_path(mkit.common_dir(), HEADS_DIR, "main");
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(
&path,
vec![b'0'; usize::try_from(REF_FILE_MAX_BYTES).unwrap() + 1],
)
.unwrap();
let err = read_ref(&mkit, "main").unwrap_err();
assert!(matches!(err, RefError::InvalidRef(_)));
}
#[test]
fn list_refs_empty() {
let (_dir, mkit) = fresh_repo();
let refs = list_refs(&mkit).unwrap();
assert!(refs.is_empty());
}
#[test]
fn list_refs_sorted() {
let (_dir, mkit) = fresh_repo();
write_ref(&mkit, "main", &h("m")).unwrap();
write_ref(&mkit, "dev", &h("d")).unwrap();
let refs = list_refs(&mkit).unwrap();
assert_eq!(refs.len(), 2);
assert_eq!(refs[0].name, "dev");
assert_eq!(refs[1].name, "main");
}
#[test]
fn nested_refs_listed_recursively() {
let (_dir, mkit) = fresh_repo();
write_ref(&mkit, "feature/deep/topic", &h("nested")).unwrap();
let refs = list_refs(&mkit).unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].name, "feature/deep/topic");
}
#[test]
fn list_refs_silently_skips_entries_beyond_max_depth() {
let (_dir, mkit) = fresh_repo();
let deep_name = (0..40)
.map(|i| format!("d{i}"))
.collect::<Vec<_>>()
.join("/");
write_ref(&mkit, &deep_name, &h("deep")).unwrap();
write_ref(&mkit, "main", &h("shallow")).unwrap();
let refs = list_refs(&mkit).unwrap();
let names: Vec<&str> = refs.iter().map(|r| r.name.as_str()).collect();
assert!(names.contains(&"main"), "shallow ref must still be listed");
assert!(
!names.contains(&deep_name.as_str()),
"a ref nested beyond MAX_REF_DEPTH must be silently skipped, got {names:?}"
);
}
#[test]
fn delete_ref_basic() {
let (_dir, mkit) = fresh_repo();
write_ref(&mkit, "feature", &h("f")).unwrap();
delete_ref(&mkit, "feature").unwrap();
assert_eq!(read_ref(&mkit, "feature").unwrap(), None);
}
#[test]
fn delete_nonexistent_ref_errors() {
let (_dir, mkit) = fresh_repo();
let err = delete_ref(&mkit, "nope").unwrap_err();
assert!(matches!(err, RefError::NotFound(_)));
}
#[test]
fn refuse_delete_current_branch() {
let (_dir, mkit) = fresh_repo();
write_ref(&mkit, "main", &h("m")).unwrap();
let err = delete_ref_safe(&mkit, "main").unwrap_err();
assert!(matches!(err, RefError::CurrentBranch(_)));
}
#[test]
fn cas_any_clobbers() {
let (_dir, mkit) = fresh_repo();
update_ref(&mkit, "main", RefWriteCondition::Any, &h("a")).unwrap();
update_ref(&mkit, "main", RefWriteCondition::Any, &h("b")).unwrap();
assert_eq!(read_ref(&mkit, "main").unwrap(), Some(h("b")));
}
#[test]
fn cas_missing_succeeds_when_absent() {
let (_dir, mkit) = fresh_repo();
update_ref(&mkit, "main", RefWriteCondition::Missing, &h("a")).unwrap();
assert_eq!(read_ref(&mkit, "main").unwrap(), Some(h("a")));
}
#[test]
fn cas_missing_fails_when_present() {
let (_dir, mkit) = fresh_repo();
write_ref(&mkit, "main", &h("a")).unwrap();
let err = update_ref(&mkit, "main", RefWriteCondition::Missing, &h("b")).unwrap_err();
assert!(matches!(err, RefError::Conflict(_)));
}
#[test]
fn cas_match_succeeds_on_correct_hash() {
let (_dir, mkit) = fresh_repo();
write_ref(&mkit, "main", &h("a")).unwrap();
update_ref(&mkit, "main", RefWriteCondition::Match(h("a")), &h("b")).unwrap();
assert_eq!(read_ref(&mkit, "main").unwrap(), Some(h("b")));
}
#[test]
fn cas_match_fails_on_wrong_hash() {
let (_dir, mkit) = fresh_repo();
write_ref(&mkit, "main", &h("a")).unwrap();
let err = update_ref(&mkit, "main", RefWriteCondition::Match(h("z")), &h("b")).unwrap_err();
assert!(matches!(err, RefError::Conflict(_)));
}
#[test]
fn cas_match_fails_on_missing_ref() {
let (_dir, mkit) = fresh_repo();
let err = update_ref(&mkit, "main", RefWriteCondition::Match(h("a")), &h("b")).unwrap_err();
assert!(matches!(err, RefError::Conflict(_)));
}
#[test]
fn cas_match_race_never_loses_an_update_across_uncoordinated_callers() {
let (_dir, layout_a) = fresh_repo();
let layout_b = RepoLayout::linked(
layout_a.worktree_root().join("other-worktree"),
layout_a.common_dir().join("worktrees").join("other"),
layout_a.common_dir(),
);
let base = h("base");
write_ref(&layout_a, "main", &base).unwrap();
let iterations: usize = 500;
let mut double_success_iteration = None;
for i in 0..iterations {
update_ref(&layout_a, "main", RefWriteCondition::Any, &base).unwrap();
let val_a = h(&format!("race-a-{i}"));
let val_b = h(&format!("race-b-{i}"));
let barrier = Barrier::new(2);
let (result_a, result_b) = std::thread::scope(|scope| {
let handle_a = scope.spawn(|| {
barrier.wait();
update_ref(&layout_a, "main", RefWriteCondition::Match(base), &val_a)
});
let handle_b = scope.spawn(|| {
barrier.wait();
update_ref(&layout_b, "main", RefWriteCondition::Match(base), &val_b)
});
(handle_a.join().unwrap(), handle_b.join().unwrap())
});
if result_a.is_ok() && result_b.is_ok() {
double_success_iteration = Some(i);
break;
}
}
assert!(
double_success_iteration.is_none(),
"both uncoordinated Match CAS callers reported success on iteration \
{double_success_iteration:?} — an update was silently lost \
(INV-15/INV-6 violation)"
);
}
#[test]
fn cas_match_succeeds_repeatedly_when_uncontended() {
let (_dir, mkit) = fresh_repo();
let mut current = h("seed");
write_ref(&mkit, "main", ¤t).unwrap();
for i in 0..20 {
let next = h(&format!("v{i}"));
update_ref(&mkit, "main", RefWriteCondition::Match(current), &next).unwrap();
assert_eq!(read_ref(&mkit, "main").unwrap(), Some(next));
current = next;
}
}
#[test]
fn cas_delete_refuses_when_ref_moved_after_read() {
let (_dir, mkit) = fresh_repo();
let t = h("t");
let c = h("c");
write_ref(&mkit, "main", &t).unwrap();
let read_t = read_ref(&mkit, "main").unwrap().unwrap();
assert_eq!(read_t, t);
update_ref(&mkit, "main", RefWriteCondition::Match(t), &c).unwrap();
let err = delete_ref_if_matches(&mkit, "main", read_t).unwrap_err();
assert!(
matches!(err, RefError::Conflict(_)),
"expected Conflict, got {err:?}"
);
assert_eq!(
read_ref(&mkit, "main").unwrap(),
Some(c),
"the concurrently-landed commit must survive the refused delete untouched"
);
}
#[test]
fn cas_delete_refusal_leaves_ref_deletable_against_its_new_value() {
let (_dir, mkit) = fresh_repo();
let t = h("t");
let c = h("c");
write_ref(&mkit, "main", &t).unwrap();
update_ref(&mkit, "main", RefWriteCondition::Match(t), &c).unwrap();
assert!(matches!(
delete_ref_if_matches(&mkit, "main", t).unwrap_err(),
RefError::Conflict(_)
));
delete_ref_if_matches(&mkit, "main", c).unwrap();
assert_eq!(read_ref(&mkit, "main").unwrap(), None);
}
#[test]
fn cas_delete_fails_on_missing_ref() {
let (_dir, mkit) = fresh_repo();
let err = delete_ref_if_matches(&mkit, "main", h("anything")).unwrap_err();
assert!(matches!(err, RefError::Conflict(_)));
}
#[test]
fn cas_delete_vs_match_advance_race_never_lets_both_win_or_loses_the_advance() {
let (_dir, mkit) = fresh_repo();
let iterations: usize = 500;
let mut bad_iteration: Option<(usize, &'static str)> = None;
for i in 0..iterations {
let base = h(&format!("base-{i}"));
write_ref(&mkit, "main", &base).unwrap();
let new_tip = h(&format!("advanced-{i}"));
let barrier = Barrier::new(2);
let (advance_result, delete_result) = std::thread::scope(|scope| {
let advance_handle = scope.spawn(|| {
barrier.wait();
update_ref(&mkit, "main", RefWriteCondition::Match(base), &new_tip)
});
let delete_handle = scope.spawn(|| {
barrier.wait();
delete_ref_if_matches(&mkit, "main", base)
});
(
advance_handle.join().unwrap(),
delete_handle.join().unwrap(),
)
});
match (&advance_result, &delete_result) {
(Ok(()), Ok(())) => {
bad_iteration = Some((
i,
"both the concurrent advance and the concurrent delete reported success",
));
}
(Ok(()), Err(RefError::Conflict(_))) => {
if read_ref(&mkit, "main").unwrap() != Some(new_tip) {
bad_iteration = Some((
i,
"advance reported success but its value is not on disk — lost update",
));
}
}
(Err(RefError::Conflict(_)), Ok(())) => {
if read_ref(&mkit, "main").unwrap().is_some() {
bad_iteration = Some((
i,
"delete reported success but the ref is still present on disk",
));
}
}
(Err(RefError::Conflict(_)), Err(RefError::Conflict(_))) => {
bad_iteration = Some((
i,
"both the advance and the delete reported Conflict against a freshly-written base",
));
}
_ => {
bad_iteration = Some((i, "unexpected error variant"));
}
}
if bad_iteration.is_some() {
break;
}
}
assert!(
bad_iteration.is_none(),
"iteration {bad_iteration:?}: a concurrent Match-conditioned advance and a \
CAS-guarded delete on the same ref did not serialize correctly (issue #658)"
);
}
#[test]
fn cas_match_does_not_contend_across_different_refs() {
let (_dir, mkit) = fresh_repo();
write_ref(&mkit, "main", &h("m0")).unwrap();
write_ref(&mkit, "other", &h("o0")).unwrap();
let other_path = ref_path(mkit.common_dir(), HEADS_DIR, "other");
let other_lock_name = cas_lock_name(mkit.common_dir(), &other_path);
let common_dir = mkit.common_dir().to_path_buf();
let (holding_tx, holding_rx) = std::sync::mpsc::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
let holder = std::thread::spawn(move || {
let _lock = crate::repo_lock::acquire_default(&common_dir, &other_lock_name).unwrap();
holding_tx.send(()).unwrap();
release_rx.recv().unwrap();
});
holding_rx.recv().unwrap();
let start = std::time::Instant::now();
update_ref(&mkit, "main", RefWriteCondition::Match(h("m0")), &h("m1")).unwrap();
let elapsed = start.elapsed();
release_tx.send(()).unwrap();
holder.join().unwrap();
assert!(
elapsed < std::time::Duration::from_secs(1),
"Match CAS on \"main\" took {elapsed:?} while \"other\"'s ref lock was held \
elsewhere — refs are contending when they shouldn't be"
);
assert_eq!(read_ref(&mkit, "main").unwrap(), Some(h("m1")));
}
#[test]
fn write_rejects_invalid_branch_name() {
let (_dir, mkit) = fresh_repo();
let err = write_ref(&mkit, "../escape", &h("x")).unwrap_err();
assert!(matches!(err, RefError::InvalidRefName(_)));
let err = write_head_branch(&mkit, "bad//branch").unwrap_err();
assert!(matches!(err, RefError::InvalidRefName(_)));
}
#[test]
fn write_and_read_tag() {
let (_dir, mkit) = fresh_repo();
let commit = h("v1.0");
write_tag(&mkit, "v1.0", &commit).unwrap();
assert_eq!(read_tag(&mkit, "v1.0").unwrap(), Some(commit));
}
#[test]
fn list_tags_sorted() {
let (_dir, mkit) = fresh_repo();
write_tag(&mkit, "v2.0", &h("v2")).unwrap();
write_tag(&mkit, "v1.0", &h("v1")).unwrap();
write_tag(&mkit, "alpha", &h("a")).unwrap();
let tags = list_tags(&mkit).unwrap();
assert_eq!(
tags.iter().map(|r| r.name.as_str()).collect::<Vec<_>>(),
vec!["alpha", "v1.0", "v2.0"]
);
}
#[test]
fn tag_and_branch_same_name_independent() {
let (_dir, mkit) = fresh_repo();
let tag = h("tag");
let branch = h("branch");
write_tag(&mkit, "main", &tag).unwrap();
write_ref(&mkit, "main", &branch).unwrap();
assert_eq!(read_tag(&mkit, "main").unwrap(), Some(tag));
assert_eq!(read_ref(&mkit, "main").unwrap(), Some(branch));
}
#[test]
fn delete_tag_basic() {
let (_dir, mkit) = fresh_repo();
write_tag(&mkit, "release", &h("r")).unwrap();
delete_tag(&mkit, "release").unwrap();
assert_eq!(read_tag(&mkit, "release").unwrap(), None);
}
#[test]
fn delete_nonexistent_tag_errors() {
let (_dir, mkit) = fresh_repo();
let err = delete_tag(&mkit, "missing").unwrap_err();
assert!(matches!(err, RefError::NotFound(_)));
}
#[test]
fn load_shallow_returns_none_when_missing() {
let (_dir, mkit) = fresh_repo();
assert_eq!(load_shallow_boundaries(&mkit).unwrap(), None);
}
#[test]
fn write_and_load_shallow_round_trip() {
let (_dir, mkit) = fresh_repo();
let bs = vec![h("b1"), h("b2"), h("b3")];
write_shallow_boundaries(&mkit, &bs).unwrap();
let loaded = load_shallow_boundaries(&mkit).unwrap().unwrap();
assert_eq!(loaded.len(), 3);
for b in &bs {
assert!(loaded.contains(b));
}
}
#[test]
fn write_empty_shallow_removes_file() {
let (_dir, mkit) = fresh_repo();
write_shallow_boundaries(&mkit, &[h("x")]).unwrap();
assert!(load_shallow_boundaries(&mkit).unwrap().is_some());
write_shallow_boundaries(&mkit, &[]).unwrap();
assert_eq!(load_shallow_boundaries(&mkit).unwrap(), None);
}
#[test]
fn load_shallow_rejects_oversize_file() {
let (_dir, mkit) = fresh_repo();
let path = mkit.shallow_file();
fs::write(
&path,
vec![b'a'; usize::try_from(SHALLOW_MAX_BYTES).unwrap() + 1],
)
.unwrap();
let err = load_shallow_boundaries(&mkit).unwrap_err();
assert!(matches!(err, RefError::InvalidRef(_)));
}
#[test]
fn load_shallow_skips_invalid_lines() {
let (_dir, mkit) = fresh_repo();
let path = mkit.shallow_file();
let valid = h("ok");
let valid_hex = to_hex(&valid);
let mut content = String::new();
content.push_str("short\n");
content.push_str(&valid_hex);
content.push('\n');
content.push_str(&"z".repeat(64));
content.push('\n');
std::fs::write(&path, content).unwrap();
let loaded = load_shallow_boundaries(&mkit).unwrap().unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0], valid);
}
#[test]
fn write_remote_ref_loop_pays_one_dir_sync_per_ref_today() {
let (_dir, mkit) = fresh_repo();
let n: u64 = 25;
crate::atomic::testing::reset_dir_sync_calls();
for i in 0..n {
write_remote_ref(
&mkit,
"origin",
&format!("branch-{i}"),
&h(&format!("c{i}")),
)
.unwrap();
}
let calls = crate::atomic::testing::dir_sync_calls();
assert_eq!(
calls, n,
"the current per-ref write path must cost exactly one directory \
fsync per ref (O(N)); got {calls} for {n} refs"
);
}
#[test]
fn remote_ref_batch_pays_one_dir_sync_for_many_refs() {
let (_dir, mkit) = fresh_repo();
let n = 25;
let entries: Vec<(String, Hash)> = (0..n)
.map(|i| (format!("branch-{i}"), h(&format!("c{i}"))))
.collect();
crate::atomic::testing::reset_dir_sync_calls();
let mut batch = RemoteRefBatch::new(&mkit, "origin").unwrap();
for (branch, hash) in &entries {
batch.write(branch, hash).unwrap();
}
batch.commit().unwrap();
let calls = crate::atomic::testing::dir_sync_calls();
assert_eq!(
calls, 1,
"batching {n} tracking-ref writes into one flat remote \
namespace must cost exactly one directory fsync (O(1)), got {calls}"
);
}
#[test]
fn remote_ref_batch_matches_per_ref_loop_final_state() {
let (_dir, old_path) = fresh_repo();
let (_dir2, new_path) = fresh_repo();
let n = 12;
let entries: Vec<(String, Hash)> = (0..n)
.map(|i| (format!("team/branch-{i}"), h(&format!("state{i}"))))
.collect();
for (branch, hash) in &entries {
write_remote_ref(&old_path, "origin", branch, hash).unwrap();
}
let mut batch = RemoteRefBatch::new(&new_path, "origin").unwrap();
for (branch, hash) in &entries {
batch.write(branch, hash).unwrap();
}
batch.commit().unwrap();
for (branch, hash) in &entries {
let old_val = read_remote_ref(&old_path, "origin", branch).unwrap();
let new_val = read_remote_ref(&new_path, "origin", branch).unwrap();
assert_eq!(old_val, Some(*hash));
assert_eq!(new_val, Some(*hash));
assert_eq!(old_val, new_val, "branch {branch} diverged");
}
}
#[test]
fn remote_ref_batch_partial_failure_keeps_already_written_refs_visible() {
let (_dir, mkit) = fresh_repo();
let mut batch = RemoteRefBatch::new(&mkit, "origin").unwrap();
batch.write("good-1", &h("g1")).unwrap();
batch.write("good-2", &h("g2")).unwrap();
let err = batch.write("bad//name", &h("x")).unwrap_err();
assert!(matches!(err, RefError::InvalidRefName(_)));
batch.commit().unwrap();
assert_eq!(
read_remote_ref(&mkit, "origin", "good-1").unwrap(),
Some(h("g1"))
);
assert_eq!(
read_remote_ref(&mkit, "origin", "good-2").unwrap(),
Some(h("g2"))
);
let never_written = read_remote_ref(&mkit, "origin", "bad//name").unwrap_err();
assert!(matches!(never_written, RefError::InvalidRefName(_)));
}
#[test]
fn remote_ref_batch_rejects_invalid_remote_name() {
let (_dir, mkit) = fresh_repo();
let err = RemoteRefBatch::new(&mkit, "../escape").unwrap_err();
assert!(matches!(err, RefError::InvalidRefName(_)));
}
#[test]
fn remote_ref_batch_of_zero_entries_is_a_noop_commit() {
let (_dir, mkit) = fresh_repo();
crate::atomic::testing::reset_dir_sync_calls();
let batch = RemoteRefBatch::new(&mkit, "origin").unwrap();
batch.commit().unwrap();
assert_eq!(
crate::atomic::testing::dir_sync_calls(),
0,
"an empty batch must not touch any directory"
);
}
#[cfg(feature = "history-mmr")]
mod history_coupling {
use super::*;
use crate::history::{CommitHistory, TokioExecutor};
use std::sync::Arc;
#[test]
fn update_ref_with_history_appends_to_journal_under_lock() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist = CommitHistory::open_at(exec.clone(), &mkit, "main").unwrap();
let c1 = h("c1");
let c2 = h("c2");
update_ref_with_history(&mkit, "main", RefWriteCondition::Any, &c1, &mut hist).unwrap();
update_ref_with_history(&mkit, "main", RefWriteCondition::Match(c1), &c2, &mut hist)
.unwrap();
assert_eq!(read_ref(&mkit, "main").unwrap(), Some(c2));
assert_eq!(hist.len(), 2, "two appends → two leaves in the MMR");
}
#[test]
fn update_ref_with_history_does_not_contend_across_branches() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist = CommitHistory::open_at(exec, &mkit, "main").unwrap();
let other_lock_name = history_lock_name("other");
let common_dir = mkit.common_dir().to_path_buf();
let (holding_tx, holding_rx) = std::sync::mpsc::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
let holder = std::thread::spawn(move || {
let _lock =
crate::repo_lock::acquire_default(&common_dir, &other_lock_name).unwrap();
holding_tx.send(()).unwrap();
release_rx.recv().unwrap();
});
holding_rx.recv().unwrap();
let start = std::time::Instant::now();
update_ref_with_history(&mkit, "main", RefWriteCondition::Any, &h("m1"), &mut hist)
.unwrap();
let elapsed = start.elapsed();
release_tx.send(()).unwrap();
holder.join().unwrap();
assert!(
elapsed < std::time::Duration::from_secs(1),
"update_ref_with_history on \"main\" took {elapsed:?} while \"other\"'s \
per-branch lock was held elsewhere — branches are contending when they \
shouldn't be"
);
assert_eq!(read_ref(&mkit, "main").unwrap(), Some(h("m1")));
}
#[test]
fn update_ref_with_history_still_contends_on_the_same_branch() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist = CommitHistory::open_at(exec, &mkit, "main").unwrap();
let main_lock_name = history_lock_name("main");
let common_dir = mkit.common_dir().to_path_buf();
let (holding_tx, holding_rx) = std::sync::mpsc::channel();
let held_for = std::time::Duration::from_millis(200);
let holder = std::thread::spawn(move || {
let _lock =
crate::repo_lock::acquire_default(&common_dir, &main_lock_name).unwrap();
holding_tx.send(()).unwrap();
std::thread::sleep(held_for);
});
holding_rx.recv().unwrap();
let start = std::time::Instant::now();
update_ref_with_history(&mkit, "main", RefWriteCondition::Any, &h("m1"), &mut hist)
.unwrap();
let elapsed = start.elapsed();
holder.join().unwrap();
assert!(
elapsed >= held_for / 2,
"update_ref_with_history on \"main\" returned in {elapsed:?} while \"main\"'s \
own lock was held for {held_for:?} elsewhere — same-branch mutual exclusion \
is broken"
);
}
#[test]
fn update_ref_with_history_rejects_mem_history() {
let (_dir, mkit) = fresh_repo();
let mut mem_hist = CommitHistory::open();
let err = update_ref_with_history(
&mkit,
"main",
RefWriteCondition::Any,
&h("x"),
&mut mem_hist,
)
.unwrap_err();
assert!(matches!(err, RefError::InvalidRef(_)));
}
#[test]
fn update_ref_with_history_rejects_branch_mismatch() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist = CommitHistory::open_at(exec, &mkit, "main").unwrap();
let err = update_ref_with_history(
&mkit,
"feature",
RefWriteCondition::Any,
&h("x"),
&mut hist,
)
.unwrap_err();
assert!(matches!(err, RefError::InvalidRef(_)));
}
#[test]
fn update_ref_with_history_cas_failure_does_not_append() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist = CommitHistory::open_at(exec, &mkit, "main").unwrap();
write_ref(&mkit, "main", &h("existing")).unwrap();
let err = update_ref_with_history(
&mkit,
"main",
RefWriteCondition::Missing,
&h("new"),
&mut hist,
)
.unwrap_err();
assert!(matches!(err, RefError::Conflict(_)));
assert_eq!(
hist.len(),
0,
"CAS failure must NOT have appended to history"
);
}
#[test]
fn update_ref_with_history_heals_one_ahead_gap_before_appending_new_value() {
use crate::history::{Position, verify_inclusion};
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let c0 = h("c0");
let c1 = h("c1");
let c2 = h("c2");
let mut hist = CommitHistory::open_at(exec, &mkit, "main").unwrap();
update_ref_with_history(&mkit, "main", RefWriteCondition::Any, &c0, &mut hist).unwrap();
assert_eq!(hist.len(), 1);
write_ref(&mkit, "main", &c1).unwrap();
assert_eq!(hist.len(), 1, "journal is still missing c1's append");
update_ref_with_history(&mkit, "main", RefWriteCondition::Match(c1), &c2, &mut hist)
.unwrap();
assert_eq!(hist.len(), 3);
let root = hist.root();
let proof0 = hist.prove(Position(0)).unwrap();
assert!(verify_inclusion(&c0, Position(0), &proof0, &root));
let proof1 = hist.prove(Position(1)).unwrap();
assert!(verify_inclusion(&c1, Position(1), &proof1, &root));
let proof2 = hist.prove(Position(2)).unwrap();
assert!(verify_inclusion(&c2, Position(2), &proof2, &root));
}
#[test]
fn update_ref_with_history_concurrent_handles_do_not_interleave_or_corrupt() {
use crate::history::{Position, verify_inclusion};
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist_a = CommitHistory::open_at(exec.clone(), &mkit, "main").unwrap();
let mut hist_b = CommitHistory::open_at(exec, &mkit, "main").unwrap();
let a1 = h("a1");
let b1 = h("b1");
update_ref_with_history(&mkit, "main", RefWriteCondition::Any, &a1, &mut hist_a)
.unwrap();
update_ref_with_history(&mkit, "main", RefWriteCondition::Any, &b1, &mut hist_b)
.unwrap();
assert_eq!(read_ref(&mkit, "main").unwrap(), Some(b1));
assert_eq!(
hist_b.len(),
2,
"b's append must land after a's, not clobber it"
);
let root = hist_b.root();
let proof0 = hist_b.prove(Position(0)).unwrap();
assert!(verify_inclusion(&a1, Position(0), &proof0, &root));
let proof1 = hist_b.prove(Position(1)).unwrap();
assert!(verify_inclusion(&b1, Position(1), &proof1, &root));
}
#[test]
fn delete_ref_with_history_prevents_partition_reuse_on_recreate() {
use crate::history::{Position, verify_inclusion};
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let a1 = h("a1");
let a2 = h("a2");
let mut hist_a = CommitHistory::open_at(exec.clone(), &mkit, "feature").unwrap();
update_ref_with_history(&mkit, "feature", RefWriteCondition::Any, &a1, &mut hist_a)
.unwrap();
update_ref_with_history(
&mkit,
"feature",
RefWriteCondition::Match(a1),
&a2,
&mut hist_a,
)
.unwrap();
assert_eq!(hist_a.len(), 2);
drop(hist_a);
delete_ref_with_history(&mkit, "feature", exec.clone()).unwrap();
assert_eq!(read_ref(&mkit, "feature").unwrap(), None);
let b1 = h("b1");
let mut hist_b = CommitHistory::open_at(exec, &mkit, "feature").unwrap();
assert_eq!(
hist_b.len(),
0,
"recreated branch must not inherit the deleted incarnation's leaves"
);
update_ref_with_history(
&mkit,
"feature",
RefWriteCondition::Missing,
&b1,
&mut hist_b,
)
.unwrap();
assert_eq!(
hist_b.len(),
1,
"the new incarnation's journal must contain exactly its own one leaf"
);
let root = hist_b.root();
for pos in 0..hist_b.len() {
let proof = hist_b.prove(Position(pos)).unwrap();
assert!(
!verify_inclusion(&a1, Position(pos), &proof, &root),
"deleted incarnation's leaf a1 must not verify against the new root"
);
assert!(
!verify_inclusion(&a2, Position(pos), &proof, &root),
"deleted incarnation's leaf a2 must not verify against the new root"
);
}
}
#[test]
fn delete_ref_with_history_races_update_without_tearing_ref_and_journal() {
let iterations = 50;
for i in 0..iterations {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let seed = h(&format!("seed-{i}"));
let mut seed_hist = CommitHistory::open_at(exec.clone(), &mkit, "feature").unwrap();
update_ref_with_history(
&mkit,
"feature",
RefWriteCondition::Missing,
&seed,
&mut seed_hist,
)
.unwrap();
drop(seed_hist);
let next = h(&format!("next-{i}"));
let mut update_hist =
CommitHistory::open_at(exec.clone(), &mkit, "feature").unwrap();
let barrier = Barrier::new(2);
std::thread::scope(|scope| {
scope.spawn(|| {
barrier.wait();
let _ = delete_ref_with_history(&mkit, "feature", exec.clone());
});
scope.spawn(|| {
barrier.wait();
let _ = update_ref_with_history(
&mkit,
"feature",
RefWriteCondition::Match(seed),
&next,
&mut update_hist,
);
});
});
drop(update_hist);
let ref_exists = read_ref(&mkit, "feature").unwrap().is_some();
let leaves = CommitHistory::open_at(exec, &mkit, "feature")
.unwrap()
.len();
assert_eq!(
ref_exists,
leaves > 0,
"iteration {i}: torn state after racing delete vs. update — \
ref_exists={ref_exists}, journal has {leaves} leaves \
(INV-4/INV-18-adjacent: a live ref must have a non-empty \
journal, and a deleted branch's journal must not retain \
leaves from before the delete)"
);
}
}
#[test]
fn delete_ref_with_history_removes_journal_partition_from_disk() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist = CommitHistory::open_at(exec.clone(), &mkit, "feature").unwrap();
update_ref_with_history(&mkit, "feature", RefWriteCondition::Any, &h("a"), &mut hist)
.unwrap();
drop(hist);
let journal_blobs = mkit.history_dir().join("feature__journal-blobs");
assert!(journal_blobs.exists());
delete_ref_with_history(&mkit, "feature", exec).unwrap();
assert!(
!journal_blobs.exists(),
"delete_ref_with_history must remove the on-disk journal partition"
);
}
#[test]
fn delete_ref_with_history_errors_on_missing_branch_without_touching_journal() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let err = delete_ref_with_history(&mkit, "nope", exec).unwrap_err();
assert!(matches!(err, RefError::NotFound(_)));
}
#[test]
fn delete_ref_safe_with_history_refuses_current_branch() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist = CommitHistory::open_at(exec.clone(), &mkit, "main").unwrap();
update_ref_with_history(&mkit, "main", RefWriteCondition::Any, &h("m"), &mut hist)
.unwrap();
drop(hist);
let err = delete_ref_safe_with_history(&mkit, "main", exec).unwrap_err();
assert!(matches!(err, RefError::CurrentBranch(_)));
assert!(mkit.history_dir().join("main__journal-blobs").exists());
}
#[test]
fn delete_ref_with_history_supports_rename_by_destroying_the_old_name_only() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist_old = CommitHistory::open_at(exec.clone(), &mkit, "old").unwrap();
update_ref_with_history(
&mkit,
"old",
RefWriteCondition::Any,
&h("o1"),
&mut hist_old,
)
.unwrap();
drop(hist_old);
let mut hist_new = CommitHistory::open_at(exec.clone(), &mkit, "new").unwrap();
update_ref_with_history(
&mkit,
"new",
RefWriteCondition::Missing,
&h("o1"),
&mut hist_new,
)
.unwrap();
assert_eq!(hist_new.len(), 1);
drop(hist_new);
delete_ref_with_history(&mkit, "old", exec.clone()).unwrap();
assert_eq!(read_ref(&mkit, "old").unwrap(), None);
assert_eq!(read_ref(&mkit, "new").unwrap(), Some(h("o1")));
assert!(!mkit.history_dir().join("old__journal-blobs").exists());
assert!(mkit.history_dir().join("new__journal-blobs").exists());
let hist_old_reopened = CommitHistory::open_at(exec, &mkit, "old").unwrap();
assert_eq!(hist_old_reopened.len(), 0);
}
#[test]
fn delete_ref_with_history_if_matches_succeeds_and_destroys_journal_on_match() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist = CommitHistory::open_at(exec.clone(), &mkit, "feature").unwrap();
let tip = h("tip");
update_ref_with_history(&mkit, "feature", RefWriteCondition::Any, &tip, &mut hist)
.unwrap();
drop(hist);
delete_ref_with_history_if_matches(&mkit, "feature", tip, exec).unwrap();
assert_eq!(read_ref(&mkit, "feature").unwrap(), None);
assert!(!mkit.history_dir().join("feature__journal-blobs").exists());
}
#[test]
fn delete_ref_with_history_if_matches_leaves_ref_and_journal_untouched_on_conflict() {
let (_dir, mkit) = fresh_repo();
let exec = Arc::new(TokioExecutor::new().unwrap());
let mut hist = CommitHistory::open_at(exec.clone(), &mkit, "feature").unwrap();
let stale = h("stale");
let current = h("current");
update_ref_with_history(&mkit, "feature", RefWriteCondition::Any, &stale, &mut hist)
.unwrap();
update_ref_with_history(
&mkit,
"feature",
RefWriteCondition::Match(stale),
¤t,
&mut hist,
)
.unwrap();
let leaves_before = hist.len();
drop(hist);
let err = delete_ref_with_history_if_matches(&mkit, "feature", stale, exec.clone())
.unwrap_err();
assert!(matches!(err, RefError::Conflict(_)));
assert_eq!(read_ref(&mkit, "feature").unwrap(), Some(current));
let hist_reopened = CommitHistory::open_at(exec, &mkit, "feature").unwrap();
assert_eq!(
hist_reopened.len(),
leaves_before,
"a refused conditional delete must not touch the journal"
);
}
}
}