use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::VcsError;
use crate::hash::ObjectId;
use crate::object::{Object, TagObject};
use crate::store::{HeadState, ReflogEntry, Store};
pub fn create_branch(
store: &mut dyn Store,
name: &str,
commit_id: ObjectId,
) -> Result<(), VcsError> {
let ref_name = format!("refs/heads/{name}");
if store.get_ref(&ref_name)?.is_some() {
return Err(VcsError::BranchExists {
name: name.to_owned(),
});
}
store.set_ref(&ref_name, commit_id)
}
pub fn delete_branch(store: &mut dyn Store, name: &str) -> Result<(), VcsError> {
let ref_name = format!("refs/heads/{name}");
let branch_id = store
.get_ref(&ref_name)?
.ok_or_else(|| VcsError::RefNotFound {
name: name.to_owned(),
})?;
if let Ok(Some(head_id)) = crate::store::resolve_head(store) {
if head_id != branch_id
&& !crate::dag::is_ancestor(store, branch_id, head_id).unwrap_or(false)
{
return Err(VcsError::BranchNotMerged {
name: name.to_owned(),
});
}
}
store.delete_ref(&ref_name)
}
pub fn force_delete_branch(store: &mut dyn Store, name: &str) -> Result<(), VcsError> {
let ref_name = format!("refs/heads/{name}");
store.delete_ref(&ref_name)
}
pub fn rename_branch(
store: &mut dyn Store,
old_name: &str,
new_name: &str,
) -> Result<(), VcsError> {
let old_ref = format!("refs/heads/{old_name}");
let new_ref = format!("refs/heads/{new_name}");
let id = store
.get_ref(&old_ref)?
.ok_or_else(|| VcsError::RefNotFound {
name: old_name.to_owned(),
})?;
if store.get_ref(&new_ref)?.is_some() {
return Err(VcsError::BranchExists {
name: new_name.to_owned(),
});
}
store.set_ref(&new_ref, id)?;
store.delete_ref(&old_ref)?;
if let Ok(entries) = store.read_reflog(&old_ref, None) {
for entry in &entries {
store.append_reflog(
&new_ref,
ReflogEntry {
old_id: entry.old_id,
new_id: entry.new_id,
author: entry.author.clone(),
timestamp: entry.timestamp,
message: format!("renamed from {old_name}: {}", entry.message),
},
)?;
}
}
if let Ok(HeadState::Branch(current)) = store.get_head() {
if current == old_name {
store.set_head(HeadState::Branch(new_name.to_owned()))?;
}
}
Ok(())
}
pub fn list_branches(store: &dyn Store) -> Result<Vec<(String, ObjectId)>, VcsError> {
let refs = store.list_refs("refs/heads/")?;
Ok(refs
.into_iter()
.map(|(full_name, id)| {
let name = full_name
.strip_prefix("refs/heads/")
.unwrap_or(&full_name)
.to_owned();
(name, id)
})
.collect())
}
pub fn create_tag(store: &mut dyn Store, name: &str, commit_id: ObjectId) -> Result<(), VcsError> {
let ref_name = format!("refs/tags/{name}");
if store.get_ref(&ref_name)?.is_some() {
return Err(VcsError::TagExists {
name: name.to_owned(),
});
}
store.set_ref(&ref_name, commit_id)
}
pub fn create_tag_force(
store: &mut dyn Store,
name: &str,
commit_id: ObjectId,
) -> Result<(), VcsError> {
let ref_name = format!("refs/tags/{name}");
store.set_ref(&ref_name, commit_id)
}
pub fn create_annotated_tag(
store: &mut dyn Store,
name: &str,
target: ObjectId,
tagger: &str,
message: &str,
) -> Result<ObjectId, VcsError> {
let ref_name = format!("refs/tags/{name}");
if store.get_ref(&ref_name)?.is_some() {
return Err(VcsError::TagExists {
name: name.to_owned(),
});
}
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let tag_obj = Object::Tag(TagObject {
target,
tagger: tagger.to_owned(),
timestamp,
message: message.to_owned(),
});
let tag_id = store.put(&tag_obj)?;
store.set_ref(&ref_name, tag_id)?;
Ok(tag_id)
}
pub fn delete_tag(store: &mut dyn Store, name: &str) -> Result<(), VcsError> {
let ref_name = format!("refs/tags/{name}");
store.delete_ref(&ref_name)
}
pub fn list_tags(store: &dyn Store) -> Result<Vec<(String, ObjectId)>, VcsError> {
let refs = store.list_refs("refs/tags/")?;
Ok(refs
.into_iter()
.map(|(full_name, id)| {
let name = full_name
.strip_prefix("refs/tags/")
.unwrap_or(&full_name)
.to_owned();
(name, id)
})
.collect())
}
pub fn checkout_branch(store: &mut dyn Store, name: &str) -> Result<(), VcsError> {
let ref_name = format!("refs/heads/{name}");
if store.get_ref(&ref_name)?.is_none() {
return Err(VcsError::RefNotFound {
name: name.to_owned(),
});
}
store.set_head(HeadState::Branch(name.to_owned()))
}
pub fn checkout_detached(store: &mut dyn Store, commit_id: ObjectId) -> Result<(), VcsError> {
store.set_head(HeadState::Detached(commit_id))
}
pub fn create_and_checkout_branch(
store: &mut dyn Store,
name: &str,
commit_id: ObjectId,
) -> Result<(), VcsError> {
create_branch(store, name, commit_id)?;
store.set_head(HeadState::Branch(name.to_owned()))
}
pub fn resolve_ref(store: &dyn Store, target: &str) -> Result<ObjectId, VcsError> {
if target.len() == 64 {
if let Ok(id) = target.parse::<ObjectId>() {
return Ok(id);
}
}
if target == "HEAD" {
return crate::store::resolve_head(store)?.ok_or_else(|| VcsError::RefNotFound {
name: "HEAD".to_owned(),
});
}
let branch_ref = format!("refs/heads/{target}");
if let Some(id) = store.get_ref(&branch_ref)? {
return Ok(id);
}
let tag_ref = format!("refs/tags/{target}");
if let Some(id) = store.get_ref(&tag_ref)? {
return Ok(peel_tag(store, id));
}
Err(VcsError::RefNotFound {
name: target.to_owned(),
})
}
fn peel_tag(store: &dyn Store, mut id: ObjectId) -> ObjectId {
for _ in 0..10 {
match store.get(&id) {
Ok(Object::Tag(tag)) => id = tag.target,
_ => return id,
}
}
id
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MemStore;
use crate::error::VcsError;
#[test]
fn branch_create_list_delete() -> Result<(), VcsError> {
let mut store = MemStore::new();
let id = ObjectId::from_bytes([1; 32]);
create_branch(&mut store, "feature", id)?;
let branches = list_branches(&store)?;
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].0, "feature");
assert_eq!(branches[0].1, id);
assert!(create_branch(&mut store, "feature", id).is_err());
delete_branch(&mut store, "feature")?;
assert!(list_branches(&store)?.is_empty());
Ok(())
}
#[test]
fn tag_create_list_delete() -> Result<(), VcsError> {
let mut store = MemStore::new();
let id = ObjectId::from_bytes([2; 32]);
create_tag(&mut store, "v1.0", id)?;
let tags = list_tags(&store)?;
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].0, "v1.0");
delete_tag(&mut store, "v1.0")?;
assert!(list_tags(&store)?.is_empty());
Ok(())
}
#[test]
fn checkout_branch_test() -> Result<(), VcsError> {
let mut store = MemStore::new();
let id = ObjectId::from_bytes([1; 32]);
store.set_ref("refs/heads/dev", id)?;
checkout_branch(&mut store, "dev")?;
assert_eq!(store.get_head()?, HeadState::Branch("dev".into()));
Ok(())
}
#[test]
fn checkout_nonexistent_branch_fails() {
let mut store = MemStore::new();
assert!(checkout_branch(&mut store, "nonexistent").is_err());
}
#[test]
fn resolve_ref_branch() -> Result<(), VcsError> {
let mut store = MemStore::new();
let id = ObjectId::from_bytes([3; 32]);
store.set_ref("refs/heads/main", id)?;
assert_eq!(resolve_ref(&store, "main")?, id);
Ok(())
}
#[test]
fn resolve_ref_tag() -> Result<(), VcsError> {
let mut store = MemStore::new();
let id = ObjectId::from_bytes([4; 32]);
store.set_ref("refs/tags/v1.0", id)?;
assert_eq!(resolve_ref(&store, "v1.0")?, id);
Ok(())
}
#[test]
fn resolve_ref_hex() -> Result<(), VcsError> {
let store = MemStore::new();
let id = ObjectId::from_bytes([5; 32]);
let hex = id.to_string();
assert_eq!(resolve_ref(&store, &hex)?, id);
Ok(())
}
#[test]
fn resolve_ref_head() -> Result<(), VcsError> {
let mut store = MemStore::new();
let id = ObjectId::from_bytes([6; 32]);
store.set_ref("refs/heads/main", id)?;
assert_eq!(resolve_ref(&store, "HEAD")?, id);
Ok(())
}
#[test]
fn resolve_ref_nonexistent() {
let store = MemStore::new();
assert!(resolve_ref(&store, "nonexistent").is_err());
}
}