1use std::path::{Path, PathBuf};
2
3#[derive(Debug, Clone)]
5pub struct EddaPaths {
6 pub root: PathBuf,
7 pub edda_dir: PathBuf,
8 pub ledger_dir: PathBuf,
9 pub ledger_db: PathBuf,
10 pub blobs_dir: PathBuf,
11 pub branches_dir: PathBuf,
12 pub drafts_dir: PathBuf,
13 pub lock_file: PathBuf,
14 pub config_json: PathBuf,
15 pub patterns_dir: PathBuf,
16 pub blob_meta_json: PathBuf,
17 pub tombstones_jsonl: PathBuf,
18 pub archive_dir: PathBuf,
19 pub archive_blobs_dir: PathBuf,
20}
21
22impl EddaPaths {
23 pub fn discover(repo_root: impl Into<PathBuf>) -> Self {
25 let root = repo_root.into();
26 let edda_dir = root.join(".edda");
27 let ledger_dir = edda_dir.join("ledger");
28 let archive_dir = edda_dir.join("archive");
29 Self {
30 ledger_db: edda_dir.join("ledger.db"),
31 blobs_dir: ledger_dir.join("blobs"),
32 blob_meta_json: ledger_dir.join("blob_meta.json"),
33 tombstones_jsonl: ledger_dir.join("tombstones.jsonl"),
34 branches_dir: edda_dir.join("branches"),
35 drafts_dir: edda_dir.join("drafts"),
36 lock_file: edda_dir.join("LOCK"),
37 config_json: edda_dir.join("config.json"),
38 patterns_dir: edda_dir.join("patterns"),
39 archive_blobs_dir: archive_dir.join("blobs"),
40 archive_dir,
41 ledger_dir,
42 edda_dir,
43 root,
44 }
45 }
46
47 pub fn ensure_layout(&self) -> anyhow::Result<()> {
49 for dir in [
50 &self.ledger_dir,
51 &self.blobs_dir,
52 &self.branches_dir,
53 &self.drafts_dir,
54 &self.patterns_dir,
55 ] {
56 std::fs::create_dir_all(dir)?;
57 }
58 Ok(())
59 }
60
61 pub fn is_initialized(&self) -> bool {
63 self.edda_dir.is_dir()
64 }
65
66 pub fn branch_dir(&self, name: &str) -> PathBuf {
68 self.branches_dir.join(name)
69 }
70}
71
72impl EddaPaths {
73 pub fn find_root(start: &Path) -> Option<PathBuf> {
81 let mut cur = start.to_path_buf();
83 loop {
84 if cur.join(".edda").is_dir() {
85 return Some(cur);
86 }
87 if !cur.pop() {
88 break;
89 }
90 }
91
92 edda_core::git::resolve_git_root(start).filter(|root| root.join(".edda").is_dir())
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn discover_builds_correct_paths() {
103 let p = EddaPaths::discover("/tmp/repo");
104 assert_eq!(p.edda_dir, PathBuf::from("/tmp/repo/.edda"));
105 assert_eq!(p.blobs_dir, PathBuf::from("/tmp/repo/.edda/ledger/blobs"));
106 assert_eq!(p.lock_file, PathBuf::from("/tmp/repo/.edda/LOCK"));
107 assert_eq!(p.patterns_dir, PathBuf::from("/tmp/repo/.edda/patterns"));
108 assert_eq!(
109 p.blob_meta_json,
110 PathBuf::from("/tmp/repo/.edda/ledger/blob_meta.json")
111 );
112 assert_eq!(
113 p.tombstones_jsonl,
114 PathBuf::from("/tmp/repo/.edda/ledger/tombstones.jsonl")
115 );
116 assert_eq!(p.archive_dir, PathBuf::from("/tmp/repo/.edda/archive"));
117 assert_eq!(
118 p.archive_blobs_dir,
119 PathBuf::from("/tmp/repo/.edda/archive/blobs")
120 );
121 }
122
123 #[test]
124 fn ensure_layout_creates_dirs() {
125 let tmp = std::env::temp_dir().join(format!("edda_test_{}", std::process::id()));
126 let _ = std::fs::remove_dir_all(&tmp);
127 let p = EddaPaths::discover(&tmp);
128 p.ensure_layout().unwrap();
129 assert!(p.ledger_dir.is_dir());
130 assert!(p.blobs_dir.is_dir());
131 assert!(p.branches_dir.is_dir());
132 assert!(p.drafts_dir.is_dir());
133 assert!(p.patterns_dir.is_dir());
134 let _ = std::fs::remove_dir_all(&tmp);
135 }
136
137 use std::sync::atomic::{AtomicU64, Ordering};
138 static PATH_TEST_CTR: AtomicU64 = AtomicU64::new(0);
139
140 fn unique_tmp(label: &str) -> PathBuf {
141 let n = PATH_TEST_CTR.fetch_add(1, Ordering::SeqCst);
142 std::env::temp_dir().join(format!("edda_path_{label}_{}_{n}", std::process::id()))
143 }
144
145 #[test]
146 fn find_root_walks_up_to_edda_dir() {
147 let tmp = unique_tmp("walkup");
148 let _ = std::fs::remove_dir_all(&tmp);
149 std::fs::create_dir_all(tmp.join(".edda")).unwrap();
151 let deep = tmp.join("sub").join("deep");
152 std::fs::create_dir_all(&deep).unwrap();
153
154 let found = EddaPaths::find_root(&deep);
155 assert_eq!(found.unwrap(), tmp);
156 let _ = std::fs::remove_dir_all(&tmp);
157 }
158
159 #[test]
160 fn find_root_worktree_outside_repo() {
161 let tmp = unique_tmp("wt_outside");
164 let _ = std::fs::remove_dir_all(&tmp);
165 let repo = tmp.join("repo");
166 let wt = tmp.join("wt");
167
168 std::fs::create_dir_all(repo.join(".git").join("worktrees").join("feat-x")).unwrap();
170 std::fs::create_dir_all(repo.join(".edda")).unwrap();
171
172 std::fs::create_dir_all(&wt).unwrap();
174 let gitdir = repo.join(".git").join("worktrees").join("feat-x");
175 let gitdir_str = gitdir
176 .canonicalize()
177 .unwrap()
178 .to_string_lossy()
179 .replace('\\', "/");
180 std::fs::write(wt.join(".git"), format!("gitdir: {gitdir_str}")).unwrap();
181
182 let found = EddaPaths::find_root(&wt);
183 assert!(found.is_some(), "should resolve worktree to main repo");
184 assert!(found.unwrap().join(".edda").is_dir());
186 let _ = std::fs::remove_dir_all(&tmp);
187 }
188
189 #[test]
190 fn find_root_non_git_no_edda_returns_none() {
191 let tmp = unique_tmp("no_git");
192 let _ = std::fs::remove_dir_all(&tmp);
193 std::fs::create_dir_all(&tmp).unwrap();
194
195 assert!(EddaPaths::find_root(&tmp).is_none());
196 let _ = std::fs::remove_dir_all(&tmp);
197 }
198}