1use anyhow::Result;
2use std::fs;
3use std::path::Path;
4
5pub type BranchEntry = (String, String);
7
8pub fn resolve_head(shard_dir: &Path) -> Result<(Option<String>, Option<String>)> {
14 let head_path = shard_dir.join("HEAD");
15 if !head_path.exists() {
16 return Ok((None, None));
17 }
18 let head = fs::read_to_string(&head_path)?;
19 let head = head.trim().to_string();
20
21 if let Some(branch_name) = head.strip_prefix("ref: refs/heads/") {
22 let branch_path = shard_dir.join("refs").join("heads").join(branch_name);
23 let commit_id = if branch_path.exists() {
24 Some(fs::read_to_string(&branch_path)?.trim().to_string())
25 } else {
26 None
27 };
28 Ok((Some(branch_name.to_string()), commit_id))
29 } else {
30 Ok((None, Some(head)))
32 }
33}
34
35pub fn set_head_branch(shard_dir: &Path, branch: &str) -> Result<()> {
37 fs::write(
38 shard_dir.join("HEAD"),
39 format!("ref: refs/heads/{}", branch),
40 )?;
41 Ok(())
42}
43
44pub fn set_head_commit(shard_dir: &Path, commit_id: &str) -> Result<()> {
46 fs::write(shard_dir.join("HEAD"), commit_id)?;
47 Ok(())
48}
49
50pub fn update_branch_ref(shard_dir: &Path, branch: &str, commit_id: &str) -> Result<()> {
52 let branch_path = shard_dir.join("refs").join("heads").join(branch);
53 fs::create_dir_all(branch_path.parent().unwrap())?;
54 fs::write(&branch_path, commit_id)?;
55 Ok(())
56}
57
58pub fn create_branch(shard_dir: &Path, name: &str, commit_id: &str) -> Result<()> {
60 let branch_path = shard_dir.join("refs").join("heads").join(name);
61 if branch_path.exists() {
62 anyhow::bail!("Branch '{}' already exists", name);
63 }
64 update_branch_ref(shard_dir, name, commit_id)?;
65 println!(
66 "Created branch '{}' at {}",
67 name,
68 &commit_id[..8.min(commit_id.len())]
69 );
70 Ok(())
71}
72
73pub fn delete_branch(shard_dir: &Path, name: &str) -> Result<()> {
75 let branch_path = shard_dir.join("refs").join("heads").join(name);
76 if !branch_path.exists() {
77 anyhow::bail!("Branch '{}' not found", name);
78 }
79 let (current, _) = resolve_head(shard_dir)?;
80 if current.as_deref() == Some(name) {
81 anyhow::bail!(
82 "Cannot delete branch '{}' — it is currently checked out",
83 name
84 );
85 }
86 fs::remove_file(&branch_path)?;
87 println!("Deleted branch '{}'", name);
88 Ok(())
89}
90
91pub fn list_branches(shard_dir: &Path) -> Result<(Option<String>, Vec<BranchEntry>)> {
93 let current = resolve_head(shard_dir)?.0;
94 let refs_dir = shard_dir.join("refs").join("heads");
95 if !refs_dir.exists() {
96 return Ok((current, Vec::new()));
97 }
98 let mut branches = Vec::new();
99 let mut entries: Vec<_> = fs::read_dir(&refs_dir)?
100 .filter_map(|e| e.ok())
101 .filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false))
102 .collect();
103 entries.sort_by_key(|e| e.file_name());
104 for entry in entries {
105 let name = entry.file_name().to_string_lossy().to_string();
106 let commit_id = fs::read_to_string(entry.path())?.trim().to_string();
107 branches.push((name, commit_id));
108 }
109 Ok((current, branches))
110}
111
112pub fn resolve_rev(shard_dir: &Path, name: &str) -> Result<String> {
115 let branch_path = shard_dir.join("refs").join("heads").join(name);
116 if branch_path.exists() {
117 return Ok(fs::read_to_string(&branch_path)?.trim().to_string());
118 }
119 Ok(name.to_string())
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use tempfile::tempdir;
127
128 fn init_shard(dir: &Path) {
129 fs::create_dir_all(dir.join("refs/heads")).unwrap();
130 set_head_branch(dir, "main").unwrap();
131 }
132
133 #[test]
134 fn test_resolve_head_empty() {
135 let dir = tempdir().unwrap();
136 let (branch, commit) = resolve_head(dir.path()).unwrap();
137
138 assert!(branch.is_none());
139 assert!(commit.is_none());
140 }
141
142 #[test]
143 fn test_set_head_branch_and_resolve() {
144 let dir = tempdir().unwrap();
145 fs::create_dir_all(dir.path().join("refs/heads")).unwrap();
146 set_head_branch(dir.path(), "main").unwrap();
147 update_branch_ref(dir.path(), "main", "abc123").unwrap();
148 let (branch, commit) = resolve_head(dir.path()).unwrap();
149 assert_eq!(branch.as_deref(), Some("main"));
150 assert_eq!(commit.as_deref(), Some("abc123"));
151 }
152
153 #[test]
154 fn test_resolve_head_detached() {
155 let dir = tempdir().unwrap();
156 set_head_commit(dir.path(), "detachedhash").unwrap();
157 let (branch, commit) = resolve_head(dir.path()).unwrap();
158 assert!(branch.is_none());
159 assert_eq!(commit.as_deref(), Some("detachedhash"));
160 }
161
162 #[test]
163 fn test_resolve_rev_branch() {
164 let dir = tempdir().unwrap();
165 fs::create_dir_all(dir.path().join("refs/heads")).unwrap();
166 update_branch_ref(dir.path(), "feature", "featurehash").unwrap();
167 let result = resolve_rev(dir.path(), "feature").unwrap();
168 assert_eq!(result, "featurehash");
169 }
170
171 #[test]
172 fn test_resolve_rev_commit_id() {
173 let dir = tempdir().unwrap();
174 let result = resolve_rev(dir.path(), "abc123").unwrap();
175 assert_eq!(result, "abc123");
176 }
177
178 #[test]
179 fn test_create_and_delete_branch() {
180 let dir = tempdir().unwrap();
181 let shard = dir.path();
182 init_shard(shard);
183 update_branch_ref(shard, "main", "somecommit").unwrap();
185
186 create_branch(shard, "test-branch", "testcommit").unwrap();
187 assert!(create_branch(shard, "test-branch", "other").is_err());
189
190 delete_branch(shard, "test-branch").unwrap();
191 assert!(delete_branch(shard, "nonexistent").is_err());
193 }
194
195 #[test]
196 fn test_delete_current_branch_fails() {
197 let dir = tempdir().unwrap();
198 let shard = dir.path();
199 init_shard(shard);
200 update_branch_ref(shard, "main", "commit1").unwrap();
201 let result = delete_branch(shard, "main");
202 assert!(result.is_err());
203 assert!(result
204 .unwrap_err()
205 .to_string()
206 .contains("currently checked out"));
207 }
208
209 #[test]
210 fn test_list_branches() {
211 let dir = tempdir().unwrap();
212 let shard = dir.path();
213 init_shard(shard);
214 update_branch_ref(shard, "main", "hash1").unwrap();
215 update_branch_ref(shard, "dev", "hash2").unwrap();
216
217 let (current, branches) = list_branches(shard).unwrap();
218 assert_eq!(current.as_deref(), Some("main"));
219 assert!(branches.iter().any(|(n, _)| n == "main"));
220 assert!(branches.iter().any(|(n, _)| n == "dev"));
221 }
222
223 #[test]
224 fn test_resolve_head_branch_no_commits() {
225 let dir = tempdir().unwrap();
226 let shard = dir.path();
227 set_head_branch(shard, "main").unwrap();
228 let (branch, commit) = resolve_head(shard).unwrap();
229 assert_eq!(branch.as_deref(), Some("main"));
230 assert!(commit.is_none());
231 }
232}