use objects::{
error::{HeddleError, Result},
object::{StateId, SyntheticFrontierName},
};
use super::{RefManager, format_state_id_text, parse_state_id_text};
use crate::fs_atomic::create_dir_all_durable;
impl RefManager {
fn synthetic_dir(&self) -> std::path::PathBuf {
self.refs_dir().join("synthetic")
}
fn synthetic_frontier_path(&self, name: &SyntheticFrontierName) -> std::path::PathBuf {
self.synthetic_dir()
.join(objects::name_encoding::name_path(&synthetic_storage_name(
name,
)))
.join("value")
}
pub fn set_synthetic_frontier(
&self,
name: &SyntheticFrontierName,
state: &StateId,
) -> Result<()> {
self.write_chokepoint(|_lock| {
objects::name_encoding::write_name_entry(
&self.synthetic_dir(),
&synthetic_storage_name(name),
)?;
let path = self.synthetic_frontier_path(name);
let parent = path.parent().ok_or_else(|| {
HeddleError::Config("invalid synthetic frontier path".to_string())
})?;
create_dir_all_durable(parent)?;
self.write_string(&path, &format_state_id_text(state))?;
Ok(())
})
}
pub fn get_synthetic_frontier(&self, name: &SyntheticFrontierName) -> Result<Option<StateId>> {
objects::name_encoding::verify_name_entry(
&self.synthetic_dir(),
&synthetic_storage_name(name),
)?;
let path = self.synthetic_frontier_path(name);
match self.read_optional_string(&path)? {
Some(contents) => parse_state_id_text(contents.trim())
.map(Some)
.map_err(|error| HeddleError::InvalidObject(error.to_string())),
None => Ok(None),
}
}
pub fn list_synthetic_frontiers(&self) -> Result<Vec<(SyntheticFrontierName, StateId)>> {
let dir = self.synthetic_dir();
if !dir.exists() {
return Ok(Vec::new());
}
let mut out = Vec::new();
for decoded in self.list_refs_recursive(&dir, "")? {
let Some((thread, change)) = decoded
.strip_prefix("heddle/frontier/")
.and_then(|suffix| suffix.rsplit_once('/'))
else {
continue;
};
let mapped = format!(
"heddle/frontier/{}/{}",
objects::name_encoding::native_git_name(thread),
change
);
let Ok(name) = SyntheticFrontierName::parse(&mapped) else {
continue;
};
let Some(state) = self.get_synthetic_frontier(&name)? else {
continue;
};
out.push((name, state));
}
out.sort_by(|a, b| a.0.cmp(&b.0));
Ok(out)
}
}
fn synthetic_storage_name(name: &SyntheticFrontierName) -> String {
format!(
"heddle/frontier/{}/{}",
objects::name_encoding::git_name(name.thread()),
name.change_id().to_string_full()
)
}
#[cfg(test)]
mod tests {
use objects::object::{ChangeId, ThreadName};
use tempfile::TempDir;
use super::*;
use crate::refs::fresh_state_id;
fn cid(last: u8) -> ChangeId {
let mut bytes = [0u8; 16];
bytes[15] = last;
ChangeId::from_bytes(bytes)
}
#[test]
fn prefix_sharing_siblings_are_distinct_and_individually_fetchable() {
let temp = TempDir::new().unwrap();
let refs = RefManager::new(temp.path());
let mut left_bytes = [0xaa; 16];
left_bytes[15] = 1;
let mut right_bytes = [0xaa; 16];
right_bytes[15] = 2;
let left = SyntheticFrontierName::new("main", ChangeId::from_bytes(left_bytes)).unwrap();
let right = SyntheticFrontierName::new("main", ChangeId::from_bytes(right_bytes)).unwrap();
let left_state = fresh_state_id();
let right_state = fresh_state_id();
refs.set_synthetic_frontier(&left, &left_state).unwrap();
refs.set_synthetic_frontier(&right, &right_state).unwrap();
assert_ne!(left.as_name(), right.as_name());
assert_eq!(
refs.get_synthetic_frontier(&left).unwrap(),
Some(left_state)
);
assert_eq!(
refs.get_synthetic_frontier(&right).unwrap(),
Some(right_state)
);
assert_eq!(refs.list_synthetic_frontiers().unwrap().len(), 2);
}
#[test]
fn long_reserved_import_name_has_bounded_exact_synthetic_storage() {
let temp = TempDir::new().expect("refs");
let refs = RefManager::new(temp.path());
let git = format!("heddle/{}", "界".repeat(333));
let thread = ThreadName::from_git_branch(&git).expect("Git branch");
let name = SyntheticFrontierName::new(&thread, cid(9)).expect("synthetic");
let state = fresh_state_id();
refs.set_thread(&thread, &state)
.expect("long imported thread");
refs.set_synthetic_frontier(&name, &state)
.expect("long synthetic");
assert_eq!(refs.list_threads().expect("threads"), vec![thread]);
assert_eq!(
refs.list_synthetic_frontiers().expect("synthetics"),
vec![(name, state)]
);
}
#[test]
fn user_thread_at_change_id_does_not_overwrite_synthetic_root() {
let temp = TempDir::new().unwrap();
let refs = RefManager::new(temp.path());
let change = cid(7);
let synthetic = SyntheticFrontierName::new("main", change).unwrap();
let synthetic_state = fresh_state_id();
let user_state = fresh_state_id();
let user = ThreadName::try_new(format!("main@{}", change.to_string_full())).unwrap();
refs.set_synthetic_frontier(&synthetic, &synthetic_state)
.unwrap();
refs.set_thread(&user, &user_state).unwrap();
assert_eq!(
refs.get_synthetic_frontier(&synthetic).unwrap(),
Some(synthetic_state)
);
assert_eq!(refs.get_thread(&user).unwrap(), Some(user_state));
assert!(
refs.set_thread(&ThreadName::new(synthetic.as_name()), &user_state)
.is_err()
);
}
}