← snapshot
2666 bytes
//! Named refs: the mutable edge of an immutable DAG.
use std::collections::BTreeMap;
use jac_core::SnapshotId;
use thiserror::Error;
/// A validated ref name (`main`, `feature/retry`).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RefName(String);
impl RefName {
/// Parses and validates a ref name.
///
/// # Errors
///
/// Returns [`RefError::InvalidName`] if empty, or containing empty
/// segments, leading/trailing `/`, or `.`/`..` segments.
pub fn parse(s: &str) -> Result<Self, RefError> {
if s.is_empty() || s.starts_with('/') || s.ends_with('/') {
return Err(RefError::InvalidName);
}
for seg in s.split('/') {
if seg.is_empty() || seg == "." || seg == ".." {
return Err(RefError::InvalidName);
}
}
Ok(Self(s.to_owned()))
}
/// The name as a string slice.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl core::fmt::Display for RefName {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.0)
}
}
/// Failures naming refs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum RefError {
/// The name was empty or malformed.
#[error("invalid ref name")]
InvalidName,
}
/// Where refs live.
pub trait RefStore {
/// Points `name` at `to`, creating it if absent.
fn set(&mut self, name: &RefName, to: SnapshotId);
/// Where `name` points, if anywhere.
fn get(&self, name: &RefName) -> Option<SnapshotId>;
}
/// In-memory ref store for tests and the demo.
#[derive(Debug, Default)]
pub struct MemoryRefStore {
refs: BTreeMap<RefName, SnapshotId>,
}
impl MemoryRefStore {
/// An empty store.
#[must_use]
pub const fn new() -> Self {
Self {
refs: BTreeMap::new(),
}
}
/// All refs and their heads, in name order.
pub fn iter(&self) -> impl Iterator<Item = (&RefName, SnapshotId)> {
self.refs.iter().map(|(name, id)| (name, *id))
}
}
impl RefStore for MemoryRefStore {
fn set(&mut self, name: &RefName, to: SnapshotId) {
self.refs.insert(name.clone(), to);
}
fn get(&self, name: &RefName) -> Option<SnapshotId> {
self.refs.get(name).copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_are_validated() {
for bad in ["", "/x", "x/", "a//b", "a/./b", "a/../b"] {
assert!(RefName::parse(bad).is_err(), "{bad:?} should be rejected");
}
assert!(RefName::parse("feature/retry").is_ok());
}
}