1use flate2::read::ZlibDecoder;
15use flate2::write::ZlibEncoder;
16use flate2::Compression;
17use hashtree_config::{Config, StorageBackend};
18use hashtree_core::store::{Store, StoreError, StoreStats};
19use hashtree_core::types::Hash;
20use hashtree_core::{Cid, DirEntry, HashTree, HashTreeConfig, LinkType};
21use hashtree_fs::FsBlobStore;
22#[cfg(feature = "lmdb")]
23use hashtree_lmdb::LmdbBlobStore;
24use sha1::{Digest, Sha1};
25use std::collections::{BTreeMap, HashMap};
26use std::io::{Read, Write};
27use std::path::Path;
28use std::sync::Arc;
29use tokio::runtime::{Handle, Runtime};
30use tracing::{debug, info, warn};
31
32use super::object::{parse_tree, GitObject, ObjectId, ObjectType};
33use super::refs::{validate_ref_name, Ref};
34use super::{Error, Result};
35
36type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
38
39#[derive(Default)]
40struct RefDirectory {
41 files: BTreeMap<String, String>,
42 dirs: BTreeMap<String, RefDirectory>,
43}
44
45impl RefDirectory {
46 fn insert(&mut self, parts: &[&str], value: String) {
47 let Some((name, rest)) = parts.split_first() else {
48 return;
49 };
50
51 if rest.is_empty() {
52 self.files.insert((*name).to_string(), value);
53 } else {
54 self.dirs
55 .entry((*name).to_string())
56 .or_default()
57 .insert(rest, value);
58 }
59 }
60}
61
62enum RuntimeExecutor {
64 Owned(Runtime),
65 Handle(Handle),
66}
67
68impl RuntimeExecutor {
69 fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
70 match self {
71 RuntimeExecutor::Owned(rt) => rt.block_on(f),
72 RuntimeExecutor::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
73 }
74 }
75}
76
77pub enum LocalStore {
79 Fs(FsBlobStore),
80 #[cfg(feature = "lmdb")]
81 Lmdb(LmdbBlobStore),
82}
83
84impl LocalStore {
85 pub(crate) fn new_for_backend<P: AsRef<Path>>(
86 path: P,
87 backend: StorageBackend,
88 max_bytes: u64,
89 ) -> std::result::Result<Self, StoreError> {
90 let path = path.as_ref();
91 #[cfg(feature = "lmdb")]
92 {
93 return Self::new_for_backend_with_openers(
94 path,
95 backend,
96 max_bytes,
97 Self::open_fs_store,
98 Self::open_lmdb_store,
99 );
100 }
101
102 #[cfg(not(feature = "lmdb"))]
103 match backend {
104 StorageBackend::Fs => Self::open_fs_store(path, max_bytes),
105 #[cfg(not(feature = "lmdb"))]
106 StorageBackend::Lmdb => {
107 warn!(
108 "LMDB backend requested but lmdb feature not enabled, using filesystem storage"
109 );
110 Self::open_fs_store(path, max_bytes)
111 }
112 }
113 }
114
115 fn open_fs_store(path: &Path, max_bytes: u64) -> std::result::Result<Self, StoreError> {
116 if max_bytes > 0 {
117 Ok(LocalStore::Fs(FsBlobStore::with_max_bytes(
118 path, max_bytes,
119 )?))
120 } else {
121 Ok(LocalStore::Fs(FsBlobStore::new(path)?))
122 }
123 }
124
125 #[cfg(feature = "lmdb")]
126 fn open_lmdb_store(path: &Path, max_bytes: u64) -> std::result::Result<Self, StoreError> {
127 if max_bytes > 0 {
128 Ok(LocalStore::Lmdb(LmdbBlobStore::with_max_bytes(
129 path, max_bytes,
130 )?))
131 } else {
132 Ok(LocalStore::Lmdb(LmdbBlobStore::new(path)?))
133 }
134 }
135
136 #[cfg(feature = "lmdb")]
137 fn new_for_backend_with_openers<FS, LMDB>(
138 path: &Path,
139 backend: StorageBackend,
140 max_bytes: u64,
141 fs_open: FS,
142 lmdb_open: LMDB,
143 ) -> std::result::Result<Self, StoreError>
144 where
145 FS: Fn(&Path, u64) -> std::result::Result<Self, StoreError>,
146 LMDB: Fn(&Path, u64) -> std::result::Result<Self, StoreError>,
147 {
148 match backend {
149 StorageBackend::Fs => fs_open(path, max_bytes),
150 StorageBackend::Lmdb => match lmdb_open(path, max_bytes) {
151 Ok(store) => Ok(store),
152 Err(err) if should_fallback_from_lmdb_error(&err) => {
153 warn!(
154 path = %path.display(),
155 "LMDB backend is unsupported in this environment, falling back to filesystem storage"
156 );
157 fs_open(path, max_bytes)
158 }
159 Err(err) => Err(err),
160 },
161 }
162 }
163
164 pub fn new<P: AsRef<Path>>(path: P) -> std::result::Result<Self, StoreError> {
166 Self::new_with_max_bytes(path, 0)
167 }
168
169 pub fn new_with_max_bytes<P: AsRef<Path>>(
171 path: P,
172 max_bytes: u64,
173 ) -> std::result::Result<Self, StoreError> {
174 let config = Config::load_or_default();
175 Self::new_for_backend(path, config.storage.backend, max_bytes)
176 }
177
178 pub fn list(&self) -> std::result::Result<Vec<Hash>, StoreError> {
180 match self {
181 LocalStore::Fs(store) => store.list(),
182 #[cfg(feature = "lmdb")]
183 LocalStore::Lmdb(store) => store.list(),
184 }
185 }
186
187 pub fn get_sync(&self, hash: &Hash) -> std::result::Result<Option<Vec<u8>>, StoreError> {
189 match self {
190 LocalStore::Fs(store) => store.get_sync(hash),
191 #[cfg(feature = "lmdb")]
192 LocalStore::Lmdb(store) => store.get_sync(hash),
193 }
194 }
195}
196
197#[cfg(feature = "lmdb")]
198pub(crate) fn should_fallback_from_lmdb_error(error: &StoreError) -> bool {
199 matches!(
200 error,
201 StoreError::Io(io_error) if io_error.raw_os_error() == Some(libc::ENOSYS)
202 )
203}
204
205#[async_trait::async_trait]
206impl Store for LocalStore {
207 async fn put(&self, hash: Hash, data: Vec<u8>) -> std::result::Result<bool, StoreError> {
208 match self {
209 LocalStore::Fs(store) => store.put(hash, data).await,
210 #[cfg(feature = "lmdb")]
211 LocalStore::Lmdb(store) => store.put(hash, data).await,
212 }
213 }
214
215 async fn get(&self, hash: &Hash) -> std::result::Result<Option<Vec<u8>>, StoreError> {
216 match self {
217 LocalStore::Fs(store) => store.get(hash).await,
218 #[cfg(feature = "lmdb")]
219 LocalStore::Lmdb(store) => store.get(hash).await,
220 }
221 }
222
223 async fn has(&self, hash: &Hash) -> std::result::Result<bool, StoreError> {
224 match self {
225 LocalStore::Fs(store) => store.has(hash).await,
226 #[cfg(feature = "lmdb")]
227 LocalStore::Lmdb(store) => store.has(hash).await,
228 }
229 }
230
231 async fn delete(&self, hash: &Hash) -> std::result::Result<bool, StoreError> {
232 match self {
233 LocalStore::Fs(store) => store.delete(hash).await,
234 #[cfg(feature = "lmdb")]
235 LocalStore::Lmdb(store) => store.delete(hash).await,
236 }
237 }
238
239 fn set_max_bytes(&self, max: u64) {
240 match self {
241 LocalStore::Fs(store) => store.set_max_bytes(max),
242 #[cfg(feature = "lmdb")]
243 LocalStore::Lmdb(store) => store.set_max_bytes(max),
244 }
245 }
246
247 fn max_bytes(&self) -> Option<u64> {
248 match self {
249 LocalStore::Fs(store) => store.max_bytes(),
250 #[cfg(feature = "lmdb")]
251 LocalStore::Lmdb(store) => store.max_bytes(),
252 }
253 }
254
255 async fn stats(&self) -> StoreStats {
256 match self {
257 LocalStore::Fs(store) => match store.stats() {
258 Ok(stats) => StoreStats {
259 count: stats.count as u64,
260 bytes: stats.total_bytes,
261 pinned_count: stats.pinned_count as u64,
262 pinned_bytes: stats.pinned_bytes,
263 },
264 Err(_) => StoreStats::default(),
265 },
266 #[cfg(feature = "lmdb")]
267 LocalStore::Lmdb(store) => match store.stats() {
268 Ok(stats) => StoreStats {
269 count: stats.count as u64,
270 bytes: stats.total_bytes,
271 pinned_count: 0,
272 pinned_bytes: 0,
273 },
274 Err(_) => StoreStats::default(),
275 },
276 }
277 }
278
279 async fn evict_if_needed(&self) -> std::result::Result<u64, StoreError> {
280 match self {
281 LocalStore::Fs(store) => store.evict_if_needed().await,
282 #[cfg(feature = "lmdb")]
283 LocalStore::Lmdb(store) => store.evict_if_needed().await,
284 }
285 }
286}
287
288pub struct GitStorage {
290 store: Arc<LocalStore>,
291 tree: HashTree<LocalStore>,
292 runtime: RuntimeExecutor,
293 objects: std::sync::RwLock<HashMap<String, Vec<u8>>>,
295 refs: std::sync::RwLock<HashMap<String, String>>,
296 root_cid: std::sync::RwLock<Option<Cid>>,
298}
299
300impl GitStorage {
301 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
303 let config = Config::load_or_default();
304 let max_size_bytes = config
305 .storage
306 .max_size_gb
307 .saturating_mul(1024 * 1024 * 1024);
308 Self::open_with_max_bytes(path, max_size_bytes)
309 }
310
311 pub fn open_with_max_bytes(path: impl AsRef<Path>, max_size_bytes: u64) -> Result<Self> {
313 let config = Config::load_or_default();
314 Self::open_with_backend_and_max_bytes(path, config.storage.backend, max_size_bytes)
315 }
316
317 pub fn open_with_backend_and_max_bytes(
318 path: impl AsRef<Path>,
319 backend: StorageBackend,
320 max_size_bytes: u64,
321 ) -> Result<Self> {
322 let runtime = match Handle::try_current() {
323 Ok(handle) => RuntimeExecutor::Handle(handle),
324 Err(_) => {
325 let rt = Runtime::new()
326 .map_err(|e| Error::StorageError(format!("tokio runtime: {}", e)))?;
327 RuntimeExecutor::Owned(rt)
328 }
329 };
330
331 let store_path = path.as_ref().join("blobs");
332 let store = Arc::new(
333 LocalStore::new_for_backend(&store_path, backend, max_size_bytes)
334 .map_err(|e| Error::StorageError(format!("local store: {}", e)))?,
335 );
336
337 let tree = HashTree::new(HashTreeConfig::new(store.clone()));
339
340 Ok(Self {
341 store,
342 tree,
343 runtime,
344 objects: std::sync::RwLock::new(HashMap::new()),
345 refs: std::sync::RwLock::new(HashMap::new()),
346 root_cid: std::sync::RwLock::new(None),
347 })
348 }
349
350 pub fn evict_if_needed(&self) -> Result<u64> {
352 self.runtime
353 .block_on(self.store.evict_if_needed())
354 .map_err(|e| Error::StorageError(format!("evict: {}", e)))
355 }
356
357 fn write_object(&self, obj: &GitObject) -> Result<ObjectId> {
359 let oid = obj.id();
360 let key = oid.to_hex();
361
362 let loose = obj.to_loose_format();
363 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
364 encoder.write_all(&loose)?;
365 let compressed = encoder.finish()?;
366
367 let mut objects = self
368 .objects
369 .write()
370 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
371 objects.insert(key, compressed);
372
373 if let Ok(mut root) = self.root_cid.write() {
375 *root = None;
376 }
377
378 Ok(oid)
379 }
380
381 pub fn write_raw_object(&self, obj_type: ObjectType, content: &[u8]) -> Result<ObjectId> {
383 let obj = GitObject::new(obj_type, content.to_vec());
384 self.write_object(&obj)
385 }
386
387 #[allow(dead_code)]
389 fn read_object(&self, oid: &ObjectId) -> Result<GitObject> {
390 let key = oid.to_hex();
391 let objects = self
392 .objects
393 .read()
394 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
395 let compressed = objects
396 .get(&key)
397 .ok_or_else(|| Error::ObjectNotFound(key.clone()))?;
398
399 let mut decoder = ZlibDecoder::new(compressed.as_slice());
400 let mut data = Vec::new();
401 decoder.read_to_end(&mut data)?;
402
403 GitObject::from_loose_format(&data)
404 }
405
406 pub fn write_ref(&self, name: &str, target: &Ref) -> Result<()> {
408 validate_ref_name(name)?;
409
410 let value = match target {
411 Ref::Direct(oid) => oid.to_hex(),
412 Ref::Symbolic(target) => format!("ref: {}", target),
413 };
414
415 let mut refs = self
416 .refs
417 .write()
418 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
419 refs.insert(name.to_string(), value);
420
421 if let Ok(mut root) = self.root_cid.write() {
423 *root = None;
424 }
425
426 Ok(())
427 }
428
429 #[allow(dead_code)]
431 pub fn read_ref(&self, name: &str) -> Result<Option<Ref>> {
432 let refs = self
433 .refs
434 .read()
435 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
436
437 match refs.get(name) {
438 Some(value) => {
439 if let Some(target) = value.strip_prefix("ref: ") {
440 Ok(Some(Ref::Symbolic(target.to_string())))
441 } else {
442 let oid = ObjectId::from_hex(value)
443 .ok_or_else(|| Error::StorageError(format!("invalid ref: {}", value)))?;
444 Ok(Some(Ref::Direct(oid)))
445 }
446 }
447 None => Ok(None),
448 }
449 }
450
451 #[allow(dead_code)]
453 pub fn list_refs(&self) -> Result<HashMap<String, String>> {
454 let refs = self
455 .refs
456 .read()
457 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
458 Ok(refs.clone())
459 }
460
461 pub fn delete_ref(&self, name: &str) -> Result<bool> {
463 let mut refs = self
464 .refs
465 .write()
466 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
467 let existed = refs.remove(name).is_some();
468
469 if let Ok(mut root) = self.root_cid.write() {
471 *root = None;
472 }
473
474 Ok(existed)
475 }
476
477 pub fn import_compressed_object(&self, oid: &str, compressed_data: Vec<u8>) -> Result<()> {
480 let mut objects = self
481 .objects
482 .write()
483 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
484 objects.insert(oid.to_string(), compressed_data);
485
486 if let Ok(mut root) = self.root_cid.write() {
488 *root = None;
489 }
490
491 Ok(())
492 }
493
494 pub fn import_ref(&self, name: &str, value: &str) -> Result<()> {
496 let mut refs = self
497 .refs
498 .write()
499 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
500 refs.insert(name.to_string(), value.to_string());
501
502 if let Ok(mut root) = self.root_cid.write() {
504 *root = None;
505 }
506
507 Ok(())
508 }
509
510 #[cfg(test)]
512 pub fn has_ref(&self, name: &str) -> Result<bool> {
513 let refs = self
514 .refs
515 .read()
516 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
517 Ok(refs.contains_key(name))
518 }
519
520 #[cfg(test)]
522 pub fn object_count(&self) -> Result<usize> {
523 let objects = self
524 .objects
525 .read()
526 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
527 Ok(objects.len())
528 }
529
530 #[allow(dead_code)]
532 pub fn get_root_cid(&self) -> Result<Option<Cid>> {
533 let root = self
534 .root_cid
535 .read()
536 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
537 Ok(root.clone())
538 }
539
540 #[allow(dead_code)]
542 pub fn default_branch(&self) -> Result<Option<String>> {
543 let refs = self
544 .refs
545 .read()
546 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
547
548 if let Some(head) = refs.get("HEAD") {
549 if let Some(target) = head.strip_prefix("ref: ") {
550 return Ok(Some(target.to_string()));
551 }
552 }
553 Ok(None)
554 }
555
556 fn get_commit_tree(
558 &self,
559 commit_oid: &str,
560 objects: &HashMap<String, Vec<u8>>,
561 ) -> Option<String> {
562 let compressed = objects.get(commit_oid)?;
563
564 let mut decoder = ZlibDecoder::new(&compressed[..]);
566 let mut decompressed = Vec::new();
567 decoder.read_to_end(&mut decompressed).ok()?;
568
569 let null_pos = decompressed.iter().position(|&b| b == 0)?;
571 let content = &decompressed[null_pos + 1..];
572
573 let content_str = std::str::from_utf8(content).ok()?;
575 let first_line = content_str.lines().next()?;
576 first_line
577 .strip_prefix("tree ")
578 .map(|tree_hash| tree_hash.to_string())
579 }
580
581 fn get_object_content(
583 &self,
584 oid: &str,
585 objects: &HashMap<String, Vec<u8>>,
586 ) -> Option<(ObjectType, Vec<u8>)> {
587 let compressed = objects.get(oid)?;
588 Self::parse_compressed_object(compressed)
589 }
590
591 fn parse_compressed_object(compressed: &[u8]) -> Option<(ObjectType, Vec<u8>)> {
592 let mut decoder = ZlibDecoder::new(compressed);
593 let mut decompressed = Vec::new();
594 decoder.read_to_end(&mut decompressed).ok()?;
595
596 let null_pos = decompressed.iter().position(|&b| b == 0)?;
598 let header = std::str::from_utf8(&decompressed[..null_pos]).ok()?;
599 let obj_type = if header.starts_with("blob") {
600 ObjectType::Blob
601 } else if header.starts_with("tree") {
602 ObjectType::Tree
603 } else if header.starts_with("commit") {
604 ObjectType::Commit
605 } else if header.starts_with("tag") {
606 ObjectType::Tag
607 } else {
608 return None;
609 };
610 let content = decompressed[null_pos + 1..].to_vec();
611 Some((obj_type, content))
612 }
613
614 async fn load_base_compressed_object<S: Store>(
615 &self,
616 oid: &str,
617 base_tree: &HashTree<S>,
618 base_objects_cid: &Cid,
619 ) -> Result<Option<Vec<u8>>> {
620 if oid.len() != 40 {
621 return Err(Error::ObjectNotFound(oid.to_string()));
622 }
623
624 let path = format!("{}/{}", &oid[..2], &oid[2..]);
625 let Some(object_cid) = base_tree
626 .resolve_path(base_objects_cid, &path)
627 .await
628 .map_err(|e| Error::StorageError(format!("resolve {} in base objects: {}", oid, e)))?
629 else {
630 return Ok(None);
631 };
632
633 base_tree
634 .get(&object_cid, None)
635 .await
636 .map_err(|e| Error::StorageError(format!("read {} from base objects: {}", oid, e)))
637 }
638
639 async fn get_object_content_from_base<S: Store>(
640 &self,
641 oid: &str,
642 base_tree: &HashTree<S>,
643 base_objects_cid: &Cid,
644 ) -> Result<Option<(ObjectType, Vec<u8>)>> {
645 let Some(compressed) = self
646 .load_base_compressed_object(oid, base_tree, base_objects_cid)
647 .await?
648 else {
649 return Ok(None);
650 };
651
652 Ok(Self::parse_compressed_object(&compressed))
653 }
654
655 async fn get_tree_entries_from_sources<S: Store>(
656 &self,
657 tree_oid: &str,
658 objects: &HashMap<String, Vec<u8>>,
659 base_tree: Option<&HashTree<S>>,
660 base_objects_cid: Option<&Cid>,
661 ) -> Result<Vec<super::object::TreeEntry>> {
662 let object = match self.get_object_content(tree_oid, objects) {
663 Some(object) => Some(object),
664 None => {
665 if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
666 self.get_object_content_from_base(tree_oid, base_tree, base_objects_cid)
667 .await?
668 } else {
669 None
670 }
671 }
672 }
673 .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
674
675 if object.0 != ObjectType::Tree {
676 return Err(Error::InvalidObjectType(format!(
677 "expected tree, got {:?}",
678 object.0
679 )));
680 }
681
682 parse_tree(&object.1)
683 }
684
685 fn tree_entry_to_dir_entry(entry: &hashtree_core::TreeEntry) -> DirEntry {
686 DirEntry::from_cid(
687 &entry.name,
688 &Cid {
689 hash: entry.hash,
690 key: entry.key,
691 },
692 )
693 .with_size(entry.size)
694 .with_link_type(entry.link_type)
695 }
696
697 async fn import_object_from_base<S: Store>(
698 &self,
699 oid: &str,
700 objects: &mut HashMap<String, Vec<u8>>,
701 base_tree: &HashTree<S>,
702 base_objects_cid: &Cid,
703 ) -> Result<bool> {
704 if objects.contains_key(oid) {
705 return Ok(true);
706 }
707 let Some(compressed) = self
708 .load_base_compressed_object(oid, base_tree, base_objects_cid)
709 .await?
710 else {
711 return Ok(false);
712 };
713
714 objects.insert(oid.to_string(), compressed);
715 Ok(true)
716 }
717
718 fn seed_missing_object_from_base_boxed<'a, S: Store + 'a>(
719 &'a self,
720 oid: &'a str,
721 objects: &'a mut HashMap<String, Vec<u8>>,
722 base_tree: &'a HashTree<S>,
723 base_objects_cid: &'a Cid,
724 ) -> BoxFuture<'a, Result<bool>> {
725 Box::pin(self.import_object_from_base(oid, objects, base_tree, base_objects_cid))
726 }
727
728 fn peel_tag_target(&self, oid: &str, objects: &HashMap<String, Vec<u8>>) -> Option<String> {
729 let (obj_type, content) = self.get_object_content(oid, objects)?;
730 if obj_type != ObjectType::Tag {
731 return Some(oid.to_string());
732 }
733
734 let target = std::str::from_utf8(&content)
735 .ok()?
736 .lines()
737 .find_map(|line| line.strip_prefix("object "))
738 .map(str::trim)?
739 .to_string();
740
741 match self.get_object_content(&target, objects)?.0 {
742 ObjectType::Tag => self.peel_tag_target(&target, objects),
743 _ => Some(target),
744 }
745 }
746
747 fn build_info_refs_content(
748 &self,
749 refs: &HashMap<String, String>,
750 objects: &HashMap<String, Vec<u8>>,
751 ) -> String {
752 let mut lines = Vec::new();
753
754 for (name, value) in refs {
755 if name == "HEAD" {
756 continue;
757 }
758
759 let oid = value.trim().to_string();
760 lines.push((name.clone(), oid.clone()));
761
762 if name.starts_with("refs/tags/") {
763 if let Some(peeled) = self.peel_tag_target(&oid, objects) {
764 if peeled != oid {
765 lines.push((format!("{}^{{}}", name), peeled));
766 }
767 }
768 }
769 }
770
771 lines.sort_by(|a, b| a.0.cmp(&b.0));
772
773 let mut content = String::new();
774 for (name, oid) in lines {
775 content.push_str(&oid);
776 content.push('\t');
777 content.push_str(&name);
778 content.push('\n');
779 }
780 content
781 }
782
783 async fn build_info_dir(
784 &self,
785 refs: &HashMap<String, String>,
786 objects: &HashMap<String, Vec<u8>>,
787 ) -> Result<Cid> {
788 let info_refs = self.build_info_refs_content(refs, objects);
789 let (info_refs_cid, info_refs_size) = self
790 .tree
791 .put(info_refs.as_bytes())
792 .await
793 .map_err(|e| Error::StorageError(format!("put info/refs: {}", e)))?;
794
795 self.tree
796 .put_directory(vec![
797 DirEntry::from_cid("refs", &info_refs_cid).with_size(info_refs_size)
798 ])
799 .await
800 .map_err(|e| Error::StorageError(format!("put info dir: {}", e)))
801 }
802
803 pub fn build_tree(&self) -> Result<Cid> {
805 self.build_tree_with_base_objects::<LocalStore>(None, None, None)
806 }
807
808 pub fn validate_root_contains_direct_refs(&self, root_cid: &Cid) -> Result<()> {
809 let direct_refs: Vec<String> = {
810 let refs = self
811 .refs
812 .read()
813 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
814 refs.values()
815 .filter(|value| {
816 !value.starts_with("ref: ")
817 && value.len() == 40
818 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
819 })
820 .cloned()
821 .collect()
822 };
823
824 if direct_refs.is_empty() {
825 return Ok(());
826 }
827
828 let objects_dir = self
829 .runtime
830 .block_on(self.tree.resolve_path(root_cid, ".git/objects"))
831 .map_err(|e| Error::StorageError(format!("resolve .git/objects: {}", e)))?;
832 if objects_dir.is_none() {
833 return Err(Error::StorageError(
834 "built root is missing .git/objects".to_string(),
835 ));
836 }
837
838 for oid in direct_refs {
839 let object_path = format!(".git/objects/{}/{}", &oid[..2], &oid[2..]);
840 let object_cid = self
841 .runtime
842 .block_on(self.tree.resolve_path(root_cid, &object_path))
843 .map_err(|e| Error::StorageError(format!("resolve {}: {}", object_path, e)))?;
844 if object_cid.is_none() {
845 return Err(Error::ObjectNotFound(oid));
846 }
847 }
848
849 Ok(())
850 }
851
852 pub fn build_tree_with_base_objects<S: Store>(
853 &self,
854 base_tree: Option<&HashTree<S>>,
855 base_root: Option<&Cid>,
856 base_commit_sha: Option<&str>,
857 ) -> Result<Cid> {
858 if let Ok(root) = self.root_cid.read() {
860 if let Some(ref cid) = *root {
861 return Ok(cid.clone());
862 }
863 }
864
865 if let Err(err) = self.evict_if_needed() {
866 debug!("pre-build eviction skipped: {}", err);
867 }
868
869 let objects = self
870 .objects
871 .read()
872 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
873 let refs = self
874 .refs
875 .read()
876 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
877
878 let (default_branch, commit_sha) = if let Some(head) = refs.get("HEAD") {
880 let branch = head.strip_prefix("ref: ").map(String::from);
881 let sha = branch.as_ref().and_then(|b| refs.get(b)).cloned();
882 (branch, sha)
883 } else {
884 let mut branch_info: Option<(String, String)> = None;
886 for (ref_name, sha) in refs.iter() {
887 if ref_name.starts_with("refs/heads/") {
888 branch_info = Some((ref_name.clone(), sha.clone()));
889 break;
890 }
891 }
892 match branch_info {
893 Some((branch, sha)) => (Some(branch), Some(sha)),
894 None => (None, None),
895 }
896 };
897
898 let tree_sha = commit_sha
900 .as_ref()
901 .and_then(|sha| self.get_commit_tree(sha, &objects));
902
903 let mut objects_clone = objects.clone();
905
906 let base_objects_cid = if let (Some(base_tree), Some(base_root)) = (base_tree, base_root) {
907 self.runtime
908 .block_on(base_tree.resolve_path(base_root, ".git/objects"))
909 .map_err(|e| Error::StorageError(format!("resolve base .git/objects: {}", e)))?
910 } else {
911 None
912 };
913
914 let base_tree_sha =
915 if let (Some(base_tree), Some(base_objects_cid), Some(base_commit_sha)) =
916 (base_tree, base_objects_cid.as_ref(), base_commit_sha)
917 {
918 self.runtime.block_on(async {
919 let Some((obj_type, content)) = self
920 .get_object_content_from_base(base_commit_sha, base_tree, base_objects_cid)
921 .await?
922 else {
923 return Ok(None);
924 };
925
926 if obj_type != ObjectType::Commit {
927 return Err(Error::InvalidObjectType(format!(
928 "expected commit, got {:?}",
929 obj_type
930 )));
931 }
932
933 let content = std::str::from_utf8(&content).map_err(|_| {
934 Error::InvalidObjectFormat("commit: invalid utf-8".to_string())
935 })?;
936 Ok::<Option<String>, Error>(
937 content
938 .lines()
939 .find_map(|line| line.strip_prefix("tree "))
940 .map(str::trim)
941 .map(str::to_string),
942 )
943 })?
944 } else {
945 None
946 };
947
948 let base_root_entries = if let (Some(base_tree), Some(base_root)) = (base_tree, base_root) {
949 Some(
950 self.runtime
951 .block_on(base_tree.list_directory(base_root))
952 .map_err(|e| Error::StorageError(format!("list base root: {}", e)))?,
953 )
954 } else {
955 None
956 };
957
958 let root_cid = loop {
959 let build_result = self.runtime.block_on(async {
960 let objects_cid = self
962 .build_objects_dir_with_base(&objects_clone, base_tree, base_objects_cid.as_ref())
963 .await?;
964
965 let refs_cid = self.build_refs_dir(&refs).await?;
967
968 let info_cid = self.build_info_dir(&refs, &objects_clone).await?;
970
971 let head_content = refs.get("HEAD")
974 .map(|h| if h.ends_with('\n') { h.clone() } else { format!("{}\n", h) })
975 .or_else(|| default_branch.as_ref().map(|b| format!("ref: {}\n", b)))
976 .unwrap_or_else(|| "ref: refs/heads/main\n".to_string());
977 debug!("HEAD content: {:?}", head_content);
978 let (head_cid, head_size) = self.tree.put(head_content.as_bytes()).await
979 .map_err(|e| Error::StorageError(format!("put HEAD: {}", e)))?;
980 debug!("HEAD hash: {}", hex::encode(head_cid.hash));
981
982 let mut git_entries = vec![
984 DirEntry::from_cid("HEAD", &head_cid).with_size(head_size),
985 DirEntry::from_cid("info", &info_cid).with_link_type(LinkType::Dir),
986 DirEntry::from_cid("objects", &objects_cid).with_link_type(LinkType::Dir),
987 DirEntry::from_cid("refs", &refs_cid).with_link_type(LinkType::Dir),
988 ];
989
990 if let Some(ref branch) = default_branch {
992 let config = format!(
993 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n[init]\n\tdefaultBranch = {}\n",
994 branch.trim_start_matches("refs/heads/")
995 );
996 let (config_cid, config_size) = self.tree.put(config.as_bytes()).await
997 .map_err(|e| Error::StorageError(format!("put config: {}", e)))?;
998 git_entries.push(DirEntry::from_cid("config", &config_cid).with_size(config_size));
999 }
1000
1001 if let Some(ref tree_oid) = tree_sha {
1003 let index_result = if let (Some(base_tree), Some(base_objects_cid), Some(base_tree_sha)) =
1004 (base_tree, base_objects_cid.as_ref(), base_tree_sha.as_deref())
1005 {
1006 self.build_index_file_with_base(
1007 tree_oid,
1008 &objects_clone,
1009 base_tree,
1010 base_objects_cid,
1011 base_tree_sha,
1012 base_root_entries.as_ref(),
1013 )
1014 .await
1015 } else {
1016 self.build_index_file(tree_oid, &objects_clone)
1017 };
1018 match index_result {
1019 Ok(index_data) => {
1020 let (index_cid, index_size) = self.tree.put(&index_data).await
1021 .map_err(|e| Error::StorageError(format!("put index: {}", e)))?;
1022 git_entries.push(DirEntry::from_cid("index", &index_cid).with_size(index_size));
1023 info!("Added git index file ({} bytes)", index_data.len());
1024 }
1025 Err(e) => {
1026 debug!("Failed to build git index file: {} - continuing without index", e);
1027 }
1028 }
1029 }
1030
1031 let git_cid = self.tree.put_directory(git_entries).await
1032 .map_err(|e| Error::StorageError(format!("put .git: {}", e)))?;
1033
1034 let mut root_entries = vec![DirEntry::from_cid(".git", &git_cid).with_link_type(LinkType::Dir)];
1037
1038 if let Some(ref tree_oid) = tree_sha {
1040 let working_tree_entries = if let (Some(base_tree), Some(base_objects_cid), Some(base_tree_sha)) =
1041 (base_tree, base_objects_cid.as_ref(), base_tree_sha.as_deref())
1042 {
1043 self.build_working_tree_entries_with_base(
1044 tree_oid,
1045 &objects_clone,
1046 base_tree,
1047 base_objects_cid,
1048 base_tree_sha,
1049 base_root_entries.as_ref(),
1050 )
1051 .await?
1052 } else {
1053 self.build_working_tree_entries(tree_oid, &objects_clone).await?
1054 };
1055 root_entries.extend(working_tree_entries);
1056 info!("Added {} working tree entries to root", root_entries.len() - 1);
1057 }
1058
1059 root_entries.sort_by(|a, b| a.name.cmp(&b.name));
1061
1062 let root_cid = self.tree.put_directory(root_entries).await
1063 .map_err(|e| Error::StorageError(format!("put root: {}", e)))?;
1064
1065 info!("Built hashtree root: {} (encrypted: {}) (.git dir: {})",
1066 hex::encode(root_cid.hash),
1067 root_cid.key.is_some(),
1068 hex::encode(git_cid.hash));
1069
1070 Ok::<Cid, Error>(root_cid)
1071 });
1072
1073 match build_result {
1074 Ok(root_cid) => break root_cid,
1075 Err(Error::ObjectNotFound(oid))
1076 if base_tree.is_some() && base_objects_cid.is_some() =>
1077 {
1078 let imported =
1079 self.runtime
1080 .block_on(self.seed_missing_object_from_base_boxed(
1081 &oid,
1082 &mut objects_clone,
1083 base_tree.expect("checked is_some"),
1084 base_objects_cid.as_ref().expect("checked is_some"),
1085 ))?;
1086 if imported {
1087 continue;
1088 }
1089 return Err(Error::ObjectNotFound(oid).into());
1090 }
1091 Err(err) => return Err(err.into()),
1092 }
1093 };
1094
1095 if let Ok(mut root) = self.root_cid.write() {
1097 *root = Some(root_cid.clone());
1098 }
1099
1100 Ok(root_cid)
1101 }
1102
1103 async fn build_working_tree_entries(
1105 &self,
1106 tree_oid: &str,
1107 objects: &HashMap<String, Vec<u8>>,
1108 ) -> Result<Vec<DirEntry>> {
1109 let mut entries = Vec::new();
1110
1111 let (obj_type, content) = self
1113 .get_object_content(tree_oid, objects)
1114 .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
1115
1116 if obj_type != ObjectType::Tree {
1117 return Err(Error::InvalidObjectType(format!(
1118 "expected tree, got {:?}",
1119 obj_type
1120 )));
1121 }
1122
1123 let tree_entries = parse_tree(&content)?;
1125
1126 for entry in tree_entries {
1127 let oid_hex = entry.oid.to_hex();
1128
1129 if entry.is_tree() {
1130 let sub_entries = self
1132 .build_working_tree_entries_boxed(&oid_hex, objects)
1133 .await?;
1134
1135 let dir_cid =
1137 self.tree.put_directory(sub_entries).await.map_err(|e| {
1138 Error::StorageError(format!("put dir {}: {}", entry.name, e))
1139 })?;
1140
1141 entries
1143 .push(DirEntry::from_cid(&entry.name, &dir_cid).with_link_type(LinkType::Dir));
1144 } else {
1145 if let Some((ObjectType::Blob, blob_content)) =
1147 self.get_object_content(&oid_hex, objects)
1148 {
1149 let (cid, size) = self.tree.put(&blob_content).await.map_err(|e| {
1151 Error::StorageError(format!("put blob {}: {}", entry.name, e))
1152 })?;
1153
1154 entries.push(DirEntry::from_cid(&entry.name, &cid).with_size(size));
1156 }
1157 }
1158 }
1159
1160 entries.sort_by(|a, b| a.name.cmp(&b.name));
1162
1163 Ok(entries)
1164 }
1165
1166 fn build_working_tree_entries_boxed<'a>(
1168 &'a self,
1169 tree_oid: &'a str,
1170 objects: &'a HashMap<String, Vec<u8>>,
1171 ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1172 Box::pin(self.build_working_tree_entries(tree_oid, objects))
1173 }
1174
1175 fn build_working_tree_entries_with_base_recursive_boxed<'a, S: Store + 'a>(
1176 &'a self,
1177 tree_oid: &'a str,
1178 objects: &'a HashMap<String, Vec<u8>>,
1179 base_tree: &'a HashTree<S>,
1180 base_objects_cid: &'a Cid,
1181 old_tree_oid: Option<&'a str>,
1182 old_dir_entries: Option<&'a Vec<hashtree_core::TreeEntry>>,
1183 ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1184 Box::pin(async move {
1185 let tree_entries = self
1186 .get_tree_entries_from_sources(
1187 tree_oid,
1188 objects,
1189 Some(base_tree),
1190 Some(base_objects_cid),
1191 )
1192 .await?;
1193 let old_tree_entries = if let Some(old_tree_oid) = old_tree_oid {
1194 Some(
1195 self.get_tree_entries_from_sources(
1196 old_tree_oid,
1197 objects,
1198 Some(base_tree),
1199 Some(base_objects_cid),
1200 )
1201 .await?,
1202 )
1203 } else {
1204 None
1205 };
1206
1207 let mut old_tree_entry_map = old_tree_entries
1208 .unwrap_or_default()
1209 .into_iter()
1210 .map(|entry| (entry.name.clone(), entry))
1211 .collect::<HashMap<_, _>>();
1212 let mut old_dir_entry_map = old_dir_entries
1213 .cloned()
1214 .unwrap_or_default()
1215 .into_iter()
1216 .map(|entry| (entry.name.clone(), entry))
1217 .collect::<HashMap<_, _>>();
1218
1219 let mut entries = Vec::new();
1220 for entry in tree_entries {
1221 let oid_hex = entry.oid.to_hex();
1222 let old_tree_entry = old_tree_entry_map.remove(&entry.name);
1223 let old_dir_entry = old_dir_entry_map.remove(&entry.name);
1224
1225 if entry.is_tree() {
1226 if let (Some(old_tree_entry), Some(old_dir_entry)) =
1227 (old_tree_entry.as_ref(), old_dir_entry.as_ref())
1228 {
1229 if old_tree_entry.is_tree()
1230 && old_tree_entry.oid == entry.oid
1231 && old_dir_entry.link_type == LinkType::Dir
1232 {
1233 entries.push(Self::tree_entry_to_dir_entry(old_dir_entry));
1234 continue;
1235 }
1236 }
1237
1238 let old_subtree_oid = old_tree_entry
1239 .as_ref()
1240 .filter(|old| old.is_tree())
1241 .map(|old| old.oid.to_hex());
1242 let old_subdir_entries = if let Some(old_dir_entry) = old_dir_entry.as_ref() {
1243 if old_dir_entry.link_type == LinkType::Dir {
1244 Some(
1245 base_tree
1246 .list_directory(&Cid {
1247 hash: old_dir_entry.hash,
1248 key: old_dir_entry.key,
1249 })
1250 .await
1251 .map_err(|e| {
1252 Error::StorageError(format!(
1253 "list base working dir {}: {}",
1254 entry.name, e
1255 ))
1256 })?,
1257 )
1258 } else {
1259 None
1260 }
1261 } else {
1262 None
1263 };
1264
1265 let sub_entries = self
1266 .build_working_tree_entries_with_base_recursive_boxed(
1267 &oid_hex,
1268 objects,
1269 base_tree,
1270 base_objects_cid,
1271 old_subtree_oid.as_deref(),
1272 old_subdir_entries.as_ref(),
1273 )
1274 .await?;
1275 let dir_cid = self.tree.put_directory(sub_entries).await.map_err(|e| {
1276 Error::StorageError(format!("put dir {}: {}", entry.name, e))
1277 })?;
1278 entries.push(
1279 DirEntry::from_cid(&entry.name, &dir_cid).with_link_type(LinkType::Dir),
1280 );
1281 continue;
1282 }
1283
1284 if let (Some(old_tree_entry), Some(old_dir_entry)) =
1285 (old_tree_entry.as_ref(), old_dir_entry.as_ref())
1286 {
1287 if !old_tree_entry.is_tree()
1288 && old_tree_entry.oid == entry.oid
1289 && old_dir_entry.link_type != LinkType::Dir
1290 {
1291 entries.push(Self::tree_entry_to_dir_entry(old_dir_entry));
1292 continue;
1293 }
1294 }
1295
1296 let blob_content = match self.get_object_content(&oid_hex, objects) {
1297 Some((ObjectType::Blob, blob_content)) => blob_content,
1298 Some((obj_type, _)) => {
1299 return Err(Error::InvalidObjectType(format!(
1300 "expected blob, got {:?}",
1301 obj_type
1302 )));
1303 }
1304 None => match self
1305 .get_object_content_from_base(&oid_hex, base_tree, base_objects_cid)
1306 .await?
1307 {
1308 Some((ObjectType::Blob, blob_content)) => blob_content,
1309 Some((obj_type, _)) => {
1310 return Err(Error::InvalidObjectType(format!(
1311 "expected blob, got {:?}",
1312 obj_type
1313 )));
1314 }
1315 None => return Err(Error::ObjectNotFound(oid_hex)),
1316 },
1317 };
1318
1319 let (cid, size) =
1320 self.tree.put(&blob_content).await.map_err(|e| {
1321 Error::StorageError(format!("put blob {}: {}", entry.name, e))
1322 })?;
1323 entries.push(DirEntry::from_cid(&entry.name, &cid).with_size(size));
1324 }
1325
1326 entries.sort_by(|a, b| a.name.cmp(&b.name));
1327 Ok(entries)
1328 })
1329 }
1330
1331 async fn build_working_tree_entries_with_base<S: Store>(
1332 &self,
1333 tree_oid: &str,
1334 objects: &HashMap<String, Vec<u8>>,
1335 base_tree: &HashTree<S>,
1336 base_objects_cid: &Cid,
1337 base_tree_oid: &str,
1338 base_root_entries: Option<&Vec<hashtree_core::TreeEntry>>,
1339 ) -> Result<Vec<DirEntry>> {
1340 let root_entries = base_root_entries.map(|entries| {
1341 entries
1342 .iter()
1343 .filter(|entry| entry.name != ".git")
1344 .cloned()
1345 .collect::<Vec<_>>()
1346 });
1347
1348 self.build_working_tree_entries_with_base_recursive_boxed(
1349 tree_oid,
1350 objects,
1351 base_tree,
1352 base_objects_cid,
1353 Some(base_tree_oid),
1354 root_entries.as_ref(),
1355 )
1356 .await
1357 }
1358
1359 async fn build_objects_prefix_dir(
1360 &self,
1361 prefix: &str,
1362 old_entries: Option<Vec<hashtree_core::TreeEntry>>,
1363 new_objects: &[(String, Vec<u8>)],
1364 ) -> Result<Cid> {
1365 let mut sub_entries: BTreeMap<String, DirEntry> = old_entries
1366 .unwrap_or_default()
1367 .into_iter()
1368 .map(|entry| (entry.name.clone(), Self::tree_entry_to_dir_entry(&entry)))
1369 .collect();
1370
1371 for (suffix, data) in new_objects {
1372 let (cid, size) = self.tree.put(data).await.map_err(|e| {
1373 Error::StorageError(format!("put object {}{}: {}", prefix, suffix, e))
1374 })?;
1375 sub_entries.insert(
1376 suffix.clone(),
1377 DirEntry::from_cid(suffix, &cid).with_size(size),
1378 );
1379 }
1380
1381 self.tree
1382 .put_directory(sub_entries.into_values().collect())
1383 .await
1384 .map_err(|e| Error::StorageError(format!("put objects/{}: {}", prefix, e)))
1385 }
1386
1387 async fn build_objects_dir_with_base<S: Store>(
1390 &self,
1391 objects: &HashMap<String, Vec<u8>>,
1392 base_tree: Option<&HashTree<S>>,
1393 base_objects_cid: Option<&Cid>,
1394 ) -> Result<Cid> {
1395 let mut buckets: BTreeMap<String, Vec<(String, Vec<u8>)>> = BTreeMap::new();
1396 for (oid, data) in objects {
1397 let prefix = &oid[..2];
1398 let suffix = &oid[2..];
1399 buckets
1400 .entry(prefix.to_string())
1401 .or_default()
1402 .push((suffix.to_string(), data.clone()));
1403 }
1404
1405 let mut top_entries: BTreeMap<String, DirEntry> = BTreeMap::new();
1406 let mut merged_prefixes = std::collections::HashSet::new();
1407
1408 if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
1409 for entry in base_tree
1410 .list_directory(base_objects_cid)
1411 .await
1412 .map_err(|e| Error::StorageError(format!("list base objects dir: {}", e)))?
1413 {
1414 if entry.name == "info" {
1415 top_entries.insert(entry.name.clone(), Self::tree_entry_to_dir_entry(&entry));
1416 continue;
1417 }
1418
1419 let Some(delta_objects) = buckets.get(&entry.name) else {
1420 top_entries.insert(entry.name.clone(), Self::tree_entry_to_dir_entry(&entry));
1421 continue;
1422 };
1423
1424 let prefix_cid = Cid {
1425 hash: entry.hash,
1426 key: entry.key,
1427 };
1428 let old_prefix_entries = if entry.link_type == LinkType::Dir {
1429 Some(base_tree.list_directory(&prefix_cid).await.map_err(|e| {
1430 Error::StorageError(format!("list base objects/{} dir: {}", entry.name, e))
1431 })?)
1432 } else {
1433 None
1434 };
1435 let merged_cid = self
1436 .build_objects_prefix_dir(&entry.name, old_prefix_entries, delta_objects)
1437 .await?;
1438 top_entries.insert(
1439 entry.name.clone(),
1440 DirEntry::from_cid(&entry.name, &merged_cid).with_link_type(LinkType::Dir),
1441 );
1442 merged_prefixes.insert(entry.name);
1443 }
1444 }
1445
1446 for (prefix, objs) in &buckets {
1447 if merged_prefixes.contains(prefix) {
1448 continue;
1449 }
1450 let sub_cid = self.build_objects_prefix_dir(prefix, None, objs).await?;
1451 top_entries.insert(
1452 prefix.clone(),
1453 DirEntry::from_cid(prefix, &sub_cid).with_link_type(LinkType::Dir),
1454 );
1455 }
1456
1457 if !top_entries.contains_key("info") {
1458 let (packs_cid, packs_size) = self
1459 .tree
1460 .put(b"")
1461 .await
1462 .map_err(|e| Error::StorageError(format!("put objects/info/packs: {}", e)))?;
1463 let info_cid = self
1464 .tree
1465 .put_directory(vec![
1466 DirEntry::from_cid("packs", &packs_cid).with_size(packs_size)
1467 ])
1468 .await
1469 .map_err(|e| Error::StorageError(format!("put objects/info: {}", e)))?;
1470 top_entries.insert(
1471 "info".to_string(),
1472 DirEntry::from_cid("info", &info_cid).with_link_type(LinkType::Dir),
1473 );
1474 }
1475
1476 let entry_count = top_entries.len();
1477 let cid = self
1478 .tree
1479 .put_directory(top_entries.into_values().collect())
1480 .await
1481 .map_err(|e| Error::StorageError(format!("put objects dir: {}", e)))?;
1482
1483 debug!(
1484 "Built objects dir with {} entries: {}",
1485 entry_count,
1486 hex::encode(cid.hash)
1487 );
1488 Ok(cid)
1489 }
1490
1491 async fn build_refs_dir(&self, refs: &HashMap<String, String>) -> Result<Cid> {
1493 let mut root = RefDirectory::default();
1494
1495 for (ref_name, value) in refs {
1496 let parts: Vec<&str> = ref_name.split('/').collect();
1497 if parts.len() >= 3 && parts[0] == "refs" {
1498 root.insert(&parts[1..], value.clone());
1499 }
1500 }
1501
1502 let mut ref_entries = self.build_ref_entries_recursive(&root, "refs").await?;
1503
1504 if ref_entries.is_empty() {
1505 let empty_cid = self
1507 .tree
1508 .put_directory(vec![])
1509 .await
1510 .map_err(|e| Error::StorageError(format!("put empty refs: {}", e)))?;
1511 return Ok(empty_cid);
1512 }
1513
1514 ref_entries.sort_by(|a, b| a.name.cmp(&b.name));
1515
1516 let refs_cid = self
1517 .tree
1518 .put_directory(ref_entries)
1519 .await
1520 .map_err(|e| Error::StorageError(format!("put refs dir: {}", e)))?;
1521 debug!("refs dir -> {}", hex::encode(refs_cid.hash));
1522 Ok(refs_cid)
1523 }
1524
1525 fn build_ref_entries_recursive<'a>(
1526 &'a self,
1527 dir: &'a RefDirectory,
1528 prefix: &'a str,
1529 ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1530 Box::pin(async move {
1531 let mut entries = Vec::new();
1532
1533 for (name, value) in &dir.files {
1534 let (cid, size) = self
1535 .tree
1536 .put(value.as_bytes())
1537 .await
1538 .map_err(|e| Error::StorageError(format!("put ref: {}", e)))?;
1539 debug!("{}/{} -> blob {}", prefix, name, hex::encode(cid.hash));
1540 entries.push(DirEntry::from_cid(name, &cid).with_size(size));
1541 }
1542
1543 for (name, child) in &dir.dirs {
1544 let child_prefix = format!("{prefix}/{name}");
1545 let child_entries = self
1546 .build_ref_entries_recursive(child, &child_prefix)
1547 .await?;
1548 let child_cid =
1549 self.tree.put_directory(child_entries).await.map_err(|e| {
1550 Error::StorageError(format!("put {child_prefix} dir: {}", e))
1551 })?;
1552 debug!("{} dir -> {}", child_prefix, hex::encode(child_cid.hash));
1553 entries.push(DirEntry::from_cid(name, &child_cid).with_link_type(LinkType::Dir));
1554 }
1555
1556 entries.sort_by(|a, b| a.name.cmp(&b.name));
1557 Ok(entries)
1558 })
1559 }
1560
1561 fn build_index_file(
1564 &self,
1565 tree_oid: &str,
1566 objects: &HashMap<String, Vec<u8>>,
1567 ) -> Result<Vec<u8>> {
1568 let mut entries: Vec<(String, [u8; 20], u32, u32)> = Vec::new(); self.collect_tree_entries_for_index(tree_oid, objects, "", &mut entries)?;
1571
1572 self.build_index_bytes(entries)
1573 }
1574
1575 async fn build_index_file_with_base<S: Store>(
1576 &self,
1577 tree_oid: &str,
1578 objects: &HashMap<String, Vec<u8>>,
1579 base_tree: &HashTree<S>,
1580 base_objects_cid: &Cid,
1581 base_tree_oid: &str,
1582 base_root_entries: Option<&Vec<hashtree_core::TreeEntry>>,
1583 ) -> Result<Vec<u8>> {
1584 let mut entries: Vec<(String, [u8; 20], u32, u32)> = Vec::new();
1585 let root_entries = base_root_entries.map(|entries| {
1586 entries
1587 .iter()
1588 .filter(|entry| entry.name != ".git")
1589 .cloned()
1590 .collect::<Vec<_>>()
1591 });
1592
1593 self.collect_tree_entries_for_index_with_base_boxed(
1594 tree_oid,
1595 objects,
1596 base_tree,
1597 base_objects_cid,
1598 Some(base_tree_oid),
1599 root_entries.as_ref(),
1600 "",
1601 &mut entries,
1602 )
1603 .await?;
1604
1605 self.build_index_bytes(entries)
1606 }
1607
1608 fn build_index_bytes(&self, mut entries: Vec<(String, [u8; 20], u32, u32)>) -> Result<Vec<u8>> {
1609 entries.sort_by(|a, b| a.0.cmp(&b.0));
1611
1612 let entry_count = entries.len() as u32;
1613 debug!("Building git index with {} entries", entry_count);
1614
1615 let mut index_data = Vec::new();
1617
1618 index_data.extend_from_slice(b"DIRC");
1620 index_data.extend_from_slice(&2u32.to_be_bytes()); index_data.extend_from_slice(&entry_count.to_be_bytes());
1622
1623 let now_sec = std::time::SystemTime::now()
1625 .duration_since(std::time::UNIX_EPOCH)
1626 .unwrap_or_default()
1627 .as_secs() as u32;
1628
1629 for (path, sha1, mode, size) in &entries {
1630 let entry_start = index_data.len();
1631
1632 index_data.extend_from_slice(&now_sec.to_be_bytes());
1634 index_data.extend_from_slice(&0u32.to_be_bytes());
1635 index_data.extend_from_slice(&now_sec.to_be_bytes());
1637 index_data.extend_from_slice(&0u32.to_be_bytes());
1638 index_data.extend_from_slice(&0u32.to_be_bytes());
1640 index_data.extend_from_slice(&0u32.to_be_bytes());
1641 index_data.extend_from_slice(&mode.to_be_bytes());
1643 index_data.extend_from_slice(&0u32.to_be_bytes());
1645 index_data.extend_from_slice(&0u32.to_be_bytes());
1646 index_data.extend_from_slice(&size.to_be_bytes());
1648 index_data.extend_from_slice(sha1);
1650 let path_len = std::cmp::min(path.len(), 0xFFF) as u16;
1652 index_data.extend_from_slice(&path_len.to_be_bytes());
1653 index_data.extend_from_slice(path.as_bytes());
1655 index_data.push(0); let entry_len = index_data.len() - entry_start;
1659 let padding = (8 - (entry_len % 8)) % 8;
1660 index_data.extend(std::iter::repeat_n(0, padding));
1661 }
1662
1663 let mut hasher = Sha1::new();
1665 hasher.update(&index_data);
1666 let checksum = hasher.finalize();
1667 index_data.extend_from_slice(&checksum);
1668
1669 debug!(
1670 "Built git index: {} bytes, {} entries",
1671 index_data.len(),
1672 entry_count
1673 );
1674 Ok(index_data)
1675 }
1676
1677 fn collect_tree_entries_for_index(
1679 &self,
1680 tree_oid: &str,
1681 objects: &HashMap<String, Vec<u8>>,
1682 prefix: &str,
1683 entries: &mut Vec<(String, [u8; 20], u32, u32)>,
1684 ) -> Result<()> {
1685 let (obj_type, content) = self
1686 .get_object_content(tree_oid, objects)
1687 .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
1688
1689 if obj_type != ObjectType::Tree {
1690 return Err(Error::InvalidObjectType(format!(
1691 "expected tree, got {:?}",
1692 obj_type
1693 )));
1694 }
1695
1696 let tree_entries = parse_tree(&content)?;
1697
1698 for entry in tree_entries {
1699 let path = if prefix.is_empty() {
1700 entry.name.clone()
1701 } else {
1702 format!("{}/{}", prefix, entry.name)
1703 };
1704
1705 let oid_hex = entry.oid.to_hex();
1706
1707 if entry.is_tree() {
1708 self.collect_tree_entries_for_index(&oid_hex, objects, &path, entries)?;
1710 } else {
1711 if let Some((ObjectType::Blob, blob_content)) =
1713 self.get_object_content(&oid_hex, objects)
1714 {
1715 let mut sha1_bytes = [0u8; 20];
1717 if let Ok(bytes) = hex::decode(&oid_hex) {
1718 if bytes.len() == 20 {
1719 sha1_bytes.copy_from_slice(&bytes);
1720 }
1721 }
1722
1723 let mode = entry.mode;
1725 let size = blob_content.len() as u32;
1726
1727 entries.push((path, sha1_bytes, mode, size));
1728 }
1729 }
1730 }
1731
1732 Ok(())
1733 }
1734
1735 fn collect_tree_entries_for_index_with_base_boxed<'a, S: Store + 'a>(
1736 &'a self,
1737 tree_oid: &'a str,
1738 objects: &'a HashMap<String, Vec<u8>>,
1739 base_tree: &'a HashTree<S>,
1740 base_objects_cid: &'a Cid,
1741 old_tree_oid: Option<&'a str>,
1742 old_dir_entries: Option<&'a Vec<hashtree_core::TreeEntry>>,
1743 prefix: &'a str,
1744 entries: &'a mut Vec<(String, [u8; 20], u32, u32)>,
1745 ) -> BoxFuture<'a, Result<()>> {
1746 Box::pin(async move {
1747 let tree_entries = self
1748 .get_tree_entries_from_sources(
1749 tree_oid,
1750 objects,
1751 Some(base_tree),
1752 Some(base_objects_cid),
1753 )
1754 .await?;
1755 let old_tree_entries = if let Some(old_tree_oid) = old_tree_oid {
1756 Some(
1757 self.get_tree_entries_from_sources(
1758 old_tree_oid,
1759 objects,
1760 Some(base_tree),
1761 Some(base_objects_cid),
1762 )
1763 .await?,
1764 )
1765 } else {
1766 None
1767 };
1768 let mut old_tree_entry_map = old_tree_entries
1769 .unwrap_or_default()
1770 .into_iter()
1771 .map(|entry| (entry.name.clone(), entry))
1772 .collect::<HashMap<_, _>>();
1773 let mut old_dir_entry_map = old_dir_entries
1774 .cloned()
1775 .unwrap_or_default()
1776 .into_iter()
1777 .map(|entry| (entry.name.clone(), entry))
1778 .collect::<HashMap<_, _>>();
1779
1780 for entry in tree_entries {
1781 let path = if prefix.is_empty() {
1782 entry.name.clone()
1783 } else {
1784 format!("{}/{}", prefix, entry.name)
1785 };
1786 let oid_hex = entry.oid.to_hex();
1787 let old_tree_entry = old_tree_entry_map.remove(&entry.name);
1788 let old_dir_entry = old_dir_entry_map.remove(&entry.name);
1789
1790 if entry.is_tree() {
1791 let old_subtree_oid = old_tree_entry
1792 .as_ref()
1793 .filter(|old| old.is_tree())
1794 .map(|old| old.oid.to_hex());
1795 let old_subdir_entries = if let Some(old_dir_entry) = old_dir_entry.as_ref() {
1796 if old_dir_entry.link_type == LinkType::Dir {
1797 Some(
1798 base_tree
1799 .list_directory(&Cid {
1800 hash: old_dir_entry.hash,
1801 key: old_dir_entry.key,
1802 })
1803 .await
1804 .map_err(|e| {
1805 Error::StorageError(format!(
1806 "list base working dir {}: {}",
1807 path, e
1808 ))
1809 })?,
1810 )
1811 } else {
1812 None
1813 }
1814 } else {
1815 None
1816 };
1817
1818 self.collect_tree_entries_for_index_with_base_boxed(
1819 &oid_hex,
1820 objects,
1821 base_tree,
1822 base_objects_cid,
1823 old_subtree_oid.as_deref(),
1824 old_subdir_entries.as_ref(),
1825 &path,
1826 entries,
1827 )
1828 .await?;
1829 continue;
1830 }
1831
1832 let mut sha1_bytes = [0u8; 20];
1833 if let Ok(bytes) = hex::decode(&oid_hex) {
1834 if bytes.len() == 20 {
1835 sha1_bytes.copy_from_slice(&bytes);
1836 }
1837 }
1838
1839 let size = if let (Some(old_tree_entry), Some(old_dir_entry)) =
1840 (old_tree_entry.as_ref(), old_dir_entry.as_ref())
1841 {
1842 if !old_tree_entry.is_tree()
1843 && old_tree_entry.oid == entry.oid
1844 && old_dir_entry.link_type != LinkType::Dir
1845 {
1846 old_dir_entry.size as u32
1847 } else {
1848 self.blob_size_from_sources(
1849 &oid_hex,
1850 objects,
1851 Some(base_tree),
1852 Some(base_objects_cid),
1853 )
1854 .await? as u32
1855 }
1856 } else {
1857 self.blob_size_from_sources(
1858 &oid_hex,
1859 objects,
1860 Some(base_tree),
1861 Some(base_objects_cid),
1862 )
1863 .await? as u32
1864 };
1865
1866 entries.push((path, sha1_bytes, entry.mode, size));
1867 }
1868
1869 Ok(())
1870 })
1871 }
1872
1873 async fn blob_size_from_sources<S: Store>(
1874 &self,
1875 oid: &str,
1876 objects: &HashMap<String, Vec<u8>>,
1877 base_tree: Option<&HashTree<S>>,
1878 base_objects_cid: Option<&Cid>,
1879 ) -> Result<usize> {
1880 let object = match self.get_object_content(oid, objects) {
1881 Some(object) => Some(object),
1882 None => {
1883 if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
1884 self.get_object_content_from_base(oid, base_tree, base_objects_cid)
1885 .await?
1886 } else {
1887 None
1888 }
1889 }
1890 }
1891 .ok_or_else(|| Error::ObjectNotFound(oid.to_string()))?;
1892
1893 if object.0 != ObjectType::Blob {
1894 return Err(Error::InvalidObjectType(format!(
1895 "expected blob, got {:?}",
1896 object.0
1897 )));
1898 }
1899
1900 Ok(object.1.len())
1901 }
1902
1903 pub fn store(&self) -> &Arc<LocalStore> {
1905 &self.store
1906 }
1907
1908 #[allow(dead_code)]
1910 pub fn hashtree(&self) -> &HashTree<LocalStore> {
1911 &self.tree
1912 }
1913
1914 #[allow(dead_code)]
1916 pub fn push_to_file_servers(
1917 &self,
1918 blossom: &hashtree_blossom::BlossomClient,
1919 ) -> Result<(usize, usize)> {
1920 let hashes = self
1921 .store
1922 .list()
1923 .map_err(|e| Error::StorageError(format!("list hashes: {}", e)))?;
1924
1925 info!("Pushing {} blobs to file servers", hashes.len());
1926
1927 let mut uploaded = 0;
1928 let mut existed = 0;
1929
1930 self.runtime.block_on(async {
1931 for hash in &hashes {
1932 let hex_hash = hex::encode(hash);
1933 let data = match self.store.get_sync(hash) {
1934 Ok(Some(d)) => d,
1935 _ => continue,
1936 };
1937
1938 match blossom.upload_if_missing(&data).await {
1939 Ok((_, true)) => {
1940 debug!("Uploaded {}", &hex_hash[..12]);
1941 uploaded += 1;
1942 }
1943 Ok((_, false)) => {
1944 existed += 1;
1945 }
1946 Err(e) => {
1947 debug!("Failed to upload {}: {}", &hex_hash[..12], e);
1948 }
1949 }
1950 }
1951 });
1952
1953 info!(
1954 "Upload complete: {} new, {} already existed",
1955 uploaded, existed
1956 );
1957 Ok((uploaded, existed))
1958 }
1959
1960 #[allow(dead_code)]
1962 pub fn clear(&self) -> Result<()> {
1963 let mut objects = self
1964 .objects
1965 .write()
1966 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
1967 let mut refs = self
1968 .refs
1969 .write()
1970 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
1971 let mut root = self
1972 .root_cid
1973 .write()
1974 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
1975
1976 objects.clear();
1977 refs.clear();
1978 *root = None;
1979 Ok(())
1980 }
1981}
1982
1983#[cfg(test)]
1984mod tests {
1985 use super::*;
1986 use hashtree_core::store::Store;
1987 use hashtree_core::{sha256, LinkType};
1988 use std::io::{Read, Write};
1989 use std::net::{TcpListener, TcpStream};
1990 use std::path::Path;
1991 use std::process::{Child, Command, Stdio};
1992 #[cfg(feature = "lmdb")]
1993 use std::sync::atomic::{AtomicUsize, Ordering};
1994 use std::time::{Duration, Instant};
1995 use tempfile::TempDir;
1996
1997 fn create_test_storage() -> (GitStorage, TempDir) {
1998 let temp_dir = TempDir::new().unwrap();
1999 let storage = GitStorage::open(temp_dir.path()).unwrap();
2000 (storage, temp_dir)
2001 }
2002
2003 fn create_test_storage_with_limit(max_size_bytes: u64) -> (GitStorage, TempDir) {
2004 let temp_dir = TempDir::new().unwrap();
2005 let storage = GitStorage::open_with_backend_and_max_bytes(
2006 temp_dir.path(),
2007 StorageBackend::Fs,
2008 max_size_bytes,
2009 )
2010 .unwrap();
2011 (storage, temp_dir)
2012 }
2013
2014 fn local_total_bytes(storage: &GitStorage) -> u64 {
2015 match storage.store().as_ref() {
2016 LocalStore::Fs(store) => store.stats().unwrap().total_bytes,
2017 #[cfg(feature = "lmdb")]
2018 LocalStore::Lmdb(store) => store.stats().unwrap().total_bytes,
2019 }
2020 }
2021
2022 fn write_test_commit(storage: &GitStorage) -> ObjectId {
2023 let blob_oid = storage
2024 .write_raw_object(ObjectType::Blob, b"hello from hashtree\n")
2025 .unwrap();
2026
2027 let mut tree_content = Vec::new();
2028 tree_content.extend_from_slice(b"100644 README.md\0");
2029 tree_content.extend_from_slice(&hex::decode(blob_oid.to_hex()).unwrap());
2030 let tree_oid = storage
2031 .write_raw_object(ObjectType::Tree, &tree_content)
2032 .unwrap();
2033
2034 let commit_content = format!(
2035 "tree {}\nauthor Test User <test@example.com> 0 +0000\ncommitter Test User <test@example.com> 0 +0000\n\nInitial commit\n",
2036 tree_oid.to_hex()
2037 );
2038 storage
2039 .write_raw_object(ObjectType::Commit, commit_content.as_bytes())
2040 .unwrap()
2041 }
2042
2043 fn export_tree_to_fs<S: Store>(
2044 runtime: &RuntimeExecutor,
2045 tree: &HashTree<S>,
2046 cid: &Cid,
2047 dst: &Path,
2048 ) {
2049 std::fs::create_dir_all(dst).unwrap();
2050 let entries = runtime.block_on(tree.list_directory(cid)).unwrap();
2051 for entry in entries {
2052 let entry_cid = Cid {
2053 hash: entry.hash,
2054 key: entry.key,
2055 };
2056 let path = dst.join(&entry.name);
2057 match entry.link_type {
2058 LinkType::Dir => export_tree_to_fs(runtime, tree, &entry_cid, &path),
2059 LinkType::Blob | LinkType::File => {
2060 let data = runtime
2061 .block_on(tree.get(&entry_cid, None))
2062 .unwrap()
2063 .unwrap();
2064 if let Some(parent) = path.parent() {
2065 std::fs::create_dir_all(parent).unwrap();
2066 }
2067 std::fs::write(path, data).unwrap();
2068 }
2069 }
2070 }
2071 }
2072
2073 fn spawn_http_server(root: &Path, port: u16) -> Child {
2074 Command::new("python3")
2075 .args([
2076 "-m",
2077 "http.server",
2078 &port.to_string(),
2079 "--bind",
2080 "127.0.0.1",
2081 ])
2082 .current_dir(root)
2083 .stdout(Stdio::null())
2084 .stderr(Stdio::null())
2085 .spawn()
2086 .expect("spawn python http server")
2087 }
2088
2089 fn wait_for_http_server(server: &mut Child, port: u16, path: &str) {
2090 let deadline = Instant::now() + Duration::from_secs(5);
2091
2092 loop {
2093 if let Some(status) = server.try_wait().expect("check http server status") {
2094 panic!("python http server exited before becoming ready: {status}");
2095 }
2096
2097 if let Ok(mut stream) = TcpStream::connect(("127.0.0.1", port)) {
2098 stream
2099 .set_read_timeout(Some(Duration::from_millis(200)))
2100 .expect("set read timeout");
2101 stream
2102 .set_write_timeout(Some(Duration::from_millis(200)))
2103 .expect("set write timeout");
2104 let request =
2105 format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n");
2106 if stream.write_all(request.as_bytes()).is_ok() {
2107 let mut response = String::new();
2108 if stream.read_to_string(&mut response).is_ok()
2109 && response.starts_with("HTTP/1.0 200")
2110 {
2111 return;
2112 }
2113 }
2114 }
2115
2116 if Instant::now() >= deadline {
2117 panic!("python http server did not become ready on port {port}");
2118 }
2119 std::thread::sleep(Duration::from_millis(50));
2120 }
2121 }
2122
2123 #[test]
2124 fn test_import_ref() {
2125 let (storage, _temp) = create_test_storage();
2126
2127 storage
2129 .import_ref("refs/heads/main", "abc123def456")
2130 .unwrap();
2131
2132 assert!(storage.has_ref("refs/heads/main").unwrap());
2134
2135 let refs = storage.list_refs().unwrap();
2137 assert_eq!(
2138 refs.get("refs/heads/main"),
2139 Some(&"abc123def456".to_string())
2140 );
2141 }
2142
2143 #[cfg(feature = "lmdb")]
2144 #[test]
2145 fn test_local_store_falls_back_to_fs_when_lmdb_open_returns_enosys() {
2146 let temp_dir = TempDir::new().unwrap();
2147 let fs_calls = AtomicUsize::new(0);
2148 let lmdb_calls = AtomicUsize::new(0);
2149
2150 let store = LocalStore::new_for_backend_with_openers(
2151 temp_dir.path(),
2152 StorageBackend::Lmdb,
2153 0,
2154 |path, max_bytes| {
2155 fs_calls.fetch_add(1, Ordering::SeqCst);
2156 LocalStore::open_fs_store(path, max_bytes)
2157 },
2158 |_path, _max_bytes| {
2159 lmdb_calls.fetch_add(1, Ordering::SeqCst);
2160 Err(StoreError::Io(std::io::Error::from_raw_os_error(
2161 libc::ENOSYS,
2162 )))
2163 },
2164 )
2165 .unwrap();
2166
2167 assert!(matches!(store, LocalStore::Fs(_)));
2168 assert_eq!(lmdb_calls.load(Ordering::SeqCst), 1);
2169 assert_eq!(fs_calls.load(Ordering::SeqCst), 1);
2170 }
2171
2172 #[cfg(feature = "lmdb")]
2173 #[test]
2174 fn test_local_store_does_not_fallback_on_unrelated_lmdb_errors() {
2175 let temp_dir = TempDir::new().unwrap();
2176 let fs_calls = AtomicUsize::new(0);
2177
2178 let result = LocalStore::new_for_backend_with_openers(
2179 temp_dir.path(),
2180 StorageBackend::Lmdb,
2181 0,
2182 |path, max_bytes| {
2183 fs_calls.fetch_add(1, Ordering::SeqCst);
2184 LocalStore::open_fs_store(path, max_bytes)
2185 },
2186 |_path, _max_bytes| {
2187 Err(StoreError::Io(std::io::Error::from_raw_os_error(
2188 libc::EACCES,
2189 )))
2190 },
2191 );
2192
2193 assert!(
2194 matches!(result, Err(StoreError::Io(io_error)) if io_error.raw_os_error() == Some(libc::EACCES))
2195 );
2196 assert_eq!(fs_calls.load(Ordering::SeqCst), 0);
2197 }
2198
2199 #[test]
2200 fn test_import_multiple_refs_preserves_all() {
2201 let (storage, _temp) = create_test_storage();
2202
2203 storage.import_ref("refs/heads/main", "sha_main").unwrap();
2205 storage.import_ref("refs/heads/dev", "sha_dev").unwrap();
2206 storage
2207 .import_ref("refs/heads/feature", "sha_feature")
2208 .unwrap();
2209
2210 assert!(storage.has_ref("refs/heads/main").unwrap());
2212 assert!(storage.has_ref("refs/heads/dev").unwrap());
2213 assert!(storage.has_ref("refs/heads/feature").unwrap());
2214
2215 storage
2217 .write_ref(
2218 "refs/heads/new-branch",
2219 &Ref::Direct(
2220 ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2221 ),
2222 )
2223 .unwrap();
2224
2225 let refs = storage.list_refs().unwrap();
2227 assert_eq!(refs.len(), 4);
2228 assert!(refs.contains_key("refs/heads/main"));
2229 assert!(refs.contains_key("refs/heads/dev"));
2230 assert!(refs.contains_key("refs/heads/feature"));
2231 assert!(refs.contains_key("refs/heads/new-branch"));
2232 }
2233
2234 #[test]
2235 fn test_import_compressed_object() {
2236 let (storage, _temp) = create_test_storage();
2237
2238 let fake_compressed = vec![0x78, 0x9c, 0x01, 0x02, 0x03]; storage
2242 .import_compressed_object("abc123def456", fake_compressed.clone())
2243 .unwrap();
2244
2245 assert_eq!(storage.object_count().unwrap(), 1);
2247 }
2248
2249 #[test]
2250 fn test_write_ref_overwrites_imported() {
2251 let (storage, _temp) = create_test_storage();
2252
2253 storage.import_ref("refs/heads/main", "old_sha").unwrap();
2255
2256 storage
2258 .write_ref(
2259 "refs/heads/main",
2260 &Ref::Direct(
2261 ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2262 ),
2263 )
2264 .unwrap();
2265
2266 let refs = storage.list_refs().unwrap();
2268 assert_eq!(
2269 refs.get("refs/heads/main"),
2270 Some(&"0123456789abcdef0123456789abcdef01234567".to_string())
2271 );
2272 }
2273
2274 #[test]
2275 fn test_delete_ref_preserves_others() {
2276 let (storage, _temp) = create_test_storage();
2277
2278 storage.import_ref("refs/heads/main", "sha_main").unwrap();
2280 storage.import_ref("refs/heads/dev", "sha_dev").unwrap();
2281
2282 storage.delete_ref("refs/heads/dev").unwrap();
2284
2285 assert!(storage.has_ref("refs/heads/main").unwrap());
2287 assert!(!storage.has_ref("refs/heads/dev").unwrap());
2288 }
2289
2290 #[test]
2291 fn test_clear_removes_all() {
2292 let (storage, _temp) = create_test_storage();
2293
2294 storage.import_ref("refs/heads/main", "sha_main").unwrap();
2296 storage
2297 .import_compressed_object("obj1", vec![1, 2, 3])
2298 .unwrap();
2299
2300 storage.clear().unwrap();
2302
2303 assert!(!storage.has_ref("refs/heads/main").unwrap());
2305 assert_eq!(storage.object_count().unwrap(), 0);
2306 }
2307
2308 #[test]
2309 fn test_evict_if_needed_respects_configured_limit() {
2310 let (storage, _temp) = create_test_storage_with_limit(1_024);
2311
2312 storage
2313 .write_raw_object(ObjectType::Blob, &vec![b'a'; 900])
2314 .unwrap();
2315 storage
2316 .write_raw_object(ObjectType::Blob, &vec![b'b'; 900])
2317 .unwrap();
2318 storage
2319 .write_ref(
2320 "refs/heads/main",
2321 &Ref::Direct(
2322 ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2323 ),
2324 )
2325 .unwrap();
2326
2327 storage.build_tree().unwrap();
2328
2329 let before = local_total_bytes(&storage);
2330 assert!(before > 1_024);
2331
2332 let freed = storage.evict_if_needed().unwrap();
2333 assert!(freed > 0);
2334
2335 let after = local_total_bytes(&storage);
2336 assert!(after <= 1_024);
2337 }
2338
2339 #[test]
2340 fn test_build_tree_evicts_stale_blobs_before_writing_new_tree() {
2341 let max_size_bytes = 16 * 1024;
2342 let (storage, _temp) = create_test_storage_with_limit(max_size_bytes);
2343
2344 let stale_blobs = vec![
2345 vec![b'x'; 7 * 1024],
2346 vec![b'y'; 7 * 1024],
2347 vec![b'z'; 7 * 1024],
2348 ];
2349 let stale_hashes: Vec<Hash> = stale_blobs.iter().map(|blob| sha256(blob)).collect();
2350
2351 for (hash, blob) in stale_hashes.iter().zip(stale_blobs) {
2352 storage
2353 .runtime
2354 .block_on(storage.store().put(*hash, blob))
2355 .unwrap();
2356 }
2357
2358 let before = local_total_bytes(&storage);
2359 assert!(before > max_size_bytes);
2360
2361 let commit_oid = write_test_commit(&storage);
2362 storage
2363 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2364 .unwrap();
2365 storage
2366 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2367 .unwrap();
2368
2369 storage.build_tree().unwrap();
2370
2371 let evicted_stale = stale_hashes
2372 .iter()
2373 .filter(|hash| !storage.runtime.block_on(storage.store().has(hash)).unwrap())
2374 .count();
2375
2376 assert!(
2377 evicted_stale > 0,
2378 "expected build_tree preflight eviction to remove stale blobs before writing"
2379 );
2380 }
2381
2382 #[test]
2383 fn test_build_tree_adds_dumb_http_metadata() {
2384 let (storage, _temp) = create_test_storage();
2385 let commit_oid = write_test_commit(&storage);
2386 let tag_content = format!(
2387 "object {}\ntype commit\ntag v1.0.0\ntagger Test User <test@example.com> 0 +0000\n\nrelease\n",
2388 commit_oid.to_hex()
2389 );
2390 let tag_oid = storage
2391 .write_raw_object(ObjectType::Tag, tag_content.as_bytes())
2392 .unwrap();
2393
2394 storage
2395 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2396 .unwrap();
2397 storage
2398 .write_ref("refs/tags/v1.0.0", &Ref::Direct(tag_oid))
2399 .unwrap();
2400 storage
2401 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2402 .unwrap();
2403
2404 let root_cid = storage.build_tree().unwrap();
2405
2406 let info_refs_cid = storage
2407 .runtime
2408 .block_on(storage.tree.resolve_path(&root_cid, ".git/info/refs"))
2409 .unwrap()
2410 .expect("info/refs exists");
2411 let info_refs = storage
2412 .runtime
2413 .block_on(storage.tree.get(&info_refs_cid, None))
2414 .unwrap()
2415 .unwrap();
2416 let info_refs = String::from_utf8(info_refs).unwrap();
2417
2418 assert_eq!(
2419 info_refs,
2420 format!(
2421 "{commit}\trefs/heads/main\n{tag}\trefs/tags/v1.0.0\n{commit}\trefs/tags/v1.0.0^{{}}\n",
2422 commit = commit_oid.to_hex(),
2423 tag = tag_oid.to_hex()
2424 )
2425 );
2426
2427 let packs_cid = storage
2428 .runtime
2429 .block_on(
2430 storage
2431 .tree
2432 .resolve_path(&root_cid, ".git/objects/info/packs"),
2433 )
2434 .unwrap()
2435 .expect("objects/info/packs exists");
2436 let packs = storage
2437 .runtime
2438 .block_on(storage.tree.get(&packs_cid, None))
2439 .unwrap()
2440 .unwrap();
2441 assert!(packs.is_empty(), "objects/info/packs should be empty");
2442 }
2443
2444 #[test]
2445 fn test_build_tree_materializes_loose_refs_at_git_paths() {
2446 let (storage, _temp) = create_test_storage();
2447 let commit_oid = write_test_commit(&storage);
2448
2449 storage
2450 .write_ref("refs/heads/master", &Ref::Direct(commit_oid))
2451 .unwrap();
2452 storage
2453 .write_ref("refs/heads/codex/meshrouter-prod", &Ref::Direct(commit_oid))
2454 .unwrap();
2455 storage
2456 .write_ref("refs/tags/v1.0.0", &Ref::Direct(commit_oid))
2457 .unwrap();
2458 storage
2459 .write_ref("HEAD", &Ref::Symbolic("refs/heads/master".to_string()))
2460 .unwrap();
2461
2462 let root_cid = storage.build_tree().unwrap();
2463
2464 for path in [
2465 ".git/refs/heads/master",
2466 ".git/refs/heads/codex/meshrouter-prod",
2467 ".git/refs/tags/v1.0.0",
2468 ] {
2469 let ref_cid = storage
2470 .runtime
2471 .block_on(storage.tree.resolve_path(&root_cid, path))
2472 .unwrap()
2473 .unwrap_or_else(|| panic!("{path} should exist"));
2474 let ref_value = storage
2475 .runtime
2476 .block_on(storage.tree.get(&ref_cid, None))
2477 .unwrap()
2478 .unwrap();
2479 assert_eq!(
2480 String::from_utf8(ref_value).unwrap(),
2481 commit_oid.to_hex(),
2482 "{path} should contain the ref target",
2483 );
2484 }
2485 }
2486
2487 #[test]
2488 fn test_materialized_tree_supports_static_http_clone_from_git_dir() {
2489 let (storage, _temp) = create_test_storage();
2490 let commit_oid = write_test_commit(&storage);
2491 storage
2492 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2493 .unwrap();
2494 storage
2495 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2496 .unwrap();
2497
2498 let root_cid = storage.build_tree().unwrap();
2499 let export_dir = TempDir::new().unwrap();
2500 let repo_dir = export_dir.path().join("repo");
2501 export_tree_to_fs(&storage.runtime, &storage.tree, &root_cid, &repo_dir);
2502
2503 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2504 let port = listener.local_addr().unwrap().port();
2505 drop(listener);
2506
2507 let mut server = spawn_http_server(export_dir.path(), port);
2508 wait_for_http_server(&mut server, port, "/repo/.git/HEAD");
2509
2510 let clone_dir = TempDir::new().unwrap();
2511 let clone_path = clone_dir.path().join("clone");
2512 let output = Command::new("git")
2513 .args([
2514 "clone",
2515 &format!("http://127.0.0.1:{port}/repo/.git", port = port),
2516 clone_path.to_str().unwrap(),
2517 ])
2518 .output()
2519 .unwrap();
2520
2521 let _ = server.kill();
2522 let _ = server.wait();
2523
2524 assert!(
2525 output.status.success(),
2526 "git clone failed: {}",
2527 String::from_utf8_lossy(&output.stderr)
2528 );
2529 assert_eq!(
2530 std::fs::read_to_string(clone_path.join("README.md")).unwrap(),
2531 "hello from hashtree\n"
2532 );
2533 }
2534
2535 #[test]
2536 fn test_validate_root_contains_direct_refs_rejects_missing_tip_object() {
2537 let (storage, _temp) = create_test_storage();
2538 let commit_oid = write_test_commit(&storage);
2539 storage
2540 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2541 .unwrap();
2542 storage
2543 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2544 .unwrap();
2545
2546 let empty_objects_dir = storage
2547 .runtime
2548 .block_on(storage.tree.put_directory(vec![]))
2549 .unwrap();
2550 let refs_dir = storage
2551 .runtime
2552 .block_on(storage.tree.put_directory(vec![]))
2553 .unwrap();
2554 let info_dir = storage
2555 .runtime
2556 .block_on(storage.tree.put_directory(vec![]))
2557 .unwrap();
2558 let git_dir = storage
2559 .runtime
2560 .block_on(storage.tree.put_directory(vec![
2561 DirEntry::from_cid("HEAD", &info_dir).with_size(0),
2562 DirEntry::from_cid("info", &info_dir).with_link_type(LinkType::Dir),
2563 DirEntry::from_cid("objects", &empty_objects_dir).with_link_type(LinkType::Dir),
2564 DirEntry::from_cid("refs", &refs_dir).with_link_type(LinkType::Dir),
2565 ]))
2566 .unwrap();
2567 let root_cid = storage
2568 .runtime
2569 .block_on(storage.tree.put_directory(vec![
2570 DirEntry::from_cid(".git", &git_dir).with_link_type(LinkType::Dir),
2571 ]))
2572 .unwrap();
2573
2574 let err = storage
2575 .validate_root_contains_direct_refs(&root_cid)
2576 .expect_err("missing ref-tip object should fail validation");
2577 assert!(
2578 err.to_string().contains(&commit_oid.to_hex()),
2579 "validation error should mention missing commit oid: {}",
2580 err
2581 );
2582 }
2583}