use anyhow::Result;
use std::fs;
use std::path::Path;
pub type BranchEntry = (String, String);
pub fn resolve_head(shard_dir: &Path) -> Result<(Option<String>, Option<String>)> {
let head_path = shard_dir.join("HEAD");
if !head_path.exists() {
return Ok((None, None));
}
let head = fs::read_to_string(&head_path)?;
let head = head.trim().to_string();
if let Some(branch_name) = head.strip_prefix("ref: refs/heads/") {
let branch_path = shard_dir.join("refs").join("heads").join(branch_name);
let commit_id = if branch_path.exists() {
Some(fs::read_to_string(&branch_path)?.trim().to_string())
} else {
None
};
Ok((Some(branch_name.to_string()), commit_id))
} else {
Ok((None, Some(head)))
}
}
pub fn set_head_branch(shard_dir: &Path, branch: &str) -> Result<()> {
fs::write(
shard_dir.join("HEAD"),
format!("ref: refs/heads/{}", branch),
)?;
Ok(())
}
pub fn set_head_commit(shard_dir: &Path, commit_id: &str) -> Result<()> {
fs::write(shard_dir.join("HEAD"), commit_id)?;
Ok(())
}
pub fn update_branch_ref(shard_dir: &Path, branch: &str, commit_id: &str) -> Result<()> {
let branch_path = shard_dir.join("refs").join("heads").join(branch);
fs::create_dir_all(branch_path.parent().unwrap())?;
fs::write(&branch_path, commit_id)?;
Ok(())
}
pub fn create_branch(shard_dir: &Path, name: &str, commit_id: &str) -> Result<()> {
let branch_path = shard_dir.join("refs").join("heads").join(name);
if branch_path.exists() {
anyhow::bail!("Branch '{}' already exists", name);
}
update_branch_ref(shard_dir, name, commit_id)?;
println!(
"Created branch '{}' at {}",
name,
&commit_id[..8.min(commit_id.len())]
);
Ok(())
}
pub fn delete_branch(shard_dir: &Path, name: &str) -> Result<()> {
let branch_path = shard_dir.join("refs").join("heads").join(name);
if !branch_path.exists() {
anyhow::bail!("Branch '{}' not found", name);
}
let (current, _) = resolve_head(shard_dir)?;
if current.as_deref() == Some(name) {
anyhow::bail!(
"Cannot delete branch '{}' — it is currently checked out",
name
);
}
fs::remove_file(&branch_path)?;
println!("Deleted branch '{}'", name);
Ok(())
}
pub fn list_branches(shard_dir: &Path) -> Result<(Option<String>, Vec<BranchEntry>)> {
let current = resolve_head(shard_dir)?.0;
let refs_dir = shard_dir.join("refs").join("heads");
if !refs_dir.exists() {
return Ok((current, Vec::new()));
}
let mut branches = Vec::new();
let mut entries: Vec<_> = fs::read_dir(&refs_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false))
.collect();
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let name = entry.file_name().to_string_lossy().to_string();
let commit_id = fs::read_to_string(entry.path())?.trim().to_string();
branches.push((name, commit_id));
}
Ok((current, branches))
}
pub fn resolve_rev(shard_dir: &Path, name: &str) -> Result<String> {
let branch_path = shard_dir.join("refs").join("heads").join(name);
if branch_path.exists() {
return Ok(fs::read_to_string(&branch_path)?.trim().to_string());
}
Ok(name.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn init_shard(dir: &Path) {
fs::create_dir_all(dir.join("refs/heads")).unwrap();
set_head_branch(dir, "main").unwrap();
}
#[test]
fn test_resolve_head_empty() {
let dir = tempdir().unwrap();
let (branch, commit) = resolve_head(dir.path()).unwrap();
assert!(branch.is_none());
assert!(commit.is_none());
}
#[test]
fn test_set_head_branch_and_resolve() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("refs/heads")).unwrap();
set_head_branch(dir.path(), "main").unwrap();
update_branch_ref(dir.path(), "main", "abc123").unwrap();
let (branch, commit) = resolve_head(dir.path()).unwrap();
assert_eq!(branch.as_deref(), Some("main"));
assert_eq!(commit.as_deref(), Some("abc123"));
}
#[test]
fn test_resolve_head_detached() {
let dir = tempdir().unwrap();
set_head_commit(dir.path(), "detachedhash").unwrap();
let (branch, commit) = resolve_head(dir.path()).unwrap();
assert!(branch.is_none());
assert_eq!(commit.as_deref(), Some("detachedhash"));
}
#[test]
fn test_resolve_rev_branch() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("refs/heads")).unwrap();
update_branch_ref(dir.path(), "feature", "featurehash").unwrap();
let result = resolve_rev(dir.path(), "feature").unwrap();
assert_eq!(result, "featurehash");
}
#[test]
fn test_resolve_rev_commit_id() {
let dir = tempdir().unwrap();
let result = resolve_rev(dir.path(), "abc123").unwrap();
assert_eq!(result, "abc123");
}
#[test]
fn test_create_and_delete_branch() {
let dir = tempdir().unwrap();
let shard = dir.path();
init_shard(shard);
update_branch_ref(shard, "main", "somecommit").unwrap();
create_branch(shard, "test-branch", "testcommit").unwrap();
assert!(create_branch(shard, "test-branch", "other").is_err());
delete_branch(shard, "test-branch").unwrap();
assert!(delete_branch(shard, "nonexistent").is_err());
}
#[test]
fn test_delete_current_branch_fails() {
let dir = tempdir().unwrap();
let shard = dir.path();
init_shard(shard);
update_branch_ref(shard, "main", "commit1").unwrap();
let result = delete_branch(shard, "main");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("currently checked out"));
}
#[test]
fn test_list_branches() {
let dir = tempdir().unwrap();
let shard = dir.path();
init_shard(shard);
update_branch_ref(shard, "main", "hash1").unwrap();
update_branch_ref(shard, "dev", "hash2").unwrap();
let (current, branches) = list_branches(shard).unwrap();
assert_eq!(current.as_deref(), Some("main"));
assert!(branches.iter().any(|(n, _)| n == "main"));
assert!(branches.iter().any(|(n, _)| n == "dev"));
}
#[test]
fn test_resolve_head_branch_no_commits() {
let dir = tempdir().unwrap();
let shard = dir.path();
set_head_branch(shard, "main").unwrap();
let (branch, commit) = resolve_head(shard).unwrap();
assert_eq!(branch.as_deref(), Some("main"));
assert!(commit.is_none());
}
}