claw-vcs-store 0.1.1

Content-addressed object storage, refs, and snapshot support for Claw VCS.
Documentation
use claw_core::id::ObjectId;

use crate::layout::RepoLayout;
use crate::refs;
use crate::StoreError;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeadState {
    Symbolic { ref_name: String },
    Detached { target: ObjectId },
}

pub fn read_head(layout: &RepoLayout) -> Result<HeadState, StoreError> {
    let path = layout.head_file();
    if !path.exists() {
        return Ok(HeadState::Symbolic {
            ref_name: "heads/main".to_string(),
        });
    }
    let content = std::fs::read_to_string(&path)?;
    let trimmed = content.trim();
    if let Some(ref_name) = trimmed.strip_prefix("ref: ") {
        Ok(HeadState::Symbolic {
            ref_name: ref_name.to_string(),
        })
    } else {
        let id = ObjectId::from_hex(trimmed)?;
        Ok(HeadState::Detached { target: id })
    }
}

pub fn write_head(layout: &RepoLayout, state: &HeadState) -> Result<(), StoreError> {
    let content = match state {
        HeadState::Symbolic { ref_name } => format!("ref: {}\n", ref_name),
        HeadState::Detached { target } => format!("{}\n", target.to_hex()),
    };
    std::fs::write(layout.head_file(), content)?;
    Ok(())
}

pub fn resolve_head(layout: &RepoLayout) -> Result<Option<ObjectId>, StoreError> {
    let state = read_head(layout)?;
    match state {
        HeadState::Symbolic { ref_name } => refs::read_ref(layout, &ref_name),
        HeadState::Detached { target } => Ok(Some(target)),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use claw_core::hash::content_hash;
    use claw_core::object::TypeTag;

    #[test]
    fn head_symbolic_roundtrip() {
        let tmp = tempfile::tempdir().unwrap();
        let layout = RepoLayout::new(tmp.path());
        layout.create_dirs().unwrap();

        let state = HeadState::Symbolic {
            ref_name: "heads/main".to_string(),
        };
        write_head(&layout, &state).unwrap();
        let read_back = read_head(&layout).unwrap();
        assert_eq!(read_back, state);
    }

    #[test]
    fn head_detached_roundtrip() {
        let tmp = tempfile::tempdir().unwrap();
        let layout = RepoLayout::new(tmp.path());
        layout.create_dirs().unwrap();

        let id = content_hash(TypeTag::Blob, b"test");
        let state = HeadState::Detached { target: id };
        write_head(&layout, &state).unwrap();
        let read_back = read_head(&layout).unwrap();
        assert_eq!(read_back, state);
    }

    #[test]
    fn resolve_head_symbolic() {
        let tmp = tempfile::tempdir().unwrap();
        let layout = RepoLayout::new(tmp.path());
        layout.create_dirs().unwrap();

        let id = content_hash(TypeTag::Blob, b"test");
        refs::write_ref(&layout, "heads/main", &id).unwrap();
        write_head(
            &layout,
            &HeadState::Symbolic {
                ref_name: "heads/main".to_string(),
            },
        )
        .unwrap();

        let resolved = resolve_head(&layout).unwrap();
        assert_eq!(resolved, Some(id));
    }
}