claw-vcs-store 0.1.1

Content-addressed object storage, refs, and snapshot support for Claw VCS.
Documentation
//! Content-addressed repository storage for Claw VCS.
//!
//! `claw-store` owns `.claw/` layout operations: loose object storage, refs,
//! HEAD, reflogs, lockfiles, and object loading through COF decoding. It keeps
//! storage behavior separate from CLI and network concerns.
//!
//! # Example
//!
//! ```rust
//! use claw_core::object::Object;
//! use claw_core::types::Blob;
//! use claw_store::ClawStore;
//!
//! let temp = tempfile::tempdir()?;
//! let store = ClawStore::init(temp.path())?;
//!
//! let id = store.store_object(&Object::Blob(Blob {
//!     data: b"hello from claw".to_vec(),
//!     media_type: Some("text/plain".to_string()),
//! }))?;
//!
//! assert!(store.has_object(&id));
//! assert!(matches!(store.load_object(&id)?, Object::Blob(_)));
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
/// Store error types.
pub mod error;
/// HEAD file read/write helpers.
pub mod head;
/// Worktree index data structures.
pub mod index;
/// `.claw/` repository layout helpers.
pub mod layout;
/// Filesystem lockfile helper.
pub mod lockfile;
/// Loose object storage helpers.
pub mod loose;
/// Packfile storage helpers.
pub mod pack;
/// Reference log helpers.
pub mod reflog;
/// Reference validation and storage helpers.
pub mod refs;
/// Repository config read/write helpers.
pub mod repo;
/// Tree diff helpers used by snapshots.
pub mod tree_diff;

pub use error::StoreError;
pub use head::HeadState;

use std::path::Path;

use claw_core::cof::{cof_decode, cof_encode};
use claw_core::hash::content_hash;
use claw_core::id::ObjectId;
use claw_core::object::Object;

use crate::layout::RepoLayout;

pub struct ClawStore {
    layout: RepoLayout,
}

impl ClawStore {
    pub fn init(root: &Path) -> Result<Self, StoreError> {
        let layout = RepoLayout::new(root);
        layout.create_dirs()?;
        repo::write_default_config(&layout)?;
        head::write_head(
            &layout,
            &HeadState::Symbolic {
                ref_name: "heads/main".to_string(),
            },
        )?;
        Ok(Self { layout })
    }

    pub fn open(root: &Path) -> Result<Self, StoreError> {
        let layout = RepoLayout::new(root);
        if !layout.claw_dir().exists() {
            return Err(StoreError::NotARepository(root.to_path_buf()));
        }
        // Migrate: create HEAD and reflogs dir if missing
        if !layout.head_file().exists() {
            head::write_head(
                &layout,
                &HeadState::Symbolic {
                    ref_name: "heads/main".to_string(),
                },
            )?;
        }
        if !layout.reflogs_dir().exists() {
            std::fs::create_dir_all(layout.reflogs_dir())?;
        }
        Ok(Self { layout })
    }

    pub fn root(&self) -> &Path {
        self.layout.root()
    }

    pub fn layout(&self) -> &RepoLayout {
        &self.layout
    }

    pub fn store_object(&self, obj: &Object) -> Result<ObjectId, StoreError> {
        let payload = obj.serialize_payload()?;
        let type_tag = obj.type_tag();
        let id = content_hash(type_tag, &payload);
        let cof_data = cof_encode(type_tag, &payload)?;
        loose::write_loose_object(&self.layout, &id, &cof_data)?;
        Ok(id)
    }

    pub fn load_object(&self, id: &ObjectId) -> Result<Object, StoreError> {
        let cof_data = loose::read_loose_object(&self.layout, id)?;
        let (type_tag, payload) = cof_decode(&cof_data)?;
        let obj = Object::deserialize_payload(type_tag, &payload)?;
        Ok(obj)
    }

    /// Read the raw COF-encoded bytes for an object without decoding.
    ///
    /// This avoids the decode → re-encode cycle when the COF bytes will be
    /// sent over the wire unmodified (e.g., pack uploads, inline batch uploads).
    pub fn load_cof_bytes(&self, id: &ObjectId) -> Result<Vec<u8>, StoreError> {
        loose::read_loose_object(&self.layout, id)
    }

    pub fn has_object(&self, id: &ObjectId) -> bool {
        loose::loose_object_path(&self.layout, id).exists()
    }

    pub fn set_ref(&self, name: &str, target: &ObjectId) -> Result<(), StoreError> {
        refs::write_ref(&self.layout, name, target)
    }

    pub fn get_ref(&self, name: &str) -> Result<Option<ObjectId>, StoreError> {
        refs::read_ref(&self.layout, name)
    }

    pub fn list_refs(&self, prefix: &str) -> Result<Vec<(String, ObjectId)>, StoreError> {
        refs::list_refs(&self.layout, prefix)
    }

    pub fn delete_ref(&self, name: &str) -> Result<(), StoreError> {
        refs::delete_ref(&self.layout, name)
    }

    pub fn read_head(&self) -> Result<HeadState, StoreError> {
        head::read_head(&self.layout)
    }

    pub fn write_head(&self, state: &HeadState) -> Result<(), StoreError> {
        head::write_head(&self.layout, state)
    }

    pub fn resolve_head(&self) -> Result<Option<ObjectId>, StoreError> {
        head::resolve_head(&self.layout)
    }

    pub fn update_ref_cas(
        &self,
        name: &str,
        expected_old: Option<&ObjectId>,
        new_target: &ObjectId,
        author: &str,
        message: &str,
    ) -> Result<(), StoreError> {
        refs::update_ref_cas(
            &self.layout,
            name,
            expected_old,
            new_target,
            author,
            message,
        )
    }

    pub fn list_object_ids(&self) -> Result<Vec<ObjectId>, StoreError> {
        loose::list_loose_object_ids(&self.layout)
    }
}