use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct EddaPaths {
pub root: PathBuf,
pub edda_dir: PathBuf,
pub ledger_dir: PathBuf,
pub ledger_db: PathBuf,
pub blobs_dir: PathBuf,
pub branches_dir: PathBuf,
pub drafts_dir: PathBuf,
pub lock_file: PathBuf,
pub config_json: PathBuf,
pub patterns_dir: PathBuf,
pub blob_meta_json: PathBuf,
pub tombstones_jsonl: PathBuf,
pub archive_dir: PathBuf,
pub archive_blobs_dir: PathBuf,
}
impl EddaPaths {
pub fn discover(repo_root: impl Into<PathBuf>) -> Self {
let root = repo_root.into();
let edda_dir = root.join(".edda");
let ledger_dir = edda_dir.join("ledger");
let archive_dir = edda_dir.join("archive");
Self {
ledger_db: edda_dir.join("ledger.db"),
blobs_dir: ledger_dir.join("blobs"),
blob_meta_json: ledger_dir.join("blob_meta.json"),
tombstones_jsonl: ledger_dir.join("tombstones.jsonl"),
branches_dir: edda_dir.join("branches"),
drafts_dir: edda_dir.join("drafts"),
lock_file: edda_dir.join("LOCK"),
config_json: edda_dir.join("config.json"),
patterns_dir: edda_dir.join("patterns"),
archive_blobs_dir: archive_dir.join("blobs"),
archive_dir,
ledger_dir,
edda_dir,
root,
}
}
pub fn ensure_layout(&self) -> anyhow::Result<()> {
for dir in [
&self.ledger_dir,
&self.blobs_dir,
&self.branches_dir,
&self.drafts_dir,
&self.patterns_dir,
] {
std::fs::create_dir_all(dir)?;
}
Ok(())
}
pub fn is_initialized(&self) -> bool {
self.edda_dir.is_dir()
}
pub fn branch_dir(&self, name: &str) -> PathBuf {
self.branches_dir.join(name)
}
}
impl EddaPaths {
pub fn find_root(start: &Path) -> Option<PathBuf> {
let mut cur = start.to_path_buf();
loop {
if cur.join(".edda").is_dir() {
return Some(cur);
}
if !cur.pop() {
break;
}
}
edda_core::git::resolve_git_root(start).filter(|root| root.join(".edda").is_dir())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discover_builds_correct_paths() {
let p = EddaPaths::discover("/tmp/repo");
assert_eq!(p.edda_dir, PathBuf::from("/tmp/repo/.edda"));
assert_eq!(p.blobs_dir, PathBuf::from("/tmp/repo/.edda/ledger/blobs"));
assert_eq!(p.lock_file, PathBuf::from("/tmp/repo/.edda/LOCK"));
assert_eq!(p.patterns_dir, PathBuf::from("/tmp/repo/.edda/patterns"));
assert_eq!(
p.blob_meta_json,
PathBuf::from("/tmp/repo/.edda/ledger/blob_meta.json")
);
assert_eq!(
p.tombstones_jsonl,
PathBuf::from("/tmp/repo/.edda/ledger/tombstones.jsonl")
);
assert_eq!(p.archive_dir, PathBuf::from("/tmp/repo/.edda/archive"));
assert_eq!(
p.archive_blobs_dir,
PathBuf::from("/tmp/repo/.edda/archive/blobs")
);
}
#[test]
fn ensure_layout_creates_dirs() {
let tmp = std::env::temp_dir().join(format!("edda_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let p = EddaPaths::discover(&tmp);
p.ensure_layout().unwrap();
assert!(p.ledger_dir.is_dir());
assert!(p.blobs_dir.is_dir());
assert!(p.branches_dir.is_dir());
assert!(p.drafts_dir.is_dir());
assert!(p.patterns_dir.is_dir());
let _ = std::fs::remove_dir_all(&tmp);
}
use std::sync::atomic::{AtomicU64, Ordering};
static PATH_TEST_CTR: AtomicU64 = AtomicU64::new(0);
fn unique_tmp(label: &str) -> PathBuf {
let n = PATH_TEST_CTR.fetch_add(1, Ordering::SeqCst);
std::env::temp_dir().join(format!("edda_path_{label}_{}_{n}", std::process::id()))
}
#[test]
fn find_root_walks_up_to_edda_dir() {
let tmp = unique_tmp("walkup");
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(tmp.join(".edda")).unwrap();
let deep = tmp.join("sub").join("deep");
std::fs::create_dir_all(&deep).unwrap();
let found = EddaPaths::find_root(&deep);
assert_eq!(found.unwrap(), tmp);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn find_root_worktree_outside_repo() {
let tmp = unique_tmp("wt_outside");
let _ = std::fs::remove_dir_all(&tmp);
let repo = tmp.join("repo");
let wt = tmp.join("wt");
std::fs::create_dir_all(repo.join(".git").join("worktrees").join("feat-x")).unwrap();
std::fs::create_dir_all(repo.join(".edda")).unwrap();
std::fs::create_dir_all(&wt).unwrap();
let gitdir = repo.join(".git").join("worktrees").join("feat-x");
let gitdir_str = gitdir
.canonicalize()
.unwrap()
.to_string_lossy()
.replace('\\', "/");
std::fs::write(wt.join(".git"), format!("gitdir: {gitdir_str}")).unwrap();
let found = EddaPaths::find_root(&wt);
assert!(found.is_some(), "should resolve worktree to main repo");
assert!(found.unwrap().join(".edda").is_dir());
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn find_root_non_git_no_edda_returns_none() {
let tmp = unique_tmp("no_git");
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).unwrap();
assert!(EddaPaths::find_root(&tmp).is_none());
let _ = std::fs::remove_dir_all(&tmp);
}
}