Skip to main content

claw_store/
lib.rs

1//! Content-addressed repository storage for Claw VCS.
2//!
3//! `claw-store` owns `.claw/` layout operations: loose object storage, refs,
4//! HEAD, reflogs, lockfiles, and object loading through COF decoding. It keeps
5//! storage behavior separate from CLI and network concerns.
6//!
7//! # Example
8//!
9//! ```rust
10//! use claw_core::object::Object;
11//! use claw_core::types::Blob;
12//! use claw_store::ClawStore;
13//!
14//! let temp = tempfile::tempdir()?;
15//! let store = ClawStore::init(temp.path())?;
16//!
17//! let id = store.store_object(&Object::Blob(Blob {
18//!     data: b"hello from claw".to_vec(),
19//!     media_type: Some("text/plain".to_string()),
20//! }))?;
21//!
22//! assert!(store.has_object(&id));
23//! assert!(matches!(store.load_object(&id)?, Object::Blob(_)));
24//! # Ok::<(), Box<dyn std::error::Error>>(())
25//! ```
26//!
27/// Store error types.
28pub mod error;
29/// HEAD file read/write helpers.
30pub mod head;
31/// Worktree index data structures.
32pub mod index;
33/// `.claw/` repository layout helpers.
34pub mod layout;
35/// Filesystem lockfile helper.
36pub mod lockfile;
37/// Loose object storage helpers.
38pub mod loose;
39/// Packfile storage helpers.
40pub mod pack;
41/// Reference log helpers.
42pub mod reflog;
43/// Reference validation and storage helpers.
44pub mod refs;
45/// Repository config read/write helpers.
46pub mod repo;
47/// Tree diff helpers used by snapshots.
48pub mod tree_diff;
49
50pub use error::StoreError;
51pub use head::HeadState;
52
53use std::path::Path;
54
55use claw_core::cof::{cof_decode, cof_encode};
56use claw_core::hash::content_hash;
57use claw_core::id::ObjectId;
58use claw_core::object::Object;
59
60use crate::layout::RepoLayout;
61
62pub struct ClawStore {
63    layout: RepoLayout,
64}
65
66impl ClawStore {
67    pub fn init(root: &Path) -> Result<Self, StoreError> {
68        let layout = RepoLayout::new(root);
69        layout.create_dirs()?;
70        repo::write_default_config(&layout)?;
71        head::write_head(
72            &layout,
73            &HeadState::Symbolic {
74                ref_name: "heads/main".to_string(),
75            },
76        )?;
77        Ok(Self { layout })
78    }
79
80    pub fn open(root: &Path) -> Result<Self, StoreError> {
81        let layout = RepoLayout::new(root);
82        if !layout.claw_dir().exists() {
83            return Err(StoreError::NotARepository(root.to_path_buf()));
84        }
85        // Migrate: create HEAD and reflogs dir if missing
86        if !layout.head_file().exists() {
87            head::write_head(
88                &layout,
89                &HeadState::Symbolic {
90                    ref_name: "heads/main".to_string(),
91                },
92            )?;
93        }
94        if !layout.reflogs_dir().exists() {
95            std::fs::create_dir_all(layout.reflogs_dir())?;
96        }
97        Ok(Self { layout })
98    }
99
100    pub fn root(&self) -> &Path {
101        self.layout.root()
102    }
103
104    pub fn layout(&self) -> &RepoLayout {
105        &self.layout
106    }
107
108    pub fn store_object(&self, obj: &Object) -> Result<ObjectId, StoreError> {
109        let payload = obj.serialize_payload()?;
110        let type_tag = obj.type_tag();
111        let id = content_hash(type_tag, &payload);
112        let cof_data = cof_encode(type_tag, &payload)?;
113        loose::write_loose_object(&self.layout, &id, &cof_data)?;
114        Ok(id)
115    }
116
117    pub fn load_object(&self, id: &ObjectId) -> Result<Object, StoreError> {
118        let cof_data = loose::read_loose_object(&self.layout, id)?;
119        let (type_tag, payload) = cof_decode(&cof_data)?;
120        let obj = Object::deserialize_payload(type_tag, &payload)?;
121        Ok(obj)
122    }
123
124    /// Read the raw COF-encoded bytes for an object without decoding.
125    ///
126    /// This avoids the decode → re-encode cycle when the COF bytes will be
127    /// sent over the wire unmodified (e.g., pack uploads, inline batch uploads).
128    pub fn load_cof_bytes(&self, id: &ObjectId) -> Result<Vec<u8>, StoreError> {
129        loose::read_loose_object(&self.layout, id)
130    }
131
132    pub fn has_object(&self, id: &ObjectId) -> bool {
133        loose::loose_object_path(&self.layout, id).exists()
134    }
135
136    pub fn set_ref(&self, name: &str, target: &ObjectId) -> Result<(), StoreError> {
137        refs::write_ref(&self.layout, name, target)
138    }
139
140    pub fn get_ref(&self, name: &str) -> Result<Option<ObjectId>, StoreError> {
141        refs::read_ref(&self.layout, name)
142    }
143
144    pub fn list_refs(&self, prefix: &str) -> Result<Vec<(String, ObjectId)>, StoreError> {
145        refs::list_refs(&self.layout, prefix)
146    }
147
148    pub fn delete_ref(&self, name: &str) -> Result<(), StoreError> {
149        refs::delete_ref(&self.layout, name)
150    }
151
152    pub fn read_head(&self) -> Result<HeadState, StoreError> {
153        head::read_head(&self.layout)
154    }
155
156    pub fn write_head(&self, state: &HeadState) -> Result<(), StoreError> {
157        head::write_head(&self.layout, state)
158    }
159
160    pub fn resolve_head(&self) -> Result<Option<ObjectId>, StoreError> {
161        head::resolve_head(&self.layout)
162    }
163
164    pub fn update_ref_cas(
165        &self,
166        name: &str,
167        expected_old: Option<&ObjectId>,
168        new_target: &ObjectId,
169        author: &str,
170        message: &str,
171    ) -> Result<(), StoreError> {
172        refs::update_ref_cas(
173            &self.layout,
174            name,
175            expected_old,
176            new_target,
177            author,
178            message,
179        )
180    }
181
182    pub fn list_object_ids(&self) -> Result<Vec<ObjectId>, StoreError> {
183        loose::list_loose_object_ids(&self.layout)
184    }
185}