1use std::path::{Path, PathBuf};
4
5use cadvm_store::{Category, ObjectId, Store};
6
7use crate::error::{CoreError, Result};
8use crate::model::{Commit, CommitBody, Manifest};
9
10pub const REPO_DIR: &str = ".cadvm";
12pub const DEFAULT_BRANCH: &str = "main";
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Head {
18 Branch(String),
20 Detached(ObjectId),
22}
23
24#[derive(Debug, Clone)]
26pub struct Repository {
27 workdir: PathBuf,
28 cadvm_dir: PathBuf,
29 store: Store,
30}
31
32impl Repository {
33 pub fn init(workdir: impl AsRef<Path>) -> Result<Repository> {
35 let workdir = workdir.as_ref().to_path_buf();
36 let cadvm_dir = workdir.join(REPO_DIR);
37 if cadvm_dir.exists() {
38 return Err(CoreError::AlreadyInitialized(cadvm_dir));
39 }
40
41 let store = Store::open(cadvm_dir.join("objects"))?;
43 for sub in ["refs/heads", "tmp"] {
44 let dir = cadvm_dir.join(sub);
45 std::fs::create_dir_all(&dir).map_err(|e| CoreError::io(&dir, e))?;
46 }
47
48 let repo = Repository {
49 workdir,
50 cadvm_dir,
51 store,
52 };
53
54 std::fs::write(repo.ref_path(DEFAULT_BRANCH), b"")
56 .map_err(|e| CoreError::io(repo.ref_path(DEFAULT_BRANCH), e))?;
57 repo.write_head(&Head::Branch(DEFAULT_BRANCH.to_string()))?;
58 repo.write_index_placeholder()?;
59
60 std::fs::write(repo.config_path(), b"{}\n")
62 .map_err(|e| CoreError::io(repo.config_path(), e))?;
63
64 Ok(repo)
65 }
66
67 pub fn discover(start: impl AsRef<Path>) -> Result<Repository> {
69 let start = start.as_ref();
70 let start_abs = if start.is_absolute() {
71 start.to_path_buf()
72 } else {
73 std::env::current_dir()
74 .map_err(|e| CoreError::io(start, e))?
75 .join(start)
76 };
77 let mut dir = start_abs.as_path();
78 loop {
79 if dir.join(REPO_DIR).is_dir() {
80 return Repository::open(dir);
81 }
82 match dir.parent() {
83 Some(parent) => dir = parent,
84 None => return Err(CoreError::NotARepository(start_abs.clone())),
85 }
86 }
87 }
88
89 pub fn open(workdir: impl AsRef<Path>) -> Result<Repository> {
91 let workdir = workdir.as_ref().to_path_buf();
92 let cadvm_dir = workdir.join(REPO_DIR);
93 if !cadvm_dir.is_dir() {
94 return Err(CoreError::NotARepository(workdir));
95 }
96 let store = Store::open(cadvm_dir.join("objects"))?;
97 Ok(Repository {
98 workdir,
99 cadvm_dir,
100 store,
101 })
102 }
103
104 pub fn workdir(&self) -> &Path {
108 &self.workdir
109 }
110
111 pub fn cadvm_dir(&self) -> &Path {
113 &self.cadvm_dir
114 }
115
116 pub fn store(&self) -> &Store {
118 &self.store
119 }
120
121 fn head_path(&self) -> PathBuf {
122 self.cadvm_dir.join("HEAD")
123 }
124
125 fn refs_heads_dir(&self) -> PathBuf {
126 self.cadvm_dir.join("refs/heads")
127 }
128
129 fn ref_path(&self, branch: &str) -> PathBuf {
130 self.refs_heads_dir().join(branch)
131 }
132
133 fn index_path(&self) -> PathBuf {
134 self.cadvm_dir.join("index.json")
135 }
136
137 pub fn config_path(&self) -> PathBuf {
139 self.cadvm_dir.join("config.json")
140 }
141
142 pub fn tmp_dir(&self) -> PathBuf {
144 self.cadvm_dir.join("tmp")
145 }
146
147 fn write_index_placeholder(&self) -> Result<()> {
148 let path = self.index_path();
151 std::fs::write(&path, b"{\n \"version\": 1,\n \"entries\": []\n}\n")
152 .map_err(|e| CoreError::io(&path, e))
153 }
154
155 pub fn read_head(&self) -> Result<Head> {
159 let path = self.head_path();
160 let raw = std::fs::read_to_string(&path).map_err(|e| CoreError::io(&path, e))?;
161 let raw = raw.trim();
162 if let Some(rest) = raw.strip_prefix("ref:") {
163 let refname = rest.trim();
164 let branch = refname
165 .strip_prefix("refs/heads/")
166 .unwrap_or(refname)
167 .to_string();
168 Ok(Head::Branch(branch))
169 } else {
170 let id = raw
171 .parse::<ObjectId>()
172 .map_err(|_| CoreError::UnknownRevision(raw.to_string()))?;
173 Ok(Head::Detached(id))
174 }
175 }
176
177 pub fn write_head(&self, head: &Head) -> Result<()> {
179 let path = self.head_path();
180 let contents = match head {
181 Head::Branch(name) => format!("ref: refs/heads/{name}\n"),
182 Head::Detached(id) => format!("{}\n", id.canonical()),
183 };
184 std::fs::write(&path, contents).map_err(|e| CoreError::io(&path, e))
185 }
186
187 pub fn current_branch(&self) -> Result<Option<String>> {
189 match self.read_head()? {
190 Head::Branch(name) => Ok(Some(name)),
191 Head::Detached(_) => Ok(None),
192 }
193 }
194
195 pub fn read_ref(&self, branch: &str) -> Result<Option<ObjectId>> {
197 let path = self.ref_path(branch);
198 match std::fs::read_to_string(&path) {
199 Ok(s) => {
200 let s = s.trim();
201 if s.is_empty() {
202 Ok(None)
203 } else {
204 Ok(Some(s.parse::<ObjectId>().map_err(|_| {
205 CoreError::UnknownRevision(format!("refs/heads/{branch}"))
206 })?))
207 }
208 }
209 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
210 Err(CoreError::NoSuchBranch(branch.to_string()))
211 }
212 Err(e) => Err(CoreError::io(&path, e)),
213 }
214 }
215
216 pub fn write_ref(&self, branch: &str, commit: &ObjectId) -> Result<()> {
218 let path = self.ref_path(branch);
219 std::fs::write(&path, format!("{}\n", commit.canonical()))
220 .map_err(|e| CoreError::io(&path, e))
221 }
222
223 pub fn branch_exists(&self, branch: &str) -> bool {
225 self.ref_path(branch).exists()
226 }
227
228 pub fn list_branches(&self) -> Result<Vec<String>> {
230 let dir = self.refs_heads_dir();
231 let mut names = Vec::new();
232 let entries = std::fs::read_dir(&dir).map_err(|e| CoreError::io(&dir, e))?;
233 for entry in entries {
234 let entry = entry.map_err(|e| CoreError::io(&dir, e))?;
235 if entry
236 .file_type()
237 .map_err(|e| CoreError::io(entry.path(), e))?
238 .is_file()
239 {
240 if let Some(name) = entry.file_name().to_str() {
241 names.push(name.to_string());
242 }
243 }
244 }
245 names.sort();
246 Ok(names)
247 }
248
249 pub fn create_branch(&self, name: &str, commit: &ObjectId) -> Result<()> {
252 validate_branch_name(name)?;
253 if self.branch_exists(name) {
254 return Err(CoreError::BranchExists(name.to_string()));
255 }
256 self.write_ref(name, commit)
257 }
258
259 pub fn delete_branch(&self, name: &str) -> Result<()> {
261 if !self.branch_exists(name) {
262 return Err(CoreError::NoSuchBranch(name.to_string()));
263 }
264 if self.current_branch()?.as_deref() == Some(name) {
265 return Err(CoreError::CannotDeleteCurrentBranch(name.to_string()));
266 }
267 let path = self.ref_path(name);
268 std::fs::remove_file(&path).map_err(|e| CoreError::io(&path, e))
269 }
270
271 pub fn head_commit_id(&self) -> Result<Option<ObjectId>> {
275 match self.read_head()? {
276 Head::Branch(name) => match self.read_ref(&name) {
277 Ok(opt) => Ok(opt),
278 Err(CoreError::NoSuchBranch(_)) => Ok(None),
281 Err(e) => Err(e),
282 },
283 Head::Detached(id) => Ok(Some(id)),
284 }
285 }
286
287 pub fn write_commit(&self, body: &CommitBody) -> Result<ObjectId> {
291 Ok(self.store.put_json(Category::Commit, body)?)
292 }
293
294 pub fn read_commit(&self, id: &ObjectId) -> Result<Commit> {
296 let body: CommitBody = self.store.get_json(Category::Commit, id)?;
297 Ok(Commit::from_body(id.clone(), body))
298 }
299
300 pub fn write_manifest(&self, manifest: &Manifest) -> Result<ObjectId> {
302 Ok(self.store.put_json(Category::Manifest, manifest)?)
303 }
304
305 pub fn read_manifest(&self, id: &ObjectId) -> Result<Manifest> {
307 Ok(self.store.get_json(Category::Manifest, id)?)
308 }
309
310 pub fn manifest_of_commit(&self, commit: &ObjectId) -> Result<Manifest> {
312 let commit = self.read_commit(commit)?;
313 self.read_manifest(&commit.manifest)
314 }
315
316 pub fn head_manifest(&self) -> Result<Manifest> {
318 match self.head_commit_id()? {
319 Some(id) => self.manifest_of_commit(&id),
320 None => Ok(Manifest::empty()),
321 }
322 }
323}
324
325pub fn validate_branch_name(name: &str) -> Result<()> {
328 let invalid = name.is_empty()
329 || name.starts_with('-')
330 || name == "."
331 || name == ".."
332 || name.contains("..")
333 || name.contains(|c: char| c.is_whitespace() || c == '/' || c == '\\' || c == ':');
334 if invalid {
335 Err(CoreError::InvalidBranchName(name.to_string()))
336 } else {
337 Ok(())
338 }
339}