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, HashSet};
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::progress::{RepoTreeBuildPhase, RepoTreeBuildProgress};
34use super::refs::{validate_ref_name, Ref};
35use super::{Error, Result};
36
37type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
39
40#[derive(Default)]
41struct RefDirectory {
42 files: BTreeMap<String, String>,
43 dirs: BTreeMap<String, RefDirectory>,
44}
45
46impl RefDirectory {
47 fn insert(&mut self, parts: &[&str], value: String) {
48 let Some((name, rest)) = parts.split_first() else {
49 return;
50 };
51
52 if rest.is_empty() {
53 self.files.insert((*name).to_string(), value);
54 } else {
55 self.dirs
56 .entry((*name).to_string())
57 .or_default()
58 .insert(rest, value);
59 }
60 }
61}
62
63enum RuntimeExecutor {
65 Owned(Runtime),
66 Handle(Handle),
67}
68
69impl RuntimeExecutor {
70 fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
71 match self {
72 RuntimeExecutor::Owned(rt) => rt.block_on(f),
73 RuntimeExecutor::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
74 }
75 }
76}
77
78pub enum LocalStore {
80 Fs(FsBlobStore),
81 #[cfg(feature = "lmdb")]
82 Lmdb(LmdbBlobStore),
83}
84
85impl LocalStore {
86 pub(crate) fn new_for_backend<P: AsRef<Path>>(
87 path: P,
88 backend: StorageBackend,
89 max_bytes: u64,
90 ) -> std::result::Result<Self, StoreError> {
91 let path = path.as_ref();
92 #[cfg(feature = "lmdb")]
93 {
94 return Self::new_for_backend_with_openers(
95 path,
96 backend,
97 max_bytes,
98 Self::open_fs_store,
99 Self::open_lmdb_store,
100 );
101 }
102
103 #[cfg(not(feature = "lmdb"))]
104 match backend {
105 StorageBackend::Fs => Self::open_fs_store(path, max_bytes),
106 #[cfg(not(feature = "lmdb"))]
107 StorageBackend::Lmdb => {
108 warn!(
109 "LMDB backend requested but lmdb feature not enabled, using filesystem storage"
110 );
111 Self::open_fs_store(path, max_bytes)
112 }
113 }
114 }
115
116 fn open_fs_store(path: &Path, max_bytes: u64) -> std::result::Result<Self, StoreError> {
117 if max_bytes > 0 {
118 Ok(LocalStore::Fs(FsBlobStore::with_max_bytes(
119 path, max_bytes,
120 )?))
121 } else {
122 Ok(LocalStore::Fs(FsBlobStore::new(path)?))
123 }
124 }
125
126 #[cfg(feature = "lmdb")]
127 fn open_lmdb_store(path: &Path, max_bytes: u64) -> std::result::Result<Self, StoreError> {
128 if max_bytes > 0 {
129 Ok(LocalStore::Lmdb(LmdbBlobStore::with_max_bytes(
130 path, max_bytes,
131 )?))
132 } else {
133 Ok(LocalStore::Lmdb(LmdbBlobStore::new(path)?))
134 }
135 }
136
137 #[cfg(feature = "lmdb")]
138 fn new_for_backend_with_openers<FS, LMDB>(
139 path: &Path,
140 backend: StorageBackend,
141 max_bytes: u64,
142 fs_open: FS,
143 lmdb_open: LMDB,
144 ) -> std::result::Result<Self, StoreError>
145 where
146 FS: Fn(&Path, u64) -> std::result::Result<Self, StoreError>,
147 LMDB: Fn(&Path, u64) -> std::result::Result<Self, StoreError>,
148 {
149 match backend {
150 StorageBackend::Fs => fs_open(path, max_bytes),
151 StorageBackend::Lmdb => match lmdb_open(path, max_bytes) {
152 Ok(store) => Ok(store),
153 Err(err) if should_fallback_from_lmdb_error(&err) => {
154 warn!(
155 path = %path.display(),
156 "LMDB backend is unsupported in this environment, falling back to filesystem storage"
157 );
158 fs_open(path, max_bytes)
159 }
160 Err(err) => Err(err),
161 },
162 }
163 }
164
165 pub fn new<P: AsRef<Path>>(path: P) -> std::result::Result<Self, StoreError> {
167 Self::new_with_max_bytes(path, 0)
168 }
169
170 pub fn new_with_max_bytes<P: AsRef<Path>>(
172 path: P,
173 max_bytes: u64,
174 ) -> std::result::Result<Self, StoreError> {
175 let config = Config::load_or_default();
176 Self::new_for_backend(path, config.storage.backend, max_bytes)
177 }
178
179 pub fn list(&self) -> std::result::Result<Vec<Hash>, StoreError> {
181 match self {
182 LocalStore::Fs(store) => store.list(),
183 #[cfg(feature = "lmdb")]
184 LocalStore::Lmdb(store) => store.list(),
185 }
186 }
187
188 pub fn get_sync(&self, hash: &Hash) -> std::result::Result<Option<Vec<u8>>, StoreError> {
190 match self {
191 LocalStore::Fs(store) => store.get_sync(hash),
192 #[cfg(feature = "lmdb")]
193 LocalStore::Lmdb(store) => store.get_sync(hash),
194 }
195 }
196}
197
198#[cfg(feature = "lmdb")]
199pub(crate) fn should_fallback_from_lmdb_error(error: &StoreError) -> bool {
200 matches!(
201 error,
202 StoreError::Io(io_error) if io_error.raw_os_error() == Some(libc::ENOSYS)
203 )
204}
205
206#[async_trait::async_trait]
207impl Store for LocalStore {
208 async fn put(&self, hash: Hash, data: Vec<u8>) -> std::result::Result<bool, StoreError> {
209 match self {
210 LocalStore::Fs(store) => store.put(hash, data).await,
211 #[cfg(feature = "lmdb")]
212 LocalStore::Lmdb(store) => store.put(hash, data).await,
213 }
214 }
215
216 async fn get(&self, hash: &Hash) -> std::result::Result<Option<Vec<u8>>, StoreError> {
217 match self {
218 LocalStore::Fs(store) => store.get(hash).await,
219 #[cfg(feature = "lmdb")]
220 LocalStore::Lmdb(store) => store.get(hash).await,
221 }
222 }
223
224 async fn has(&self, hash: &Hash) -> std::result::Result<bool, StoreError> {
225 match self {
226 LocalStore::Fs(store) => store.has(hash).await,
227 #[cfg(feature = "lmdb")]
228 LocalStore::Lmdb(store) => store.has(hash).await,
229 }
230 }
231
232 async fn delete(&self, hash: &Hash) -> std::result::Result<bool, StoreError> {
233 match self {
234 LocalStore::Fs(store) => store.delete(hash).await,
235 #[cfg(feature = "lmdb")]
236 LocalStore::Lmdb(store) => store.delete(hash).await,
237 }
238 }
239
240 fn set_max_bytes(&self, max: u64) {
241 match self {
242 LocalStore::Fs(store) => store.set_max_bytes(max),
243 #[cfg(feature = "lmdb")]
244 LocalStore::Lmdb(store) => store.set_max_bytes(max),
245 }
246 }
247
248 fn max_bytes(&self) -> Option<u64> {
249 match self {
250 LocalStore::Fs(store) => store.max_bytes(),
251 #[cfg(feature = "lmdb")]
252 LocalStore::Lmdb(store) => store.max_bytes(),
253 }
254 }
255
256 async fn stats(&self) -> StoreStats {
257 match self {
258 LocalStore::Fs(store) => match store.stats() {
259 Ok(stats) => StoreStats {
260 count: stats.count as u64,
261 bytes: stats.total_bytes,
262 pinned_count: stats.pinned_count as u64,
263 pinned_bytes: stats.pinned_bytes,
264 },
265 Err(_) => StoreStats::default(),
266 },
267 #[cfg(feature = "lmdb")]
268 LocalStore::Lmdb(store) => match store.stats() {
269 Ok(stats) => StoreStats {
270 count: stats.count as u64,
271 bytes: stats.total_bytes,
272 pinned_count: 0,
273 pinned_bytes: 0,
274 },
275 Err(_) => StoreStats::default(),
276 },
277 }
278 }
279
280 async fn evict_if_needed(&self) -> std::result::Result<u64, StoreError> {
281 match self {
282 LocalStore::Fs(store) => store.evict_if_needed().await,
283 #[cfg(feature = "lmdb")]
284 LocalStore::Lmdb(store) => store.evict_if_needed().await,
285 }
286 }
287}
288
289pub struct GitStorage {
291 store: Arc<LocalStore>,
292 tree: HashTree<LocalStore>,
293 runtime: RuntimeExecutor,
294 objects: std::sync::RwLock<HashMap<String, Vec<u8>>>,
296 refs: std::sync::RwLock<HashMap<String, String>>,
297 pack_files: std::sync::RwLock<BTreeMap<String, Vec<u8>>>,
298 packed_object_ids: std::sync::RwLock<HashSet<String>>,
299 root_cid: std::sync::RwLock<Option<Cid>>,
301}
302
303impl GitStorage {
304 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
306 let config = Config::load_or_default();
307 let max_size_bytes = config
308 .storage
309 .max_size_gb
310 .saturating_mul(1024 * 1024 * 1024);
311 Self::open_with_max_bytes(path, max_size_bytes)
312 }
313
314 pub fn open_with_max_bytes(path: impl AsRef<Path>, max_size_bytes: u64) -> Result<Self> {
316 let config = Config::load_or_default();
317 Self::open_with_backend_and_max_bytes(path, config.storage.backend, max_size_bytes)
318 }
319
320 pub fn open_with_backend_and_max_bytes(
321 path: impl AsRef<Path>,
322 backend: StorageBackend,
323 max_size_bytes: u64,
324 ) -> Result<Self> {
325 let runtime = match Handle::try_current() {
326 Ok(handle) => RuntimeExecutor::Handle(handle),
327 Err(_) => {
328 let rt = Runtime::new()
329 .map_err(|e| Error::StorageError(format!("tokio runtime: {}", e)))?;
330 RuntimeExecutor::Owned(rt)
331 }
332 };
333
334 let store_path = path.as_ref().join("blobs");
335 let store = Arc::new(
336 LocalStore::new_for_backend(&store_path, backend, max_size_bytes)
337 .map_err(|e| Error::StorageError(format!("local store: {}", e)))?,
338 );
339
340 let tree = HashTree::new(HashTreeConfig::new(store.clone()));
342
343 Ok(Self {
344 store,
345 tree,
346 runtime,
347 objects: std::sync::RwLock::new(HashMap::new()),
348 refs: std::sync::RwLock::new(HashMap::new()),
349 pack_files: std::sync::RwLock::new(BTreeMap::new()),
350 packed_object_ids: std::sync::RwLock::new(HashSet::new()),
351 root_cid: std::sync::RwLock::new(None),
352 })
353 }
354
355 pub fn evict_if_needed(&self) -> Result<u64> {
357 self.runtime
358 .block_on(self.store.evict_if_needed())
359 .map_err(|e| Error::StorageError(format!("evict: {}", e)))
360 }
361
362 fn write_object(&self, obj: &GitObject) -> Result<ObjectId> {
364 let oid = obj.id();
365 let key = oid.to_hex();
366
367 let loose = obj.to_loose_format();
368 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
369 encoder.write_all(&loose)?;
370 let compressed = encoder.finish()?;
371
372 let mut objects = self
373 .objects
374 .write()
375 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
376 objects.insert(key, compressed);
377
378 if let Ok(mut root) = self.root_cid.write() {
380 *root = None;
381 }
382
383 Ok(oid)
384 }
385
386 pub fn write_raw_object(&self, obj_type: ObjectType, content: &[u8]) -> Result<ObjectId> {
388 let obj = GitObject::new(obj_type, content.to_vec());
389 self.write_object(&obj)
390 }
391
392 #[allow(dead_code)]
394 fn read_object(&self, oid: &ObjectId) -> Result<GitObject> {
395 let key = oid.to_hex();
396 let objects = self
397 .objects
398 .read()
399 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
400 let compressed = objects
401 .get(&key)
402 .ok_or_else(|| Error::ObjectNotFound(key.clone()))?;
403
404 let mut decoder = ZlibDecoder::new(compressed.as_slice());
405 let mut data = Vec::new();
406 decoder.read_to_end(&mut data)?;
407
408 GitObject::from_loose_format(&data)
409 }
410
411 pub fn write_ref(&self, name: &str, target: &Ref) -> Result<()> {
413 validate_ref_name(name)?;
414
415 let value = match target {
416 Ref::Direct(oid) => oid.to_hex(),
417 Ref::Symbolic(target) => format!("ref: {}", target),
418 };
419
420 let mut refs = self
421 .refs
422 .write()
423 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
424 refs.insert(name.to_string(), value);
425
426 if let Ok(mut root) = self.root_cid.write() {
428 *root = None;
429 }
430
431 Ok(())
432 }
433
434 #[allow(dead_code)]
436 pub fn read_ref(&self, name: &str) -> Result<Option<Ref>> {
437 let refs = self
438 .refs
439 .read()
440 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
441
442 match refs.get(name) {
443 Some(value) => {
444 if let Some(target) = value.strip_prefix("ref: ") {
445 Ok(Some(Ref::Symbolic(target.to_string())))
446 } else {
447 let oid = ObjectId::from_hex(value)
448 .ok_or_else(|| Error::StorageError(format!("invalid ref: {}", value)))?;
449 Ok(Some(Ref::Direct(oid)))
450 }
451 }
452 None => Ok(None),
453 }
454 }
455
456 #[allow(dead_code)]
458 pub fn list_refs(&self) -> Result<HashMap<String, String>> {
459 let refs = self
460 .refs
461 .read()
462 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
463 Ok(refs.clone())
464 }
465
466 pub fn delete_ref(&self, name: &str) -> Result<bool> {
468 let mut refs = self
469 .refs
470 .write()
471 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
472 let existed = refs.remove(name).is_some();
473
474 if let Ok(mut root) = self.root_cid.write() {
476 *root = None;
477 }
478
479 Ok(existed)
480 }
481
482 pub fn import_compressed_object(&self, oid: &str, compressed_data: Vec<u8>) -> Result<()> {
485 let mut objects = self
486 .objects
487 .write()
488 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
489 objects.insert(oid.to_string(), compressed_data);
490
491 if let Ok(mut root) = self.root_cid.write() {
493 *root = None;
494 }
495
496 Ok(())
497 }
498
499 pub fn import_ref(&self, name: &str, value: &str) -> Result<()> {
501 let mut refs = self
502 .refs
503 .write()
504 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
505 refs.insert(name.to_string(), value.to_string());
506
507 if let Ok(mut root) = self.root_cid.write() {
509 *root = None;
510 }
511
512 Ok(())
513 }
514
515 pub fn set_pack_files(&self, files: BTreeMap<String, Vec<u8>>) -> Result<()> {
516 self.set_pack_checkpoint_files(files, HashSet::new())
517 }
518
519 pub fn set_pack_checkpoint_files(
520 &self,
521 files: BTreeMap<String, Vec<u8>>,
522 covered_objects: HashSet<String>,
523 ) -> Result<()> {
524 let mut pack_files = self
525 .pack_files
526 .write()
527 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
528 *pack_files = files;
529 let mut packed_object_ids = self
530 .packed_object_ids
531 .write()
532 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
533 *packed_object_ids = covered_objects;
534
535 if let Ok(mut root) = self.root_cid.write() {
536 *root = None;
537 }
538
539 Ok(())
540 }
541
542 #[cfg(test)]
544 pub fn has_ref(&self, name: &str) -> Result<bool> {
545 let refs = self
546 .refs
547 .read()
548 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
549 Ok(refs.contains_key(name))
550 }
551
552 #[cfg(test)]
554 pub fn object_count(&self) -> Result<usize> {
555 let objects = self
556 .objects
557 .read()
558 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
559 Ok(objects.len())
560 }
561
562 #[allow(dead_code)]
564 pub fn get_root_cid(&self) -> Result<Option<Cid>> {
565 let root = self
566 .root_cid
567 .read()
568 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
569 Ok(root.clone())
570 }
571
572 #[allow(dead_code)]
574 pub fn default_branch(&self) -> Result<Option<String>> {
575 let refs = self
576 .refs
577 .read()
578 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
579
580 if let Some(head) = refs.get("HEAD") {
581 if let Some(target) = head.strip_prefix("ref: ") {
582 return Ok(Some(target.to_string()));
583 }
584 }
585 Ok(None)
586 }
587
588 fn get_commit_tree(
590 &self,
591 commit_oid: &str,
592 objects: &HashMap<String, Vec<u8>>,
593 ) -> Option<String> {
594 let compressed = objects.get(commit_oid)?;
595
596 let mut decoder = ZlibDecoder::new(&compressed[..]);
598 let mut decompressed = Vec::new();
599 decoder.read_to_end(&mut decompressed).ok()?;
600
601 let null_pos = decompressed.iter().position(|&b| b == 0)?;
603 let content = &decompressed[null_pos + 1..];
604
605 let content_str = std::str::from_utf8(content).ok()?;
607 let first_line = content_str.lines().next()?;
608 first_line
609 .strip_prefix("tree ")
610 .map(|tree_hash| tree_hash.to_string())
611 }
612
613 fn get_object_content(
615 &self,
616 oid: &str,
617 objects: &HashMap<String, Vec<u8>>,
618 ) -> Option<(ObjectType, Vec<u8>)> {
619 let compressed = objects.get(oid)?;
620 Self::parse_compressed_object(compressed)
621 }
622
623 fn parse_compressed_object(compressed: &[u8]) -> Option<(ObjectType, Vec<u8>)> {
624 let mut decoder = ZlibDecoder::new(compressed);
625 let mut decompressed = Vec::new();
626 decoder.read_to_end(&mut decompressed).ok()?;
627
628 let null_pos = decompressed.iter().position(|&b| b == 0)?;
630 let header = std::str::from_utf8(&decompressed[..null_pos]).ok()?;
631 let obj_type = if header.starts_with("blob") {
632 ObjectType::Blob
633 } else if header.starts_with("tree") {
634 ObjectType::Tree
635 } else if header.starts_with("commit") {
636 ObjectType::Commit
637 } else if header.starts_with("tag") {
638 ObjectType::Tag
639 } else {
640 return None;
641 };
642 let content = decompressed[null_pos + 1..].to_vec();
643 Some((obj_type, content))
644 }
645
646 async fn load_base_compressed_object<S: Store>(
647 &self,
648 oid: &str,
649 base_tree: &HashTree<S>,
650 base_objects_cid: &Cid,
651 ) -> Result<Option<Vec<u8>>> {
652 if oid.len() != 40 {
653 return Err(Error::ObjectNotFound(oid.to_string()));
654 }
655
656 let path = format!("{}/{}", &oid[..2], &oid[2..]);
657 let Some(object_cid) = base_tree
658 .resolve_path(base_objects_cid, &path)
659 .await
660 .map_err(|e| Error::StorageError(format!("resolve {} in base objects: {}", oid, e)))?
661 else {
662 return Ok(None);
663 };
664
665 base_tree
666 .get(&object_cid, None)
667 .await
668 .map_err(|e| Error::StorageError(format!("read {} from base objects: {}", oid, e)))
669 }
670
671 async fn get_object_content_from_base<S: Store>(
672 &self,
673 oid: &str,
674 base_tree: &HashTree<S>,
675 base_objects_cid: &Cid,
676 ) -> Result<Option<(ObjectType, Vec<u8>)>> {
677 let Some(compressed) = self
678 .load_base_compressed_object(oid, base_tree, base_objects_cid)
679 .await?
680 else {
681 return Ok(None);
682 };
683
684 Ok(Self::parse_compressed_object(&compressed))
685 }
686
687 async fn get_tree_entries_from_sources<S: Store>(
688 &self,
689 tree_oid: &str,
690 objects: &HashMap<String, Vec<u8>>,
691 base_tree: Option<&HashTree<S>>,
692 base_objects_cid: Option<&Cid>,
693 ) -> Result<Vec<super::object::TreeEntry>> {
694 let object = match self.get_object_content(tree_oid, objects) {
695 Some(object) => Some(object),
696 None => {
697 if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
698 self.get_object_content_from_base(tree_oid, base_tree, base_objects_cid)
699 .await?
700 } else {
701 None
702 }
703 }
704 }
705 .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
706
707 if object.0 != ObjectType::Tree {
708 return Err(Error::InvalidObjectType(format!(
709 "expected tree, got {:?}",
710 object.0
711 )));
712 }
713
714 parse_tree(&object.1)
715 }
716
717 fn tree_entry_to_dir_entry(entry: &hashtree_core::TreeEntry) -> DirEntry {
718 DirEntry::from_cid(
719 &entry.name,
720 &Cid {
721 hash: entry.hash,
722 key: entry.key,
723 },
724 )
725 .with_size(entry.size)
726 .with_link_type(entry.link_type)
727 }
728
729 async fn import_object_from_base<S: Store>(
730 &self,
731 oid: &str,
732 objects: &mut HashMap<String, Vec<u8>>,
733 base_tree: &HashTree<S>,
734 base_objects_cid: &Cid,
735 ) -> Result<bool> {
736 if objects.contains_key(oid) {
737 return Ok(true);
738 }
739 let Some(compressed) = self
740 .load_base_compressed_object(oid, base_tree, base_objects_cid)
741 .await?
742 else {
743 return Ok(false);
744 };
745
746 objects.insert(oid.to_string(), compressed);
747 Ok(true)
748 }
749
750 fn seed_missing_object_from_base_boxed<'a, S: Store + 'a>(
751 &'a self,
752 oid: &'a str,
753 objects: &'a mut HashMap<String, Vec<u8>>,
754 base_tree: &'a HashTree<S>,
755 base_objects_cid: &'a Cid,
756 ) -> BoxFuture<'a, Result<bool>> {
757 Box::pin(self.import_object_from_base(oid, objects, base_tree, base_objects_cid))
758 }
759
760 fn peel_tag_target(&self, oid: &str, objects: &HashMap<String, Vec<u8>>) -> Option<String> {
761 let (obj_type, content) = self.get_object_content(oid, objects)?;
762 if obj_type != ObjectType::Tag {
763 return Some(oid.to_string());
764 }
765
766 let target = std::str::from_utf8(&content)
767 .ok()?
768 .lines()
769 .find_map(|line| line.strip_prefix("object "))
770 .map(str::trim)?
771 .to_string();
772
773 match self.get_object_content(&target, objects)?.0 {
774 ObjectType::Tag => self.peel_tag_target(&target, objects),
775 _ => Some(target),
776 }
777 }
778
779 fn build_info_refs_content(
780 &self,
781 refs: &HashMap<String, String>,
782 objects: &HashMap<String, Vec<u8>>,
783 ) -> String {
784 let mut lines = Vec::new();
785
786 for (name, value) in refs {
787 if name == "HEAD" {
788 continue;
789 }
790
791 let oid = value.trim().to_string();
792 lines.push((name.clone(), oid.clone()));
793
794 if name.starts_with("refs/tags/") {
795 if let Some(peeled) = self.peel_tag_target(&oid, objects) {
796 if peeled != oid {
797 lines.push((format!("{}^{{}}", name), peeled));
798 }
799 }
800 }
801 }
802
803 lines.sort_by(|a, b| a.0.cmp(&b.0));
804
805 let mut content = String::new();
806 for (name, oid) in lines {
807 content.push_str(&oid);
808 content.push('\t');
809 content.push_str(&name);
810 content.push('\n');
811 }
812 content
813 }
814
815 async fn build_info_dir(
816 &self,
817 refs: &HashMap<String, String>,
818 objects: &HashMap<String, Vec<u8>>,
819 ) -> Result<Cid> {
820 let info_refs = self.build_info_refs_content(refs, objects);
821 let (info_refs_cid, info_refs_size) = self
822 .tree
823 .put(info_refs.as_bytes())
824 .await
825 .map_err(|e| Error::StorageError(format!("put info/refs: {}", e)))?;
826
827 self.tree
828 .put_directory(vec![
829 DirEntry::from_cid("refs", &info_refs_cid).with_size(info_refs_size)
830 ])
831 .await
832 .map_err(|e| Error::StorageError(format!("put info dir: {}", e)))
833 }
834
835 pub fn build_tree(&self) -> Result<Cid> {
837 self.build_tree_with_base_objects::<LocalStore>(None, None, None)
838 }
839
840 pub fn build_tree_with_progress(&self, progress: &RepoTreeBuildProgress) -> Result<Cid> {
841 self.build_tree_with_base_objects_internal::<LocalStore>(None, None, None, Some(progress))
842 }
843
844 fn is_safe_pack_name(name: &str) -> bool {
845 name.len() == "pack-".len() + 40 + ".pack".len()
846 && name.starts_with("pack-")
847 && name.ends_with(".pack")
848 && name["pack-".len()..name.len() - ".pack".len()]
849 .bytes()
850 .all(|byte| byte.is_ascii_hexdigit())
851 }
852
853 fn root_has_git_pack_checkpoint(&self, root_cid: &Cid) -> Result<bool> {
854 let Some(info_packs_cid) = self
855 .runtime
856 .block_on(self.tree.resolve_path(root_cid, ".git/objects/info/packs"))
857 .map_err(|e| Error::StorageError(format!("resolve .git/objects/info/packs: {}", e)))?
858 else {
859 return Ok(false);
860 };
861
862 let Some(info_packs_bytes) = self
863 .runtime
864 .block_on(self.tree.get(&info_packs_cid, None))
865 .map_err(|e| Error::StorageError(format!("read .git/objects/info/packs: {}", e)))?
866 else {
867 return Ok(false);
868 };
869
870 let info_packs = String::from_utf8_lossy(&info_packs_bytes);
871 for line in info_packs.lines().map(str::trim) {
872 let Some(pack_name) = line.strip_prefix("P ") else {
873 continue;
874 };
875 if !Self::is_safe_pack_name(pack_name) {
876 continue;
877 }
878
879 let idx_name = format!("{}.idx", pack_name.trim_end_matches(".pack"));
880 let pack_path = format!(".git/objects/pack/{pack_name}");
881 let idx_path = format!(".git/objects/pack/{idx_name}");
882 let pack_exists = self
883 .runtime
884 .block_on(self.tree.resolve_path(root_cid, &pack_path))
885 .map_err(|e| Error::StorageError(format!("resolve {}: {}", pack_path, e)))?
886 .is_some();
887 let idx_exists = self
888 .runtime
889 .block_on(self.tree.resolve_path(root_cid, &idx_path))
890 .map_err(|e| Error::StorageError(format!("resolve {}: {}", idx_path, e)))?
891 .is_some();
892 if pack_exists && idx_exists {
893 return Ok(true);
894 }
895 }
896
897 Ok(false)
898 }
899
900 pub fn validate_root_contains_direct_refs(&self, root_cid: &Cid) -> Result<()> {
901 let direct_refs: Vec<String> = {
902 let refs = self
903 .refs
904 .read()
905 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
906 refs.values()
907 .filter(|value| {
908 !value.starts_with("ref: ")
909 && value.len() == 40
910 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
911 })
912 .cloned()
913 .collect()
914 };
915
916 if direct_refs.is_empty() {
917 return Ok(());
918 }
919
920 let objects_dir = self
921 .runtime
922 .block_on(self.tree.resolve_path(root_cid, ".git/objects"))
923 .map_err(|e| Error::StorageError(format!("resolve .git/objects: {}", e)))?;
924 if objects_dir.is_none() {
925 return Err(Error::StorageError(
926 "built root is missing .git/objects".to_string(),
927 ));
928 }
929
930 let pack_checkpoint_available = self.root_has_git_pack_checkpoint(root_cid)?;
931 for oid in direct_refs {
932 let object_path = format!(".git/objects/{}/{}", &oid[..2], &oid[2..]);
933 let object_cid = self
934 .runtime
935 .block_on(self.tree.resolve_path(root_cid, &object_path))
936 .map_err(|e| Error::StorageError(format!("resolve {}: {}", object_path, e)))?;
937 if object_cid.is_none() && !pack_checkpoint_available {
938 return Err(Error::ObjectNotFound(oid));
939 }
940 }
941
942 Ok(())
943 }
944
945 pub fn build_tree_with_base_objects<S: Store>(
946 &self,
947 base_tree: Option<&HashTree<S>>,
948 base_root: Option<&Cid>,
949 base_commit_sha: Option<&str>,
950 ) -> Result<Cid> {
951 self.build_tree_with_base_objects_internal(base_tree, base_root, base_commit_sha, None)
952 }
953
954 pub fn build_tree_with_base_objects_with_progress<S: Store>(
955 &self,
956 base_tree: Option<&HashTree<S>>,
957 base_root: Option<&Cid>,
958 base_commit_sha: Option<&str>,
959 progress: &RepoTreeBuildProgress,
960 ) -> Result<Cid> {
961 self.build_tree_with_base_objects_internal(
962 base_tree,
963 base_root,
964 base_commit_sha,
965 Some(progress),
966 )
967 }
968
969 fn build_tree_with_base_objects_internal<S: Store>(
970 &self,
971 base_tree: Option<&HashTree<S>>,
972 base_root: Option<&Cid>,
973 base_commit_sha: Option<&str>,
974 progress: Option<&RepoTreeBuildProgress>,
975 ) -> Result<Cid> {
976 if let Ok(root) = self.root_cid.read() {
978 if let Some(ref cid) = *root {
979 if let Some(progress) = progress {
980 progress.mark_done();
981 }
982 return Ok(cid.clone());
983 }
984 }
985
986 if let Err(err) = self.evict_if_needed() {
987 debug!("pre-build eviction skipped: {}", err);
988 }
989
990 let objects = self
991 .objects
992 .read()
993 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
994 let refs = self
995 .refs
996 .read()
997 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
998
999 let (default_branch, commit_sha) = if let Some(head) = refs.get("HEAD") {
1001 let branch = head.strip_prefix("ref: ").map(String::from);
1002 let sha = branch.as_ref().and_then(|b| refs.get(b)).cloned();
1003 (branch, sha)
1004 } else {
1005 let mut branch_info: Option<(String, String)> = None;
1007 for (ref_name, sha) in refs.iter() {
1008 if ref_name.starts_with("refs/heads/") {
1009 branch_info = Some((ref_name.clone(), sha.clone()));
1010 break;
1011 }
1012 }
1013 match branch_info {
1014 Some((branch, sha)) => (Some(branch), Some(sha)),
1015 None => (None, None),
1016 }
1017 };
1018
1019 let tree_sha = commit_sha
1021 .as_ref()
1022 .and_then(|sha| self.get_commit_tree(sha, &objects));
1023
1024 let mut objects_clone = objects.clone();
1026
1027 let base_objects_cid = if let (Some(base_tree), Some(base_root)) = (base_tree, base_root) {
1028 self.runtime
1029 .block_on(base_tree.resolve_path(base_root, ".git/objects"))
1030 .map_err(|e| Error::StorageError(format!("resolve base .git/objects: {}", e)))?
1031 } else {
1032 None
1033 };
1034
1035 let base_tree_sha =
1036 if let (Some(base_tree), Some(base_objects_cid), Some(base_commit_sha)) =
1037 (base_tree, base_objects_cid.as_ref(), base_commit_sha)
1038 {
1039 self.runtime.block_on(async {
1040 let Some((obj_type, content)) = self
1041 .get_object_content_from_base(base_commit_sha, base_tree, base_objects_cid)
1042 .await?
1043 else {
1044 return Ok(None);
1045 };
1046
1047 if obj_type != ObjectType::Commit {
1048 return Err(Error::InvalidObjectType(format!(
1049 "expected commit, got {:?}",
1050 obj_type
1051 )));
1052 }
1053
1054 let content = std::str::from_utf8(&content).map_err(|_| {
1055 Error::InvalidObjectFormat("commit: invalid utf-8".to_string())
1056 })?;
1057 Ok::<Option<String>, Error>(
1058 content
1059 .lines()
1060 .find_map(|line| line.strip_prefix("tree "))
1061 .map(str::trim)
1062 .map(str::to_string),
1063 )
1064 })?
1065 } else {
1066 None
1067 };
1068
1069 let base_root_entries = if let (Some(base_tree), Some(base_root)) = (base_tree, base_root) {
1070 Some(
1071 self.runtime
1072 .block_on(base_tree.list_directory(base_root))
1073 .map_err(|e| Error::StorageError(format!("list base root: {}", e)))?,
1074 )
1075 } else {
1076 None
1077 };
1078
1079 let root_cid = loop {
1080 let build_result = self.runtime.block_on(async {
1081 let objects_cid = self
1083 .build_objects_dir_with_base(
1084 &objects_clone,
1085 base_tree,
1086 base_objects_cid.as_ref(),
1087 progress,
1088 )
1089 .await?;
1090
1091 if let Some(progress) = progress {
1093 progress.start_phase(RepoTreeBuildPhase::Refs, Some(refs.len()));
1094 }
1095 let refs_cid = self.build_refs_dir(&refs, progress).await?;
1096
1097 if let Some(progress) = progress {
1099 progress.start_phase(RepoTreeBuildPhase::Info, Some(1));
1100 }
1101 let info_cid = self.build_info_dir(&refs, &objects_clone).await?;
1102 if let Some(progress) = progress {
1103 progress.increment_processed();
1104 }
1105
1106 if let Some(progress) = progress {
1109 progress.start_phase(RepoTreeBuildPhase::Head, Some(1));
1110 }
1111 let head_content = refs.get("HEAD")
1112 .map(|h| if h.ends_with('\n') { h.clone() } else { format!("{}\n", h) })
1113 .or_else(|| default_branch.as_ref().map(|b| format!("ref: {}\n", b)))
1114 .unwrap_or_else(|| "ref: refs/heads/main\n".to_string());
1115 debug!("HEAD content: {:?}", head_content);
1116 let (head_cid, head_size) = self.tree.put(head_content.as_bytes()).await
1117 .map_err(|e| Error::StorageError(format!("put HEAD: {}", e)))?;
1118 debug!("HEAD hash: {}", hex::encode(head_cid.hash));
1119 if let Some(progress) = progress {
1120 progress.increment_processed();
1121 }
1122
1123 let mut git_entries = vec![
1125 DirEntry::from_cid("HEAD", &head_cid).with_size(head_size),
1126 DirEntry::from_cid("info", &info_cid).with_link_type(LinkType::Dir),
1127 DirEntry::from_cid("objects", &objects_cid).with_link_type(LinkType::Dir),
1128 DirEntry::from_cid("refs", &refs_cid).with_link_type(LinkType::Dir),
1129 ];
1130
1131 if let Some(ref branch) = default_branch {
1133 let config = format!(
1134 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n[init]\n\tdefaultBranch = {}\n",
1135 branch.trim_start_matches("refs/heads/")
1136 );
1137 let (config_cid, config_size) = self.tree.put(config.as_bytes()).await
1138 .map_err(|e| Error::StorageError(format!("put config: {}", e)))?;
1139 git_entries.push(DirEntry::from_cid("config", &config_cid).with_size(config_size));
1140 }
1141
1142 if let Some(ref tree_oid) = tree_sha {
1144 if let Some(progress) = progress {
1145 progress.start_phase(RepoTreeBuildPhase::Index, None);
1146 }
1147 let index_result = if let (Some(base_tree), Some(base_objects_cid), Some(base_tree_sha)) =
1148 (base_tree, base_objects_cid.as_ref(), base_tree_sha.as_deref())
1149 {
1150 self.build_index_file_with_base(
1151 tree_oid,
1152 &objects_clone,
1153 base_tree,
1154 base_objects_cid,
1155 base_tree_sha,
1156 base_root_entries.as_ref(),
1157 progress,
1158 )
1159 .await
1160 } else {
1161 self.build_index_file(tree_oid, &objects_clone, progress)
1162 };
1163 match index_result {
1164 Ok(index_data) => {
1165 let (index_cid, index_size) = self.tree.put(&index_data).await
1166 .map_err(|e| Error::StorageError(format!("put index: {}", e)))?;
1167 git_entries.push(DirEntry::from_cid("index", &index_cid).with_size(index_size));
1168 info!("Added git index file ({} bytes)", index_data.len());
1169 }
1170 Err(e) => {
1171 debug!("Failed to build git index file: {} - continuing without index", e);
1172 }
1173 }
1174 }
1175
1176 if let Some(progress) = progress {
1177 progress.start_phase(RepoTreeBuildPhase::GitDir, Some(1));
1178 }
1179 let git_cid = self.tree.put_directory(git_entries).await
1180 .map_err(|e| Error::StorageError(format!("put .git: {}", e)))?;
1181 if let Some(progress) = progress {
1182 progress.increment_processed();
1183 }
1184
1185 let mut root_entries = vec![DirEntry::from_cid(".git", &git_cid).with_link_type(LinkType::Dir)];
1188
1189 if let Some(ref tree_oid) = tree_sha {
1191 if let Some(progress) = progress {
1192 progress.start_phase(RepoTreeBuildPhase::WorkingTree, None);
1193 }
1194 let working_tree_entries = if let (Some(base_tree), Some(base_objects_cid), Some(base_tree_sha)) =
1195 (base_tree, base_objects_cid.as_ref(), base_tree_sha.as_deref())
1196 {
1197 self.build_working_tree_entries_with_base(
1198 tree_oid,
1199 &objects_clone,
1200 base_tree,
1201 base_objects_cid,
1202 base_tree_sha,
1203 base_root_entries.as_ref(),
1204 progress,
1205 )
1206 .await?
1207 } else {
1208 self.build_working_tree_entries(tree_oid, &objects_clone, progress).await?
1209 };
1210 root_entries.extend(working_tree_entries);
1211 info!("Added {} working tree entries to root", root_entries.len() - 1);
1212 }
1213
1214 root_entries.sort_by(|a, b| a.name.cmp(&b.name));
1216
1217 if let Some(progress) = progress {
1218 progress.start_phase(RepoTreeBuildPhase::Root, Some(1));
1219 }
1220 let root_cid = self.tree.put_directory(root_entries).await
1221 .map_err(|e| Error::StorageError(format!("put root: {}", e)))?;
1222 if let Some(progress) = progress {
1223 progress.increment_processed();
1224 }
1225
1226 info!("Built hashtree root: {} (encrypted: {}) (.git dir: {})",
1227 hex::encode(root_cid.hash),
1228 root_cid.key.is_some(),
1229 hex::encode(git_cid.hash));
1230
1231 Ok::<Cid, Error>(root_cid)
1232 });
1233
1234 match build_result {
1235 Ok(root_cid) => break root_cid,
1236 Err(Error::ObjectNotFound(oid))
1237 if base_tree.is_some() && base_objects_cid.is_some() =>
1238 {
1239 let imported =
1240 self.runtime
1241 .block_on(self.seed_missing_object_from_base_boxed(
1242 &oid,
1243 &mut objects_clone,
1244 base_tree.expect("checked is_some"),
1245 base_objects_cid.as_ref().expect("checked is_some"),
1246 ))?;
1247 if imported {
1248 continue;
1249 }
1250 return Err(Error::ObjectNotFound(oid).into());
1251 }
1252 Err(err) => return Err(err.into()),
1253 }
1254 };
1255
1256 if let Ok(mut root) = self.root_cid.write() {
1258 *root = Some(root_cid.clone());
1259 }
1260 if let Some(progress) = progress {
1261 progress.mark_done();
1262 }
1263
1264 Ok(root_cid)
1265 }
1266
1267 async fn build_working_tree_entries(
1269 &self,
1270 tree_oid: &str,
1271 objects: &HashMap<String, Vec<u8>>,
1272 progress: Option<&RepoTreeBuildProgress>,
1273 ) -> Result<Vec<DirEntry>> {
1274 let mut entries = Vec::new();
1275
1276 let (obj_type, content) = self
1278 .get_object_content(tree_oid, objects)
1279 .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
1280
1281 if obj_type != ObjectType::Tree {
1282 return Err(Error::InvalidObjectType(format!(
1283 "expected tree, got {:?}",
1284 obj_type
1285 )));
1286 }
1287
1288 let tree_entries = parse_tree(&content)?;
1290
1291 for entry in tree_entries {
1292 let oid_hex = entry.oid.to_hex();
1293
1294 if entry.is_tree() {
1295 let sub_entries = self
1297 .build_working_tree_entries_boxed(&oid_hex, objects, progress)
1298 .await?;
1299
1300 let dir_cid =
1302 self.tree.put_directory(sub_entries).await.map_err(|e| {
1303 Error::StorageError(format!("put dir {}: {}", entry.name, e))
1304 })?;
1305
1306 entries
1308 .push(DirEntry::from_cid(&entry.name, &dir_cid).with_link_type(LinkType::Dir));
1309 if let Some(progress) = progress {
1310 progress.record_working_dir(false);
1311 }
1312 } else {
1313 if let Some((ObjectType::Blob, blob_content)) =
1315 self.get_object_content(&oid_hex, objects)
1316 {
1317 let (cid, size) = self.tree.put(&blob_content).await.map_err(|e| {
1319 Error::StorageError(format!("put blob {}: {}", entry.name, e))
1320 })?;
1321
1322 entries.push(DirEntry::from_cid(&entry.name, &cid).with_size(size));
1324 if let Some(progress) = progress {
1325 progress.record_working_file(false);
1326 }
1327 }
1328 }
1329 }
1330
1331 entries.sort_by(|a, b| a.name.cmp(&b.name));
1333
1334 Ok(entries)
1335 }
1336
1337 fn build_working_tree_entries_boxed<'a>(
1339 &'a self,
1340 tree_oid: &'a str,
1341 objects: &'a HashMap<String, Vec<u8>>,
1342 progress: Option<&'a RepoTreeBuildProgress>,
1343 ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1344 Box::pin(self.build_working_tree_entries(tree_oid, objects, progress))
1345 }
1346
1347 fn build_working_tree_entries_with_base_recursive_boxed<'a, S: Store + 'a>(
1348 &'a self,
1349 tree_oid: &'a str,
1350 objects: &'a HashMap<String, Vec<u8>>,
1351 base_tree: &'a HashTree<S>,
1352 base_objects_cid: &'a Cid,
1353 old_tree_oid: Option<&'a str>,
1354 old_dir_entries: Option<&'a Vec<hashtree_core::TreeEntry>>,
1355 progress: Option<&'a RepoTreeBuildProgress>,
1356 ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1357 Box::pin(async move {
1358 let tree_entries = self
1359 .get_tree_entries_from_sources(
1360 tree_oid,
1361 objects,
1362 Some(base_tree),
1363 Some(base_objects_cid),
1364 )
1365 .await?;
1366 let old_tree_entries = if let Some(old_tree_oid) = old_tree_oid {
1367 Some(
1368 self.get_tree_entries_from_sources(
1369 old_tree_oid,
1370 objects,
1371 Some(base_tree),
1372 Some(base_objects_cid),
1373 )
1374 .await?,
1375 )
1376 } else {
1377 None
1378 };
1379
1380 let mut old_tree_entry_map = old_tree_entries
1381 .unwrap_or_default()
1382 .into_iter()
1383 .map(|entry| (entry.name.clone(), entry))
1384 .collect::<HashMap<_, _>>();
1385 let mut old_dir_entry_map = old_dir_entries
1386 .cloned()
1387 .unwrap_or_default()
1388 .into_iter()
1389 .map(|entry| (entry.name.clone(), entry))
1390 .collect::<HashMap<_, _>>();
1391
1392 let mut entries = Vec::new();
1393 for entry in tree_entries {
1394 let oid_hex = entry.oid.to_hex();
1395 let old_tree_entry = old_tree_entry_map.remove(&entry.name);
1396 let old_dir_entry = old_dir_entry_map.remove(&entry.name);
1397
1398 if entry.is_tree() {
1399 if let (Some(old_tree_entry), Some(old_dir_entry)) =
1400 (old_tree_entry.as_ref(), old_dir_entry.as_ref())
1401 {
1402 if old_tree_entry.is_tree()
1403 && old_tree_entry.oid == entry.oid
1404 && old_dir_entry.link_type == LinkType::Dir
1405 {
1406 entries.push(Self::tree_entry_to_dir_entry(old_dir_entry));
1407 if let Some(progress) = progress {
1408 progress.record_working_dir(true);
1409 }
1410 continue;
1411 }
1412 }
1413
1414 let old_subtree_oid = old_tree_entry
1415 .as_ref()
1416 .filter(|old| old.is_tree())
1417 .map(|old| old.oid.to_hex());
1418 let old_subdir_entries = if let Some(old_dir_entry) = old_dir_entry.as_ref() {
1419 if old_dir_entry.link_type == LinkType::Dir {
1420 Some(
1421 base_tree
1422 .list_directory(&Cid {
1423 hash: old_dir_entry.hash,
1424 key: old_dir_entry.key,
1425 })
1426 .await
1427 .map_err(|e| {
1428 Error::StorageError(format!(
1429 "list base working dir {}: {}",
1430 entry.name, e
1431 ))
1432 })?,
1433 )
1434 } else {
1435 None
1436 }
1437 } else {
1438 None
1439 };
1440
1441 let sub_entries = self
1442 .build_working_tree_entries_with_base_recursive_boxed(
1443 &oid_hex,
1444 objects,
1445 base_tree,
1446 base_objects_cid,
1447 old_subtree_oid.as_deref(),
1448 old_subdir_entries.as_ref(),
1449 progress,
1450 )
1451 .await?;
1452 let dir_cid = self.tree.put_directory(sub_entries).await.map_err(|e| {
1453 Error::StorageError(format!("put dir {}: {}", entry.name, e))
1454 })?;
1455 entries.push(
1456 DirEntry::from_cid(&entry.name, &dir_cid).with_link_type(LinkType::Dir),
1457 );
1458 if let Some(progress) = progress {
1459 progress.record_working_dir(false);
1460 }
1461 continue;
1462 }
1463
1464 if let (Some(old_tree_entry), Some(old_dir_entry)) =
1465 (old_tree_entry.as_ref(), old_dir_entry.as_ref())
1466 {
1467 if !old_tree_entry.is_tree()
1468 && old_tree_entry.oid == entry.oid
1469 && old_dir_entry.link_type != LinkType::Dir
1470 {
1471 entries.push(Self::tree_entry_to_dir_entry(old_dir_entry));
1472 if let Some(progress) = progress {
1473 progress.record_working_file(true);
1474 }
1475 continue;
1476 }
1477 }
1478
1479 let blob_content = match self.get_object_content(&oid_hex, objects) {
1480 Some((ObjectType::Blob, blob_content)) => blob_content,
1481 Some((obj_type, _)) => {
1482 return Err(Error::InvalidObjectType(format!(
1483 "expected blob, got {:?}",
1484 obj_type
1485 )));
1486 }
1487 None => match self
1488 .get_object_content_from_base(&oid_hex, base_tree, base_objects_cid)
1489 .await?
1490 {
1491 Some((ObjectType::Blob, blob_content)) => blob_content,
1492 Some((obj_type, _)) => {
1493 return Err(Error::InvalidObjectType(format!(
1494 "expected blob, got {:?}",
1495 obj_type
1496 )));
1497 }
1498 None => return Err(Error::ObjectNotFound(oid_hex)),
1499 },
1500 };
1501
1502 let (cid, size) =
1503 self.tree.put(&blob_content).await.map_err(|e| {
1504 Error::StorageError(format!("put blob {}: {}", entry.name, e))
1505 })?;
1506 entries.push(DirEntry::from_cid(&entry.name, &cid).with_size(size));
1507 if let Some(progress) = progress {
1508 progress.record_working_file(false);
1509 }
1510 }
1511
1512 entries.sort_by(|a, b| a.name.cmp(&b.name));
1513 Ok(entries)
1514 })
1515 }
1516
1517 async fn build_working_tree_entries_with_base<S: Store>(
1518 &self,
1519 tree_oid: &str,
1520 objects: &HashMap<String, Vec<u8>>,
1521 base_tree: &HashTree<S>,
1522 base_objects_cid: &Cid,
1523 base_tree_oid: &str,
1524 base_root_entries: Option<&Vec<hashtree_core::TreeEntry>>,
1525 progress: Option<&RepoTreeBuildProgress>,
1526 ) -> Result<Vec<DirEntry>> {
1527 let root_entries = base_root_entries.map(|entries| {
1528 entries
1529 .iter()
1530 .filter(|entry| entry.name != ".git")
1531 .cloned()
1532 .collect::<Vec<_>>()
1533 });
1534
1535 self.build_working_tree_entries_with_base_recursive_boxed(
1536 tree_oid,
1537 objects,
1538 base_tree,
1539 base_objects_cid,
1540 Some(base_tree_oid),
1541 root_entries.as_ref(),
1542 progress,
1543 )
1544 .await
1545 }
1546
1547 async fn build_objects_prefix_dir(
1548 &self,
1549 prefix: &str,
1550 old_entries: Option<Vec<hashtree_core::TreeEntry>>,
1551 new_objects: &[(String, Vec<u8>)],
1552 progress: Option<&RepoTreeBuildProgress>,
1553 ) -> Result<Cid> {
1554 let mut sub_entries: BTreeMap<String, DirEntry> = old_entries
1555 .unwrap_or_default()
1556 .into_iter()
1557 .map(|entry| (entry.name.clone(), Self::tree_entry_to_dir_entry(&entry)))
1558 .collect();
1559
1560 for (suffix, data) in new_objects {
1561 let (cid, size) = self.tree.put(data).await.map_err(|e| {
1562 Error::StorageError(format!("put object {}{}: {}", prefix, suffix, e))
1563 })?;
1564 sub_entries.insert(
1565 suffix.clone(),
1566 DirEntry::from_cid(suffix, &cid).with_size(size),
1567 );
1568 if let Some(progress) = progress {
1569 progress.record_object_blob();
1570 }
1571 }
1572
1573 let cid = self
1574 .tree
1575 .put_directory(sub_entries.into_values().collect())
1576 .await
1577 .map_err(|e| Error::StorageError(format!("put objects/{}: {}", prefix, e)))?;
1578 if let Some(progress) = progress {
1579 progress.record_object_prefix();
1580 }
1581 Ok(cid)
1582 }
1583
1584 async fn build_objects_dir_with_base<S: Store>(
1587 &self,
1588 objects: &HashMap<String, Vec<u8>>,
1589 base_tree: Option<&HashTree<S>>,
1590 base_objects_cid: Option<&Cid>,
1591 progress: Option<&RepoTreeBuildProgress>,
1592 ) -> Result<Cid> {
1593 if let Some(progress) = progress {
1594 progress.start_phase(RepoTreeBuildPhase::Objects, Some(objects.len()));
1595 }
1596
1597 let pack_files = self
1598 .pack_files
1599 .read()
1600 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?
1601 .clone();
1602 let packed_object_ids = self
1603 .packed_object_ids
1604 .read()
1605 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?
1606 .clone();
1607 let packs_replace_loose_objects = !packed_object_ids.is_empty() && !pack_files.is_empty();
1608 let mut buckets: BTreeMap<String, Vec<(String, Vec<u8>)>> = BTreeMap::new();
1609 for (oid, data) in objects {
1610 if packs_replace_loose_objects && packed_object_ids.contains(oid) {
1611 continue;
1612 }
1613 let prefix = &oid[..2];
1614 let suffix = &oid[2..];
1615 buckets
1616 .entry(prefix.to_string())
1617 .or_default()
1618 .push((suffix.to_string(), data.clone()));
1619 }
1620
1621 let mut top_entries: BTreeMap<String, DirEntry> = BTreeMap::new();
1622 let mut merged_prefixes = std::collections::HashSet::new();
1623 let mut inherited_pack_entries: BTreeMap<String, DirEntry> = BTreeMap::new();
1624 let empty_objects: Vec<(String, Vec<u8>)> = Vec::new();
1625
1626 if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
1627 for entry in base_tree
1628 .list_directory(base_objects_cid)
1629 .await
1630 .map_err(|e| Error::StorageError(format!("list base objects dir: {}", e)))?
1631 {
1632 if entry.name == "info" || entry.name == "pack" {
1633 if pack_files.is_empty() {
1634 top_entries
1635 .insert(entry.name.clone(), Self::tree_entry_to_dir_entry(&entry));
1636 } else if entry.name == "pack" && entry.link_type == LinkType::Dir {
1637 let pack_cid = Cid {
1638 hash: entry.hash,
1639 key: entry.key,
1640 };
1641 for pack_entry in
1642 base_tree.list_directory(&pack_cid).await.map_err(|e| {
1643 Error::StorageError(format!("list base objects/pack dir: {}", e))
1644 })?
1645 {
1646 inherited_pack_entries.insert(
1647 pack_entry.name.clone(),
1648 Self::tree_entry_to_dir_entry(&pack_entry),
1649 );
1650 }
1651 }
1652 continue;
1653 }
1654
1655 let Some(delta_objects) = buckets.get(&entry.name) else {
1656 if !packs_replace_loose_objects {
1657 top_entries
1658 .insert(entry.name.clone(), Self::tree_entry_to_dir_entry(&entry));
1659 continue;
1660 }
1661
1662 if entry.link_type != LinkType::Dir || entry.name.len() != 2 {
1663 top_entries
1664 .insert(entry.name.clone(), Self::tree_entry_to_dir_entry(&entry));
1665 continue;
1666 }
1667
1668 let prefix_cid = Cid {
1669 hash: entry.hash,
1670 key: entry.key,
1671 };
1672 let old_prefix_entries = base_tree
1673 .list_directory(&prefix_cid)
1674 .await
1675 .map_err(|e| {
1676 Error::StorageError(format!(
1677 "list base objects/{} dir: {}",
1678 entry.name, e
1679 ))
1680 })?
1681 .into_iter()
1682 .filter(|old_entry| {
1683 !packed_object_ids
1684 .contains(&format!("{}{}", entry.name, old_entry.name))
1685 })
1686 .collect::<Vec<_>>();
1687 if old_prefix_entries.is_empty() {
1688 continue;
1689 }
1690
1691 let merged_cid = self
1692 .build_objects_prefix_dir(
1693 &entry.name,
1694 Some(old_prefix_entries),
1695 &empty_objects,
1696 progress,
1697 )
1698 .await?;
1699 top_entries.insert(
1700 entry.name.clone(),
1701 DirEntry::from_cid(&entry.name, &merged_cid).with_link_type(LinkType::Dir),
1702 );
1703 merged_prefixes.insert(entry.name);
1704 continue;
1705 };
1706
1707 let prefix_cid = Cid {
1708 hash: entry.hash,
1709 key: entry.key,
1710 };
1711 let old_prefix_entries = if entry.link_type == LinkType::Dir {
1712 let mut entries = base_tree.list_directory(&prefix_cid).await.map_err(|e| {
1713 Error::StorageError(format!("list base objects/{} dir: {}", entry.name, e))
1714 })?;
1715 if packs_replace_loose_objects {
1716 entries.retain(|old_entry| {
1717 !packed_object_ids
1718 .contains(&format!("{}{}", entry.name, old_entry.name))
1719 });
1720 }
1721 Some(entries)
1722 } else {
1723 None
1724 };
1725 let merged_cid = self
1726 .build_objects_prefix_dir(
1727 &entry.name,
1728 old_prefix_entries,
1729 delta_objects,
1730 progress,
1731 )
1732 .await?;
1733 top_entries.insert(
1734 entry.name.clone(),
1735 DirEntry::from_cid(&entry.name, &merged_cid).with_link_type(LinkType::Dir),
1736 );
1737 merged_prefixes.insert(entry.name);
1738 }
1739 }
1740
1741 for (prefix, objs) in &buckets {
1742 if merged_prefixes.contains(prefix) {
1743 continue;
1744 }
1745 let sub_cid = self
1746 .build_objects_prefix_dir(prefix, None, objs, progress)
1747 .await?;
1748 top_entries.insert(
1749 prefix.clone(),
1750 DirEntry::from_cid(prefix, &sub_cid).with_link_type(LinkType::Dir),
1751 );
1752 }
1753
1754 if !pack_files.is_empty() {
1755 let mut pack_entries_by_name = inherited_pack_entries;
1756 for (name, data) in pack_files {
1757 let (cid, size) = self.tree.put(&data).await.map_err(|e| {
1758 Error::StorageError(format!("put objects/pack/{}: {}", name, e))
1759 })?;
1760 pack_entries_by_name.insert(
1761 name.clone(),
1762 DirEntry::from_cid(&name, &cid).with_size(size),
1763 );
1764 }
1765 let mut pack_names_for_info_packs = pack_entries_by_name
1766 .keys()
1767 .filter(|name| name.ends_with(".pack"))
1768 .cloned()
1769 .collect::<Vec<_>>();
1770 pack_names_for_info_packs.sort();
1771
1772 let pack_dir_cid = self
1773 .tree
1774 .put_directory(pack_entries_by_name.into_values().collect())
1775 .await
1776 .map_err(|e| Error::StorageError(format!("put objects/pack: {}", e)))?;
1777 top_entries.insert(
1778 "pack".to_string(),
1779 DirEntry::from_cid("pack", &pack_dir_cid).with_link_type(LinkType::Dir),
1780 );
1781
1782 let packs_content = pack_names_for_info_packs
1783 .iter()
1784 .map(|name| format!("P {}\n", name))
1785 .collect::<String>();
1786 let (packs_cid, packs_size) = self
1787 .tree
1788 .put(packs_content.as_bytes())
1789 .await
1790 .map_err(|e| Error::StorageError(format!("put objects/info/packs: {}", e)))?;
1791 let info_cid = self
1792 .tree
1793 .put_directory(vec![
1794 DirEntry::from_cid("packs", &packs_cid).with_size(packs_size)
1795 ])
1796 .await
1797 .map_err(|e| Error::StorageError(format!("put objects/info: {}", e)))?;
1798 top_entries.insert(
1799 "info".to_string(),
1800 DirEntry::from_cid("info", &info_cid).with_link_type(LinkType::Dir),
1801 );
1802 }
1803
1804 if !top_entries.contains_key("info") {
1805 let (packs_cid, packs_size) = self
1806 .tree
1807 .put(b"")
1808 .await
1809 .map_err(|e| Error::StorageError(format!("put objects/info/packs: {}", e)))?;
1810 let info_cid = self
1811 .tree
1812 .put_directory(vec![
1813 DirEntry::from_cid("packs", &packs_cid).with_size(packs_size)
1814 ])
1815 .await
1816 .map_err(|e| Error::StorageError(format!("put objects/info: {}", e)))?;
1817 top_entries.insert(
1818 "info".to_string(),
1819 DirEntry::from_cid("info", &info_cid).with_link_type(LinkType::Dir),
1820 );
1821 }
1822
1823 let entry_count = top_entries.len();
1824 let cid = self
1825 .tree
1826 .put_directory(top_entries.into_values().collect())
1827 .await
1828 .map_err(|e| Error::StorageError(format!("put objects dir: {}", e)))?;
1829
1830 debug!(
1831 "Built objects dir with {} entries: {}",
1832 entry_count,
1833 hex::encode(cid.hash)
1834 );
1835 Ok(cid)
1836 }
1837
1838 async fn build_refs_dir(
1840 &self,
1841 refs: &HashMap<String, String>,
1842 progress: Option<&RepoTreeBuildProgress>,
1843 ) -> Result<Cid> {
1844 let mut root = RefDirectory::default();
1845
1846 for (ref_name, value) in refs {
1847 let parts: Vec<&str> = ref_name.split('/').collect();
1848 if parts.len() >= 3 && parts[0] == "refs" {
1849 root.insert(&parts[1..], value.clone());
1850 }
1851 }
1852
1853 let mut ref_entries = self
1854 .build_ref_entries_recursive(&root, "refs", progress)
1855 .await?;
1856
1857 if ref_entries.is_empty() {
1858 let empty_cid = self
1860 .tree
1861 .put_directory(vec![])
1862 .await
1863 .map_err(|e| Error::StorageError(format!("put empty refs: {}", e)))?;
1864 return Ok(empty_cid);
1865 }
1866
1867 ref_entries.sort_by(|a, b| a.name.cmp(&b.name));
1868
1869 let refs_cid = self
1870 .tree
1871 .put_directory(ref_entries)
1872 .await
1873 .map_err(|e| Error::StorageError(format!("put refs dir: {}", e)))?;
1874 debug!("refs dir -> {}", hex::encode(refs_cid.hash));
1875 Ok(refs_cid)
1876 }
1877
1878 fn build_ref_entries_recursive<'a>(
1879 &'a self,
1880 dir: &'a RefDirectory,
1881 prefix: &'a str,
1882 progress: Option<&'a RepoTreeBuildProgress>,
1883 ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1884 Box::pin(async move {
1885 let mut entries = Vec::new();
1886
1887 for (name, value) in &dir.files {
1888 let (cid, size) = self
1889 .tree
1890 .put(value.as_bytes())
1891 .await
1892 .map_err(|e| Error::StorageError(format!("put ref: {}", e)))?;
1893 debug!("{}/{} -> blob {}", prefix, name, hex::encode(cid.hash));
1894 entries.push(DirEntry::from_cid(name, &cid).with_size(size));
1895 if let Some(progress) = progress {
1896 progress.increment_processed();
1897 }
1898 }
1899
1900 for (name, child) in &dir.dirs {
1901 let child_prefix = format!("{prefix}/{name}");
1902 let child_entries = self
1903 .build_ref_entries_recursive(child, &child_prefix, progress)
1904 .await?;
1905 let child_cid =
1906 self.tree.put_directory(child_entries).await.map_err(|e| {
1907 Error::StorageError(format!("put {child_prefix} dir: {}", e))
1908 })?;
1909 debug!("{} dir -> {}", child_prefix, hex::encode(child_cid.hash));
1910 entries.push(DirEntry::from_cid(name, &child_cid).with_link_type(LinkType::Dir));
1911 }
1912
1913 entries.sort_by(|a, b| a.name.cmp(&b.name));
1914 Ok(entries)
1915 })
1916 }
1917
1918 fn build_index_file(
1921 &self,
1922 tree_oid: &str,
1923 objects: &HashMap<String, Vec<u8>>,
1924 progress: Option<&RepoTreeBuildProgress>,
1925 ) -> Result<Vec<u8>> {
1926 let mut entries: Vec<(String, [u8; 20], u32, u32)> = Vec::new(); self.collect_tree_entries_for_index(tree_oid, objects, "", &mut entries, progress)?;
1929
1930 self.build_index_bytes(entries)
1931 }
1932
1933 async fn build_index_file_with_base<S: Store>(
1934 &self,
1935 tree_oid: &str,
1936 objects: &HashMap<String, Vec<u8>>,
1937 base_tree: &HashTree<S>,
1938 base_objects_cid: &Cid,
1939 base_tree_oid: &str,
1940 base_root_entries: Option<&Vec<hashtree_core::TreeEntry>>,
1941 progress: Option<&RepoTreeBuildProgress>,
1942 ) -> Result<Vec<u8>> {
1943 let mut entries: Vec<(String, [u8; 20], u32, u32)> = Vec::new();
1944 let root_entries = base_root_entries.map(|entries| {
1945 entries
1946 .iter()
1947 .filter(|entry| entry.name != ".git")
1948 .cloned()
1949 .collect::<Vec<_>>()
1950 });
1951
1952 self.collect_tree_entries_for_index_with_base_boxed(
1953 tree_oid,
1954 objects,
1955 base_tree,
1956 base_objects_cid,
1957 Some(base_tree_oid),
1958 root_entries.as_ref(),
1959 "",
1960 &mut entries,
1961 progress,
1962 )
1963 .await?;
1964
1965 self.build_index_bytes(entries)
1966 }
1967
1968 fn build_index_bytes(&self, mut entries: Vec<(String, [u8; 20], u32, u32)>) -> Result<Vec<u8>> {
1969 entries.sort_by(|a, b| a.0.cmp(&b.0));
1971
1972 let entry_count = entries.len() as u32;
1973 debug!("Building git index with {} entries", entry_count);
1974
1975 let mut index_data = Vec::new();
1977
1978 index_data.extend_from_slice(b"DIRC");
1980 index_data.extend_from_slice(&2u32.to_be_bytes()); index_data.extend_from_slice(&entry_count.to_be_bytes());
1982
1983 let now_sec = std::time::SystemTime::now()
1985 .duration_since(std::time::UNIX_EPOCH)
1986 .unwrap_or_default()
1987 .as_secs() as u32;
1988
1989 for (path, sha1, mode, size) in &entries {
1990 let entry_start = index_data.len();
1991
1992 index_data.extend_from_slice(&now_sec.to_be_bytes());
1994 index_data.extend_from_slice(&0u32.to_be_bytes());
1995 index_data.extend_from_slice(&now_sec.to_be_bytes());
1997 index_data.extend_from_slice(&0u32.to_be_bytes());
1998 index_data.extend_from_slice(&0u32.to_be_bytes());
2000 index_data.extend_from_slice(&0u32.to_be_bytes());
2001 index_data.extend_from_slice(&mode.to_be_bytes());
2003 index_data.extend_from_slice(&0u32.to_be_bytes());
2005 index_data.extend_from_slice(&0u32.to_be_bytes());
2006 index_data.extend_from_slice(&size.to_be_bytes());
2008 index_data.extend_from_slice(sha1);
2010 let path_len = std::cmp::min(path.len(), 0xFFF) as u16;
2012 index_data.extend_from_slice(&path_len.to_be_bytes());
2013 index_data.extend_from_slice(path.as_bytes());
2015 index_data.push(0); let entry_len = index_data.len() - entry_start;
2019 let padding = (8 - (entry_len % 8)) % 8;
2020 index_data.extend(std::iter::repeat_n(0, padding));
2021 }
2022
2023 let mut hasher = Sha1::new();
2025 hasher.update(&index_data);
2026 let checksum = hasher.finalize();
2027 index_data.extend_from_slice(&checksum);
2028
2029 debug!(
2030 "Built git index: {} bytes, {} entries",
2031 index_data.len(),
2032 entry_count
2033 );
2034 Ok(index_data)
2035 }
2036
2037 fn collect_tree_entries_for_index(
2039 &self,
2040 tree_oid: &str,
2041 objects: &HashMap<String, Vec<u8>>,
2042 prefix: &str,
2043 entries: &mut Vec<(String, [u8; 20], u32, u32)>,
2044 progress: Option<&RepoTreeBuildProgress>,
2045 ) -> Result<()> {
2046 let (obj_type, content) = self
2047 .get_object_content(tree_oid, objects)
2048 .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
2049
2050 if obj_type != ObjectType::Tree {
2051 return Err(Error::InvalidObjectType(format!(
2052 "expected tree, got {:?}",
2053 obj_type
2054 )));
2055 }
2056
2057 let tree_entries = parse_tree(&content)?;
2058
2059 for entry in tree_entries {
2060 let path = if prefix.is_empty() {
2061 entry.name.clone()
2062 } else {
2063 format!("{}/{}", prefix, entry.name)
2064 };
2065
2066 let oid_hex = entry.oid.to_hex();
2067
2068 if entry.is_tree() {
2069 self.collect_tree_entries_for_index(&oid_hex, objects, &path, entries, progress)?;
2071 } else {
2072 if let Some((ObjectType::Blob, blob_content)) =
2074 self.get_object_content(&oid_hex, objects)
2075 {
2076 let mut sha1_bytes = [0u8; 20];
2078 if let Ok(bytes) = hex::decode(&oid_hex) {
2079 if bytes.len() == 20 {
2080 sha1_bytes.copy_from_slice(&bytes);
2081 }
2082 }
2083
2084 let mode = entry.mode;
2086 let size = blob_content.len() as u32;
2087
2088 entries.push((path, sha1_bytes, mode, size));
2089 if let Some(progress) = progress {
2090 progress.record_index_entry();
2091 }
2092 }
2093 }
2094 }
2095
2096 Ok(())
2097 }
2098
2099 fn collect_tree_entries_for_index_with_base_boxed<'a, S: Store + 'a>(
2100 &'a self,
2101 tree_oid: &'a str,
2102 objects: &'a HashMap<String, Vec<u8>>,
2103 base_tree: &'a HashTree<S>,
2104 base_objects_cid: &'a Cid,
2105 old_tree_oid: Option<&'a str>,
2106 old_dir_entries: Option<&'a Vec<hashtree_core::TreeEntry>>,
2107 prefix: &'a str,
2108 entries: &'a mut Vec<(String, [u8; 20], u32, u32)>,
2109 progress: Option<&'a RepoTreeBuildProgress>,
2110 ) -> BoxFuture<'a, Result<()>> {
2111 Box::pin(async move {
2112 let tree_entries = self
2113 .get_tree_entries_from_sources(
2114 tree_oid,
2115 objects,
2116 Some(base_tree),
2117 Some(base_objects_cid),
2118 )
2119 .await?;
2120 let old_tree_entries = if let Some(old_tree_oid) = old_tree_oid {
2121 Some(
2122 self.get_tree_entries_from_sources(
2123 old_tree_oid,
2124 objects,
2125 Some(base_tree),
2126 Some(base_objects_cid),
2127 )
2128 .await?,
2129 )
2130 } else {
2131 None
2132 };
2133 let mut old_tree_entry_map = old_tree_entries
2134 .unwrap_or_default()
2135 .into_iter()
2136 .map(|entry| (entry.name.clone(), entry))
2137 .collect::<HashMap<_, _>>();
2138 let mut old_dir_entry_map = old_dir_entries
2139 .cloned()
2140 .unwrap_or_default()
2141 .into_iter()
2142 .map(|entry| (entry.name.clone(), entry))
2143 .collect::<HashMap<_, _>>();
2144
2145 for entry in tree_entries {
2146 let path = if prefix.is_empty() {
2147 entry.name.clone()
2148 } else {
2149 format!("{}/{}", prefix, entry.name)
2150 };
2151 let oid_hex = entry.oid.to_hex();
2152 let old_tree_entry = old_tree_entry_map.remove(&entry.name);
2153 let old_dir_entry = old_dir_entry_map.remove(&entry.name);
2154
2155 if entry.is_tree() {
2156 let old_subtree_oid = old_tree_entry
2157 .as_ref()
2158 .filter(|old| old.is_tree())
2159 .map(|old| old.oid.to_hex());
2160 let old_subdir_entries = if let Some(old_dir_entry) = old_dir_entry.as_ref() {
2161 if old_dir_entry.link_type == LinkType::Dir {
2162 Some(
2163 base_tree
2164 .list_directory(&Cid {
2165 hash: old_dir_entry.hash,
2166 key: old_dir_entry.key,
2167 })
2168 .await
2169 .map_err(|e| {
2170 Error::StorageError(format!(
2171 "list base working dir {}: {}",
2172 path, e
2173 ))
2174 })?,
2175 )
2176 } else {
2177 None
2178 }
2179 } else {
2180 None
2181 };
2182
2183 self.collect_tree_entries_for_index_with_base_boxed(
2184 &oid_hex,
2185 objects,
2186 base_tree,
2187 base_objects_cid,
2188 old_subtree_oid.as_deref(),
2189 old_subdir_entries.as_ref(),
2190 &path,
2191 entries,
2192 progress,
2193 )
2194 .await?;
2195 continue;
2196 }
2197
2198 let mut sha1_bytes = [0u8; 20];
2199 if let Ok(bytes) = hex::decode(&oid_hex) {
2200 if bytes.len() == 20 {
2201 sha1_bytes.copy_from_slice(&bytes);
2202 }
2203 }
2204
2205 let size = if let (Some(old_tree_entry), Some(old_dir_entry)) =
2206 (old_tree_entry.as_ref(), old_dir_entry.as_ref())
2207 {
2208 if !old_tree_entry.is_tree()
2209 && old_tree_entry.oid == entry.oid
2210 && old_dir_entry.link_type != LinkType::Dir
2211 {
2212 old_dir_entry.size as u32
2213 } else {
2214 self.blob_size_from_sources(
2215 &oid_hex,
2216 objects,
2217 Some(base_tree),
2218 Some(base_objects_cid),
2219 )
2220 .await? as u32
2221 }
2222 } else {
2223 self.blob_size_from_sources(
2224 &oid_hex,
2225 objects,
2226 Some(base_tree),
2227 Some(base_objects_cid),
2228 )
2229 .await? as u32
2230 };
2231
2232 entries.push((path, sha1_bytes, entry.mode, size));
2233 if let Some(progress) = progress {
2234 progress.record_index_entry();
2235 }
2236 }
2237
2238 Ok(())
2239 })
2240 }
2241
2242 async fn blob_size_from_sources<S: Store>(
2243 &self,
2244 oid: &str,
2245 objects: &HashMap<String, Vec<u8>>,
2246 base_tree: Option<&HashTree<S>>,
2247 base_objects_cid: Option<&Cid>,
2248 ) -> Result<usize> {
2249 let object = match self.get_object_content(oid, objects) {
2250 Some(object) => Some(object),
2251 None => {
2252 if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
2253 self.get_object_content_from_base(oid, base_tree, base_objects_cid)
2254 .await?
2255 } else {
2256 None
2257 }
2258 }
2259 }
2260 .ok_or_else(|| Error::ObjectNotFound(oid.to_string()))?;
2261
2262 if object.0 != ObjectType::Blob {
2263 return Err(Error::InvalidObjectType(format!(
2264 "expected blob, got {:?}",
2265 object.0
2266 )));
2267 }
2268
2269 Ok(object.1.len())
2270 }
2271
2272 pub fn store(&self) -> &Arc<LocalStore> {
2274 &self.store
2275 }
2276
2277 #[allow(dead_code)]
2279 pub fn hashtree(&self) -> &HashTree<LocalStore> {
2280 &self.tree
2281 }
2282
2283 #[allow(dead_code)]
2285 pub fn push_to_file_servers(
2286 &self,
2287 blossom: &hashtree_blossom::BlossomClient,
2288 ) -> Result<(usize, usize)> {
2289 let hashes = self
2290 .store
2291 .list()
2292 .map_err(|e| Error::StorageError(format!("list hashes: {}", e)))?;
2293
2294 info!("Pushing {} blobs to file servers", hashes.len());
2295
2296 let mut uploaded = 0;
2297 let mut existed = 0;
2298
2299 self.runtime.block_on(async {
2300 for hash in &hashes {
2301 let hex_hash = hex::encode(hash);
2302 let data = match self.store.get_sync(hash) {
2303 Ok(Some(d)) => d,
2304 _ => continue,
2305 };
2306
2307 match blossom.upload_if_missing(&data).await {
2308 Ok((_, true)) => {
2309 debug!("Uploaded {}", &hex_hash[..12]);
2310 uploaded += 1;
2311 }
2312 Ok((_, false)) => {
2313 existed += 1;
2314 }
2315 Err(e) => {
2316 debug!("Failed to upload {}: {}", &hex_hash[..12], e);
2317 }
2318 }
2319 }
2320 });
2321
2322 info!(
2323 "Upload complete: {} new, {} already existed",
2324 uploaded, existed
2325 );
2326 Ok((uploaded, existed))
2327 }
2328
2329 #[allow(dead_code)]
2331 pub fn clear(&self) -> Result<()> {
2332 let mut objects = self
2333 .objects
2334 .write()
2335 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
2336 let mut refs = self
2337 .refs
2338 .write()
2339 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
2340 let mut root = self
2341 .root_cid
2342 .write()
2343 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
2344
2345 objects.clear();
2346 refs.clear();
2347 *root = None;
2348 Ok(())
2349 }
2350}
2351
2352#[cfg(test)]
2353mod tests {
2354 use super::*;
2355 use hashtree_core::store::Store;
2356 use hashtree_core::{sha256, LinkType};
2357 use std::io::{Read, Write};
2358 use std::net::{TcpListener, TcpStream};
2359 use std::path::Path;
2360 use std::process::{Child, Command, Stdio};
2361 #[cfg(feature = "lmdb")]
2362 use std::sync::atomic::{AtomicUsize, Ordering};
2363 use std::time::{Duration, Instant};
2364 use tempfile::TempDir;
2365
2366 fn create_test_storage() -> (GitStorage, TempDir) {
2367 let temp_dir = TempDir::new().unwrap();
2368 let storage = GitStorage::open(temp_dir.path()).unwrap();
2369 (storage, temp_dir)
2370 }
2371
2372 fn create_test_storage_with_limit(max_size_bytes: u64) -> (GitStorage, TempDir) {
2373 let temp_dir = TempDir::new().unwrap();
2374 let storage = GitStorage::open_with_backend_and_max_bytes(
2375 temp_dir.path(),
2376 StorageBackend::Fs,
2377 max_size_bytes,
2378 )
2379 .unwrap();
2380 (storage, temp_dir)
2381 }
2382
2383 fn local_total_bytes(storage: &GitStorage) -> u64 {
2384 match storage.store().as_ref() {
2385 LocalStore::Fs(store) => store.stats().unwrap().total_bytes,
2386 #[cfg(feature = "lmdb")]
2387 LocalStore::Lmdb(store) => store.stats().unwrap().total_bytes,
2388 }
2389 }
2390
2391 fn write_test_commit(storage: &GitStorage) -> ObjectId {
2392 let blob_oid = storage
2393 .write_raw_object(ObjectType::Blob, b"hello from hashtree\n")
2394 .unwrap();
2395
2396 let mut tree_content = Vec::new();
2397 tree_content.extend_from_slice(b"100644 README.md\0");
2398 tree_content.extend_from_slice(&hex::decode(blob_oid.to_hex()).unwrap());
2399 let tree_oid = storage
2400 .write_raw_object(ObjectType::Tree, &tree_content)
2401 .unwrap();
2402
2403 let commit_content = format!(
2404 "tree {}\nauthor Test User <test@example.com> 0 +0000\ncommitter Test User <test@example.com> 0 +0000\n\nInitial commit\n",
2405 tree_oid.to_hex()
2406 );
2407 storage
2408 .write_raw_object(ObjectType::Commit, commit_content.as_bytes())
2409 .unwrap()
2410 }
2411
2412 fn export_tree_to_fs<S: Store>(
2413 runtime: &RuntimeExecutor,
2414 tree: &HashTree<S>,
2415 cid: &Cid,
2416 dst: &Path,
2417 ) {
2418 std::fs::create_dir_all(dst).unwrap();
2419 let entries = runtime.block_on(tree.list_directory(cid)).unwrap();
2420 for entry in entries {
2421 let entry_cid = Cid {
2422 hash: entry.hash,
2423 key: entry.key,
2424 };
2425 let path = dst.join(&entry.name);
2426 match entry.link_type {
2427 LinkType::Dir => export_tree_to_fs(runtime, tree, &entry_cid, &path),
2428 LinkType::Blob | LinkType::File => {
2429 let data = runtime
2430 .block_on(tree.get(&entry_cid, None))
2431 .unwrap()
2432 .unwrap();
2433 if let Some(parent) = path.parent() {
2434 std::fs::create_dir_all(parent).unwrap();
2435 }
2436 std::fs::write(path, data).unwrap();
2437 }
2438 }
2439 }
2440 }
2441
2442 fn spawn_http_server(root: &Path, port: u16) -> Child {
2443 Command::new("python3")
2444 .args([
2445 "-m",
2446 "http.server",
2447 &port.to_string(),
2448 "--bind",
2449 "127.0.0.1",
2450 ])
2451 .current_dir(root)
2452 .stdout(Stdio::null())
2453 .stderr(Stdio::null())
2454 .spawn()
2455 .expect("spawn python http server")
2456 }
2457
2458 fn wait_for_http_server(server: &mut Child, port: u16, path: &str) {
2459 let deadline = Instant::now() + Duration::from_secs(5);
2460
2461 loop {
2462 if let Some(status) = server.try_wait().expect("check http server status") {
2463 panic!("python http server exited before becoming ready: {status}");
2464 }
2465
2466 if let Ok(mut stream) = TcpStream::connect(("127.0.0.1", port)) {
2467 stream
2468 .set_read_timeout(Some(Duration::from_millis(200)))
2469 .expect("set read timeout");
2470 stream
2471 .set_write_timeout(Some(Duration::from_millis(200)))
2472 .expect("set write timeout");
2473 let request =
2474 format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n");
2475 if stream.write_all(request.as_bytes()).is_ok() {
2476 let mut response = String::new();
2477 if stream.read_to_string(&mut response).is_ok()
2478 && response.starts_with("HTTP/1.0 200")
2479 {
2480 return;
2481 }
2482 }
2483 }
2484
2485 if Instant::now() >= deadline {
2486 panic!("python http server did not become ready on port {port}");
2487 }
2488 std::thread::sleep(Duration::from_millis(50));
2489 }
2490 }
2491
2492 #[test]
2493 fn test_import_ref() {
2494 let (storage, _temp) = create_test_storage();
2495
2496 storage
2498 .import_ref("refs/heads/main", "abc123def456")
2499 .unwrap();
2500
2501 assert!(storage.has_ref("refs/heads/main").unwrap());
2503
2504 let refs = storage.list_refs().unwrap();
2506 assert_eq!(
2507 refs.get("refs/heads/main"),
2508 Some(&"abc123def456".to_string())
2509 );
2510 }
2511
2512 #[cfg(feature = "lmdb")]
2513 #[test]
2514 fn test_local_store_falls_back_to_fs_when_lmdb_open_returns_enosys() {
2515 let temp_dir = TempDir::new().unwrap();
2516 let fs_calls = AtomicUsize::new(0);
2517 let lmdb_calls = AtomicUsize::new(0);
2518
2519 let store = LocalStore::new_for_backend_with_openers(
2520 temp_dir.path(),
2521 StorageBackend::Lmdb,
2522 0,
2523 |path, max_bytes| {
2524 fs_calls.fetch_add(1, Ordering::SeqCst);
2525 LocalStore::open_fs_store(path, max_bytes)
2526 },
2527 |_path, _max_bytes| {
2528 lmdb_calls.fetch_add(1, Ordering::SeqCst);
2529 Err(StoreError::Io(std::io::Error::from_raw_os_error(
2530 libc::ENOSYS,
2531 )))
2532 },
2533 )
2534 .unwrap();
2535
2536 assert!(matches!(store, LocalStore::Fs(_)));
2537 assert_eq!(lmdb_calls.load(Ordering::SeqCst), 1);
2538 assert_eq!(fs_calls.load(Ordering::SeqCst), 1);
2539 }
2540
2541 #[cfg(feature = "lmdb")]
2542 #[test]
2543 fn test_local_store_does_not_fallback_on_unrelated_lmdb_errors() {
2544 let temp_dir = TempDir::new().unwrap();
2545 let fs_calls = AtomicUsize::new(0);
2546
2547 let result = LocalStore::new_for_backend_with_openers(
2548 temp_dir.path(),
2549 StorageBackend::Lmdb,
2550 0,
2551 |path, max_bytes| {
2552 fs_calls.fetch_add(1, Ordering::SeqCst);
2553 LocalStore::open_fs_store(path, max_bytes)
2554 },
2555 |_path, _max_bytes| {
2556 Err(StoreError::Io(std::io::Error::from_raw_os_error(
2557 libc::EACCES,
2558 )))
2559 },
2560 );
2561
2562 assert!(
2563 matches!(result, Err(StoreError::Io(io_error)) if io_error.raw_os_error() == Some(libc::EACCES))
2564 );
2565 assert_eq!(fs_calls.load(Ordering::SeqCst), 0);
2566 }
2567
2568 #[test]
2569 fn test_import_multiple_refs_preserves_all() {
2570 let (storage, _temp) = create_test_storage();
2571
2572 storage.import_ref("refs/heads/main", "sha_main").unwrap();
2574 storage.import_ref("refs/heads/dev", "sha_dev").unwrap();
2575 storage
2576 .import_ref("refs/heads/feature", "sha_feature")
2577 .unwrap();
2578
2579 assert!(storage.has_ref("refs/heads/main").unwrap());
2581 assert!(storage.has_ref("refs/heads/dev").unwrap());
2582 assert!(storage.has_ref("refs/heads/feature").unwrap());
2583
2584 storage
2586 .write_ref(
2587 "refs/heads/new-branch",
2588 &Ref::Direct(
2589 ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2590 ),
2591 )
2592 .unwrap();
2593
2594 let refs = storage.list_refs().unwrap();
2596 assert_eq!(refs.len(), 4);
2597 assert!(refs.contains_key("refs/heads/main"));
2598 assert!(refs.contains_key("refs/heads/dev"));
2599 assert!(refs.contains_key("refs/heads/feature"));
2600 assert!(refs.contains_key("refs/heads/new-branch"));
2601 }
2602
2603 #[test]
2604 fn test_import_compressed_object() {
2605 let (storage, _temp) = create_test_storage();
2606
2607 let fake_compressed = vec![0x78, 0x9c, 0x01, 0x02, 0x03]; storage
2611 .import_compressed_object("abc123def456", fake_compressed.clone())
2612 .unwrap();
2613
2614 assert_eq!(storage.object_count().unwrap(), 1);
2616 }
2617
2618 #[test]
2619 fn test_write_ref_overwrites_imported() {
2620 let (storage, _temp) = create_test_storage();
2621
2622 storage.import_ref("refs/heads/main", "old_sha").unwrap();
2624
2625 storage
2627 .write_ref(
2628 "refs/heads/main",
2629 &Ref::Direct(
2630 ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2631 ),
2632 )
2633 .unwrap();
2634
2635 let refs = storage.list_refs().unwrap();
2637 assert_eq!(
2638 refs.get("refs/heads/main"),
2639 Some(&"0123456789abcdef0123456789abcdef01234567".to_string())
2640 );
2641 }
2642
2643 #[test]
2644 fn test_delete_ref_preserves_others() {
2645 let (storage, _temp) = create_test_storage();
2646
2647 storage.import_ref("refs/heads/main", "sha_main").unwrap();
2649 storage.import_ref("refs/heads/dev", "sha_dev").unwrap();
2650
2651 storage.delete_ref("refs/heads/dev").unwrap();
2653
2654 assert!(storage.has_ref("refs/heads/main").unwrap());
2656 assert!(!storage.has_ref("refs/heads/dev").unwrap());
2657 }
2658
2659 #[test]
2660 fn test_clear_removes_all() {
2661 let (storage, _temp) = create_test_storage();
2662
2663 storage.import_ref("refs/heads/main", "sha_main").unwrap();
2665 storage
2666 .import_compressed_object("obj1", vec![1, 2, 3])
2667 .unwrap();
2668
2669 storage.clear().unwrap();
2671
2672 assert!(!storage.has_ref("refs/heads/main").unwrap());
2674 assert_eq!(storage.object_count().unwrap(), 0);
2675 }
2676
2677 #[test]
2678 fn test_evict_if_needed_respects_configured_limit() {
2679 let (storage, _temp) = create_test_storage_with_limit(1_024);
2680
2681 storage
2682 .write_raw_object(ObjectType::Blob, &vec![b'a'; 900])
2683 .unwrap();
2684 storage
2685 .write_raw_object(ObjectType::Blob, &vec![b'b'; 900])
2686 .unwrap();
2687 storage
2688 .write_ref(
2689 "refs/heads/main",
2690 &Ref::Direct(
2691 ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2692 ),
2693 )
2694 .unwrap();
2695
2696 storage.build_tree().unwrap();
2697
2698 let before = local_total_bytes(&storage);
2699 assert!(before > 1_024);
2700
2701 let freed = storage.evict_if_needed().unwrap();
2702 assert!(freed > 0);
2703
2704 let after = local_total_bytes(&storage);
2705 assert!(after <= 1_024);
2706 }
2707
2708 #[test]
2709 fn test_build_tree_evicts_stale_blobs_before_writing_new_tree() {
2710 let max_size_bytes = 16 * 1024;
2711 let (storage, _temp) = create_test_storage_with_limit(max_size_bytes);
2712
2713 let stale_blobs = vec![
2714 vec![b'x'; 7 * 1024],
2715 vec![b'y'; 7 * 1024],
2716 vec![b'z'; 7 * 1024],
2717 ];
2718 let stale_hashes: Vec<Hash> = stale_blobs.iter().map(|blob| sha256(blob)).collect();
2719
2720 for (hash, blob) in stale_hashes.iter().zip(stale_blobs) {
2721 storage
2722 .runtime
2723 .block_on(storage.store().put(*hash, blob))
2724 .unwrap();
2725 }
2726
2727 let before = local_total_bytes(&storage);
2728 assert!(before > max_size_bytes);
2729
2730 let commit_oid = write_test_commit(&storage);
2731 storage
2732 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2733 .unwrap();
2734 storage
2735 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2736 .unwrap();
2737
2738 storage.build_tree().unwrap();
2739
2740 let evicted_stale = stale_hashes
2741 .iter()
2742 .filter(|hash| !storage.runtime.block_on(storage.store().has(hash)).unwrap())
2743 .count();
2744
2745 assert!(
2746 evicted_stale > 0,
2747 "expected build_tree preflight eviction to remove stale blobs before writing"
2748 );
2749 }
2750
2751 #[test]
2752 fn test_build_tree_progress_tracks_repo_tree_work() {
2753 let (storage, _temp) = create_test_storage();
2754 let commit_oid = write_test_commit(&storage);
2755 storage
2756 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2757 .unwrap();
2758 storage
2759 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2760 .unwrap();
2761
2762 let progress = RepoTreeBuildProgress::new();
2763 storage.build_tree_with_progress(&progress).unwrap();
2764
2765 let snapshot = progress.snapshot();
2766 assert_eq!(snapshot.phase, RepoTreeBuildPhase::Done);
2767 assert_eq!(snapshot.object_blobs, 3);
2768 assert_eq!(snapshot.files, 1);
2769 assert_eq!(snapshot.dirs, 0);
2770 assert_eq!(snapshot.reused, 0);
2771 assert_eq!(
2772 snapshot.format_for_label("Building repo tree"),
2773 " Building repo tree: done (3 object blobs, 1 files, 0 dirs, 0 reused)"
2774 );
2775 }
2776
2777 #[test]
2778 fn test_build_tree_adds_dumb_http_metadata() {
2779 let (storage, _temp) = create_test_storage();
2780 let commit_oid = write_test_commit(&storage);
2781 let tag_content = format!(
2782 "object {}\ntype commit\ntag v1.0.0\ntagger Test User <test@example.com> 0 +0000\n\nrelease\n",
2783 commit_oid.to_hex()
2784 );
2785 let tag_oid = storage
2786 .write_raw_object(ObjectType::Tag, tag_content.as_bytes())
2787 .unwrap();
2788
2789 storage
2790 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2791 .unwrap();
2792 storage
2793 .write_ref("refs/tags/v1.0.0", &Ref::Direct(tag_oid))
2794 .unwrap();
2795 storage
2796 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2797 .unwrap();
2798
2799 let root_cid = storage.build_tree().unwrap();
2800
2801 let info_refs_cid = storage
2802 .runtime
2803 .block_on(storage.tree.resolve_path(&root_cid, ".git/info/refs"))
2804 .unwrap()
2805 .expect("info/refs exists");
2806 let info_refs = storage
2807 .runtime
2808 .block_on(storage.tree.get(&info_refs_cid, None))
2809 .unwrap()
2810 .unwrap();
2811 let info_refs = String::from_utf8(info_refs).unwrap();
2812
2813 assert_eq!(
2814 info_refs,
2815 format!(
2816 "{commit}\trefs/heads/main\n{tag}\trefs/tags/v1.0.0\n{commit}\trefs/tags/v1.0.0^{{}}\n",
2817 commit = commit_oid.to_hex(),
2818 tag = tag_oid.to_hex()
2819 )
2820 );
2821
2822 let packs_cid = storage
2823 .runtime
2824 .block_on(
2825 storage
2826 .tree
2827 .resolve_path(&root_cid, ".git/objects/info/packs"),
2828 )
2829 .unwrap()
2830 .expect("objects/info/packs exists");
2831 let packs = storage
2832 .runtime
2833 .block_on(storage.tree.get(&packs_cid, None))
2834 .unwrap()
2835 .unwrap();
2836 assert!(packs.is_empty(), "objects/info/packs should be empty");
2837 }
2838
2839 #[test]
2840 fn test_build_tree_writes_git_objects_info_packs_for_pack_files() {
2841 let (storage, _temp) = create_test_storage();
2842 let commit_oid = write_test_commit(&storage);
2843 storage
2844 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2845 .unwrap();
2846 storage
2847 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2848 .unwrap();
2849
2850 let pack_hash = "0123456789abcdef0123456789abcdef01234567";
2851 let pack_name = format!("pack-{pack_hash}.pack");
2852 let idx_name = format!("pack-{pack_hash}.idx");
2853 let mut pack_files = BTreeMap::new();
2854 pack_files.insert(pack_name.clone(), b"pack bytes".to_vec());
2855 pack_files.insert(idx_name.clone(), b"idx bytes".to_vec());
2856 storage.set_pack_files(pack_files).unwrap();
2857
2858 let root_cid = storage.build_tree().unwrap();
2859
2860 let packs_cid = storage
2861 .runtime
2862 .block_on(
2863 storage
2864 .tree
2865 .resolve_path(&root_cid, ".git/objects/info/packs"),
2866 )
2867 .unwrap()
2868 .expect("objects/info/packs exists");
2869 let packs = storage
2870 .runtime
2871 .block_on(storage.tree.get(&packs_cid, None))
2872 .unwrap()
2873 .unwrap();
2874 assert_eq!(
2875 String::from_utf8(packs).unwrap(),
2876 format!("P {pack_name}\n")
2877 );
2878
2879 for path in [
2880 format!(".git/objects/pack/{pack_name}"),
2881 format!(".git/objects/pack/{idx_name}"),
2882 ] {
2883 let cid = storage
2884 .runtime
2885 .block_on(storage.tree.resolve_path(&root_cid, &path))
2886 .unwrap()
2887 .unwrap_or_else(|| panic!("{path} should exist"));
2888 let content = storage
2889 .runtime
2890 .block_on(storage.tree.get(&cid, None))
2891 .unwrap()
2892 .unwrap();
2893 assert!(!content.is_empty(), "{path} should have content");
2894 }
2895 }
2896
2897 #[test]
2898 fn test_pack_checkpoint_can_replace_loose_git_objects() {
2899 let (storage, _temp) = create_test_storage();
2900 let commit_oid = write_test_commit(&storage);
2901 storage
2902 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2903 .unwrap();
2904 storage
2905 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2906 .unwrap();
2907
2908 let pack_hash = "0123456789abcdef0123456789abcdef01234567";
2909 let pack_name = format!("pack-{pack_hash}.pack");
2910 let idx_name = format!("pack-{pack_hash}.idx");
2911 let mut pack_files = BTreeMap::new();
2912 pack_files.insert(pack_name.clone(), b"pack bytes".to_vec());
2913 pack_files.insert(idx_name, b"idx bytes".to_vec());
2914 storage
2915 .set_pack_checkpoint_files(pack_files, HashSet::from([commit_oid.to_hex()]))
2916 .unwrap();
2917
2918 let root_cid = storage.build_tree().unwrap();
2919 let loose_path = format!(
2920 ".git/objects/{}/{}",
2921 &commit_oid.to_hex()[..2],
2922 &commit_oid.to_hex()[2..]
2923 );
2924 let loose_cid = storage
2925 .runtime
2926 .block_on(storage.tree.resolve_path(&root_cid, &loose_path))
2927 .unwrap();
2928 assert!(
2929 loose_cid.is_none(),
2930 "pack checkpoint should omit duplicated loose objects"
2931 );
2932
2933 storage
2934 .validate_root_contains_direct_refs(&root_cid)
2935 .expect("pack checkpoint should satisfy ref validation");
2936
2937 let packs_cid = storage
2938 .runtime
2939 .block_on(
2940 storage
2941 .tree
2942 .resolve_path(&root_cid, ".git/objects/info/packs"),
2943 )
2944 .unwrap()
2945 .expect("objects/info/packs exists");
2946 let packs = storage
2947 .runtime
2948 .block_on(storage.tree.get(&packs_cid, None))
2949 .unwrap()
2950 .unwrap();
2951 assert_eq!(
2952 String::from_utf8(packs).unwrap(),
2953 format!("P {pack_name}\n")
2954 );
2955 }
2956
2957 #[test]
2958 fn test_build_tree_materializes_loose_refs_at_git_paths() {
2959 let (storage, _temp) = create_test_storage();
2960 let commit_oid = write_test_commit(&storage);
2961
2962 storage
2963 .write_ref("refs/heads/master", &Ref::Direct(commit_oid))
2964 .unwrap();
2965 storage
2966 .write_ref("refs/heads/codex/meshrouter-prod", &Ref::Direct(commit_oid))
2967 .unwrap();
2968 storage
2969 .write_ref("refs/tags/v1.0.0", &Ref::Direct(commit_oid))
2970 .unwrap();
2971 storage
2972 .write_ref("HEAD", &Ref::Symbolic("refs/heads/master".to_string()))
2973 .unwrap();
2974
2975 let root_cid = storage.build_tree().unwrap();
2976
2977 for path in [
2978 ".git/refs/heads/master",
2979 ".git/refs/heads/codex/meshrouter-prod",
2980 ".git/refs/tags/v1.0.0",
2981 ] {
2982 let ref_cid = storage
2983 .runtime
2984 .block_on(storage.tree.resolve_path(&root_cid, path))
2985 .unwrap()
2986 .unwrap_or_else(|| panic!("{path} should exist"));
2987 let ref_value = storage
2988 .runtime
2989 .block_on(storage.tree.get(&ref_cid, None))
2990 .unwrap()
2991 .unwrap();
2992 assert_eq!(
2993 String::from_utf8(ref_value).unwrap(),
2994 commit_oid.to_hex(),
2995 "{path} should contain the ref target",
2996 );
2997 }
2998 }
2999
3000 #[test]
3001 fn test_materialized_tree_supports_static_http_clone_from_git_dir() {
3002 let (storage, _temp) = create_test_storage();
3003 let commit_oid = write_test_commit(&storage);
3004 storage
3005 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
3006 .unwrap();
3007 storage
3008 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
3009 .unwrap();
3010
3011 let root_cid = storage.build_tree().unwrap();
3012 let export_dir = TempDir::new().unwrap();
3013 let repo_dir = export_dir.path().join("repo");
3014 export_tree_to_fs(&storage.runtime, &storage.tree, &root_cid, &repo_dir);
3015
3016 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
3017 let port = listener.local_addr().unwrap().port();
3018 drop(listener);
3019
3020 let mut server = spawn_http_server(export_dir.path(), port);
3021 wait_for_http_server(&mut server, port, "/repo/.git/HEAD");
3022
3023 let clone_dir = TempDir::new().unwrap();
3024 let clone_path = clone_dir.path().join("clone");
3025 let output = Command::new("git")
3026 .current_dir(clone_dir.path())
3027 .args([
3028 "clone",
3029 &format!("http://127.0.0.1:{port}/repo/.git", port = port),
3030 clone_path.to_str().unwrap(),
3031 ])
3032 .output()
3033 .unwrap();
3034
3035 let _ = server.kill();
3036 let _ = server.wait();
3037
3038 assert!(
3039 output.status.success(),
3040 "git clone failed: {}",
3041 String::from_utf8_lossy(&output.stderr)
3042 );
3043 assert_eq!(
3044 std::fs::read_to_string(clone_path.join("README.md")).unwrap(),
3045 "hello from hashtree\n"
3046 );
3047 }
3048
3049 #[test]
3050 fn test_validate_root_contains_direct_refs_rejects_missing_tip_object() {
3051 let (storage, _temp) = create_test_storage();
3052 let commit_oid = write_test_commit(&storage);
3053 storage
3054 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
3055 .unwrap();
3056 storage
3057 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
3058 .unwrap();
3059
3060 let empty_objects_dir = storage
3061 .runtime
3062 .block_on(storage.tree.put_directory(vec![]))
3063 .unwrap();
3064 let refs_dir = storage
3065 .runtime
3066 .block_on(storage.tree.put_directory(vec![]))
3067 .unwrap();
3068 let info_dir = storage
3069 .runtime
3070 .block_on(storage.tree.put_directory(vec![]))
3071 .unwrap();
3072 let git_dir = storage
3073 .runtime
3074 .block_on(storage.tree.put_directory(vec![
3075 DirEntry::from_cid("HEAD", &info_dir).with_size(0),
3076 DirEntry::from_cid("info", &info_dir).with_link_type(LinkType::Dir),
3077 DirEntry::from_cid("objects", &empty_objects_dir).with_link_type(LinkType::Dir),
3078 DirEntry::from_cid("refs", &refs_dir).with_link_type(LinkType::Dir),
3079 ]))
3080 .unwrap();
3081 let root_cid = storage
3082 .runtime
3083 .block_on(storage.tree.put_directory(vec![
3084 DirEntry::from_cid(".git", &git_dir).with_link_type(LinkType::Dir),
3085 ]))
3086 .unwrap();
3087
3088 let err = storage
3089 .validate_root_contains_direct_refs(&root_cid)
3090 .expect_err("missing ref-tip object should fail validation");
3091 assert!(
3092 err.to_string().contains(&commit_oid.to_hex()),
3093 "validation error should mention missing commit oid: {}",
3094 err
3095 );
3096 }
3097}