use std::io::Result;
use std::path::Path;
pub fn update_reference(ref_path: &str, commit_hash: &str) -> Result<()> {
if ref_path.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Reference path cannot be empty",
));
}
if commit_hash.len() != 40 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Invalid commit hash length: expected 40 characters, got {}",
commit_hash.len()
),
));
}
if !commit_hash.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Commit hash must contain only hexadecimal characters",
));
}
if !Path::new(".xit").exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
".xit directory not found. Are you in a Xit repository?",
));
}
let path = format!(".git/{}", ref_path);
if let Some(parent) = Path::new(&path).parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, format!("{}\n", commit_hash))?;
Ok(())
}
pub fn update_head(commit_hash: &str) -> Result<()> {
update_reference("HEAD", commit_hash)
}
pub fn update_branch(branch_name: &str, commit_hash: &str) -> Result<()> {
if branch_name.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Branch name cannot be empty",
));
}
let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', ' '];
if branch_name.chars().any(|c| invalid_chars.contains(&c)) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Branch name '{}' contains invalid characters", branch_name),
));
}
let reserved_names = ["HEAD", "ORIGIN_HEAD", "FETCH_HEAD", "MERGE_HEAD"];
if reserved_names.contains(&branch_name) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("'{}' is a reserved reference name", branch_name),
));
}
let ref_path = format!("refs/heads/{}", branch_name);
update_reference(&ref_path, commit_hash)
}
pub fn update_tag(tag_name: &str, commit_hash: &str) -> Result<()> {
if tag_name.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Tag name cannot be empty",
));
}
let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', ' '];
if tag_name.chars().any(|c| invalid_chars.contains(&c)) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Tag name '{}' contains invalid characters", tag_name),
));
}
let ref_path = format!("refs/tags/{}", tag_name);
update_reference(&ref_path, commit_hash)
}
pub fn create_branch(branch_name: &str, commit_hash: &str) -> Result<()> {
let ref_path = format!("refs/heads/{}", branch_name);
let full_path = format!(".xit/{}", ref_path);
if Path::new(&full_path).exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("Branch '{}' already exists", branch_name),
));
}
update_branch(branch_name, commit_hash)
}
pub fn create_tag(tag_name: &str, commit_hash: &str) -> Result<()> {
let ref_path = format!("refs/tags/{}", tag_name);
let full_path = format!(".xit/{}", ref_path);
if Path::new(&full_path).exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("Tag '{}' already exists", tag_name),
));
}
update_tag(tag_name, commit_hash)
}
pub fn delete_branch(branch_name: &str) -> Result<()> {
if branch_name.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Branch name cannot be empty",
));
}
let ref_path = format!("refs/heads/{}", branch_name);
let full_path = format!(".xit/{}", ref_path);
if !Path::new(&full_path).exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Branch '{}' does not exist", branch_name),
));
}
std::fs::remove_file(&full_path)?;
Ok(())
}
pub fn delete_tag(tag_name: &str) -> Result<()> {
if tag_name.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Tag name cannot be empty",
));
}
let ref_path = format!("refs/tags/{}", tag_name);
let full_path = format!(".xit/{}", ref_path);
if !Path::new(&full_path).exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Tag '{}' does not exist", tag_name),
));
}
std::fs::remove_file(&full_path)?;
Ok(())
}
pub fn read_reference(ref_path: &str) -> Result<String> {
if ref_path.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Reference path cannot be empty",
));
}
let path = format!(".git/{}", ref_path);
if !Path::new(&path).exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Reference '{}' does not exist", ref_path),
));
}
let content = std::fs::read_to_string(&path)?;
let commit_hash = content.trim_end_matches('\n');
if commit_hash.len() != 40 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Invalid commit hash in reference: expected 40 characters, got {}",
commit_hash.len()
),
));
}
if !commit_hash.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid commit hash format in reference",
));
}
Ok(commit_hash.to_string())
}
pub fn reference_exists(ref_path: &str) -> bool {
if ref_path.is_empty() {
return false;
}
let path = format!(".git/{}", ref_path);
Path::new(&path).exists()
}
pub fn list_branches() -> Result<Vec<String>> {
let heads_dir = ".git/refs/heads";
if !Path::new(heads_dir).exists() {
return Ok(Vec::new());
}
let mut branches = Vec::new();
for entry in std::fs::read_dir(heads_dir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
if let Some(name) = entry.file_name().to_str() {
branches.push(name.to_string());
}
}
}
branches.sort();
Ok(branches)
}
pub fn list_tags() -> Result<Vec<String>> {
let tags_dir = ".git/refs/tags";
if !Path::new(tags_dir).exists() {
return Ok(Vec::new());
}
let mut tags = Vec::new();
for entry in std::fs::read_dir(tags_dir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
if let Some(name) = entry.file_name().to_str() {
tags.push(name.to_string());
}
}
}
tags.sort();
Ok(tags)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_update_reference() {
let git_dir = ".git";
fs::create_dir_all(git_dir).unwrap();
let ref_path = "refs/heads/test-branch";
let commit_hash = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
update_reference(ref_path, commit_hash).unwrap();
let content = fs::read_to_string(format!("{}/{}", git_dir, ref_path)).unwrap();
assert_eq!(content, format!("{}\n", commit_hash));
fs::remove_dir_all(git_dir).unwrap();
}
}