use somatize_core::error::{Result, SomaError};
use std::fs;
use std::path::{Path, PathBuf};
pub const PARENT_ENV: &str = "SOMA_PARENT_RUN";
pub fn head_path(root: impl AsRef<Path>) -> PathBuf {
root.as_ref().join("HEAD")
}
pub fn read_head(root: impl AsRef<Path>) -> Option<String> {
let text = fs::read_to_string(head_path(root)).ok()?;
let trimmed = text.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
}
pub fn write_head(root: impl AsRef<Path>, run_id: &str) -> Result<()> {
let root = root.as_ref();
fs::create_dir_all(root)?;
let final_path = head_path(root);
let tmp = final_path.with_extension("tmp");
fs::write(&tmp, format!("{run_id}\n"))?;
fs::rename(&tmp, &final_path)?;
Ok(())
}
pub fn clear_head(root: impl AsRef<Path>) -> Result<()> {
match fs::remove_file(head_path(root)) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(SomaError::Io(e)),
}
}
pub fn run_exists(root: impl AsRef<Path>, run_id: &str) -> bool {
root.as_ref()
.join("runs")
.join(run_id)
.join("manifest.json")
.exists()
}
pub fn checkout(root: impl AsRef<Path>, run_id: &str) -> Result<()> {
let root = root.as_ref();
if !run_exists(root, run_id) {
return Err(SomaError::Other(format!(
"no run '{run_id}' under {}/runs — checkout needs a run that exists",
root.display()
)));
}
write_head(root, run_id)
}
pub fn resolve_parent(root: impl AsRef<Path>, explicit: Option<&str>) -> Option<String> {
let root = root.as_ref();
let env = std::env::var(PARENT_ENV).ok();
resolve_parent_from(explicit, env.as_deref(), || read_head(root))
}
pub fn resolve_parent_from(
explicit: Option<&str>,
env: Option<&str>,
head: impl FnOnce() -> Option<String>,
) -> Option<String> {
let non_empty = |s: &str| {
let s = s.trim();
(!s.is_empty()).then(|| s.to_string())
};
explicit
.and_then(non_empty)
.or_else(|| env.and_then(non_empty))
.or_else(head)
}
pub fn advance_head(root: impl AsRef<Path>, run_id: &str) -> bool {
write_head(root, run_id).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn head_roundtrips_and_tolerates_absence() {
let root = TempDir::new().unwrap();
assert_eq!(read_head(root.path()), None);
write_head(root.path(), "run_a").unwrap();
assert_eq!(read_head(root.path()).as_deref(), Some("run_a"));
let raw = fs::read_to_string(head_path(root.path())).unwrap();
assert_eq!(raw, "run_a\n");
write_head(root.path(), "run_b").unwrap();
assert_eq!(read_head(root.path()).as_deref(), Some("run_b"));
clear_head(root.path()).unwrap();
assert_eq!(read_head(root.path()), None);
clear_head(root.path()).unwrap();
}
#[test]
fn a_blank_head_reads_as_no_parent() {
let root = TempDir::new().unwrap();
fs::write(head_path(root.path()), " \n").unwrap();
assert_eq!(read_head(root.path()), None);
}
#[test]
fn write_head_creates_the_root() {
let root = TempDir::new().unwrap();
let nested = root.path().join("deep").join(".soma");
write_head(&nested, "run_x").unwrap();
assert_eq!(read_head(&nested).as_deref(), Some("run_x"));
}
#[test]
fn precedence_is_explicit_then_env_then_head() {
let head = || Some("from_head".to_string());
assert_eq!(
resolve_parent_from(Some("explicit"), Some("env"), head).as_deref(),
Some("explicit")
);
assert_eq!(
resolve_parent_from(None, Some("env"), head).as_deref(),
Some("env")
);
assert_eq!(
resolve_parent_from(None, None, head).as_deref(),
Some("from_head")
);
assert_eq!(resolve_parent_from(None, None, || None), None);
assert_eq!(resolve_parent_from(Some(" "), Some(""), || None), None);
}
#[test]
fn head_is_not_read_when_a_parent_is_already_known() {
let mut read = false;
let head = || {
read = true;
Some("from_head".to_string())
};
assert_eq!(
resolve_parent_from(Some("explicit"), None, head).as_deref(),
Some("explicit")
);
assert!(!read, "the filesystem is only touched as a last resort");
}
#[test]
fn checkout_refuses_a_run_that_does_not_exist() {
let root = TempDir::new().unwrap();
let err = checkout(root.path(), "typo_run").unwrap_err();
assert!(err.to_string().contains("no run 'typo_run'"), "{err}");
assert_eq!(read_head(root.path()), None, "HEAD must not move");
let run_dir = root.path().join("runs").join("run_real");
fs::create_dir_all(&run_dir).unwrap();
fs::write(run_dir.join("manifest.json"), "{}").unwrap();
assert!(run_exists(root.path(), "run_real"));
checkout(root.path(), "run_real").unwrap();
assert_eq!(read_head(root.path()).as_deref(), Some("run_real"));
}
#[test]
fn advancing_head_reports_whether_it_moved() {
let root = TempDir::new().unwrap();
assert!(advance_head(root.path(), "run_1"));
assert_eq!(read_head(root.path()).as_deref(), Some("run_1"));
}
}