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 pub fn add_pack_covered_objects(&self, covered_objects: HashSet<String>) -> Result<()> {
543 let mut packed_object_ids = self
544 .packed_object_ids
545 .write()
546 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
547 packed_object_ids.extend(covered_objects);
548
549 if let Ok(mut root) = self.root_cid.write() {
550 *root = None;
551 }
552
553 Ok(())
554 }
555
556 #[cfg(test)]
558 pub fn has_ref(&self, name: &str) -> Result<bool> {
559 let refs = self
560 .refs
561 .read()
562 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
563 Ok(refs.contains_key(name))
564 }
565
566 #[cfg(test)]
568 pub fn object_count(&self) -> Result<usize> {
569 let objects = self
570 .objects
571 .read()
572 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
573 Ok(objects.len())
574 }
575
576 #[allow(dead_code)]
578 pub fn get_root_cid(&self) -> Result<Option<Cid>> {
579 let root = self
580 .root_cid
581 .read()
582 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
583 Ok(root.clone())
584 }
585
586 #[allow(dead_code)]
588 pub fn default_branch(&self) -> Result<Option<String>> {
589 let refs = self
590 .refs
591 .read()
592 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
593
594 if let Some(head) = refs.get("HEAD") {
595 if let Some(target) = head.strip_prefix("ref: ") {
596 return Ok(Some(target.to_string()));
597 }
598 }
599 Ok(None)
600 }
601
602 fn get_commit_tree(
604 &self,
605 commit_oid: &str,
606 objects: &HashMap<String, Vec<u8>>,
607 ) -> Option<String> {
608 let compressed = objects.get(commit_oid)?;
609
610 let mut decoder = ZlibDecoder::new(&compressed[..]);
612 let mut decompressed = Vec::new();
613 decoder.read_to_end(&mut decompressed).ok()?;
614
615 let null_pos = decompressed.iter().position(|&b| b == 0)?;
617 let content = &decompressed[null_pos + 1..];
618
619 let content_str = std::str::from_utf8(content).ok()?;
621 let first_line = content_str.lines().next()?;
622 first_line
623 .strip_prefix("tree ")
624 .map(|tree_hash| tree_hash.to_string())
625 }
626
627 fn get_object_content(
629 &self,
630 oid: &str,
631 objects: &HashMap<String, Vec<u8>>,
632 ) -> Option<(ObjectType, Vec<u8>)> {
633 let compressed = objects.get(oid)?;
634 Self::parse_compressed_object(compressed)
635 }
636
637 fn parse_compressed_object(compressed: &[u8]) -> Option<(ObjectType, Vec<u8>)> {
638 let mut decoder = ZlibDecoder::new(compressed);
639 let mut decompressed = Vec::new();
640 decoder.read_to_end(&mut decompressed).ok()?;
641
642 let null_pos = decompressed.iter().position(|&b| b == 0)?;
644 let header = std::str::from_utf8(&decompressed[..null_pos]).ok()?;
645 let obj_type = if header.starts_with("blob") {
646 ObjectType::Blob
647 } else if header.starts_with("tree") {
648 ObjectType::Tree
649 } else if header.starts_with("commit") {
650 ObjectType::Commit
651 } else if header.starts_with("tag") {
652 ObjectType::Tag
653 } else {
654 return None;
655 };
656 let content = decompressed[null_pos + 1..].to_vec();
657 Some((obj_type, content))
658 }
659
660 async fn load_base_compressed_object<S: Store>(
661 &self,
662 oid: &str,
663 base_tree: &HashTree<S>,
664 base_objects_cid: &Cid,
665 ) -> Result<Option<Vec<u8>>> {
666 if oid.len() != 40 {
667 return Err(Error::ObjectNotFound(oid.to_string()));
668 }
669
670 let path = format!("{}/{}", &oid[..2], &oid[2..]);
671 let Some(object_cid) = base_tree
672 .resolve_path(base_objects_cid, &path)
673 .await
674 .map_err(|e| Error::StorageError(format!("resolve {} in base objects: {}", oid, e)))?
675 else {
676 return Ok(None);
677 };
678
679 base_tree
680 .get(&object_cid, None)
681 .await
682 .map_err(|e| Error::StorageError(format!("read {} from base objects: {}", oid, e)))
683 }
684
685 async fn get_object_content_from_base<S: Store>(
686 &self,
687 oid: &str,
688 base_tree: &HashTree<S>,
689 base_objects_cid: &Cid,
690 ) -> Result<Option<(ObjectType, Vec<u8>)>> {
691 let Some(compressed) = self
692 .load_base_compressed_object(oid, base_tree, base_objects_cid)
693 .await?
694 else {
695 return Ok(None);
696 };
697
698 Ok(Self::parse_compressed_object(&compressed))
699 }
700
701 async fn get_tree_entries_from_sources<S: Store>(
702 &self,
703 tree_oid: &str,
704 objects: &HashMap<String, Vec<u8>>,
705 base_tree: Option<&HashTree<S>>,
706 base_objects_cid: Option<&Cid>,
707 ) -> Result<Vec<super::object::TreeEntry>> {
708 let object = match self.get_object_content(tree_oid, objects) {
709 Some(object) => Some(object),
710 None => {
711 if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
712 self.get_object_content_from_base(tree_oid, base_tree, base_objects_cid)
713 .await?
714 } else {
715 None
716 }
717 }
718 }
719 .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
720
721 if object.0 != ObjectType::Tree {
722 return Err(Error::InvalidObjectType(format!(
723 "expected tree, got {:?}",
724 object.0
725 )));
726 }
727
728 parse_tree(&object.1)
729 }
730
731 fn tree_entry_to_dir_entry(entry: &hashtree_core::TreeEntry) -> DirEntry {
732 DirEntry::from_cid(
733 &entry.name,
734 &Cid {
735 hash: entry.hash,
736 key: entry.key,
737 },
738 )
739 .with_size(entry.size)
740 .with_link_type(entry.link_type)
741 }
742
743 async fn import_object_from_base<S: Store>(
744 &self,
745 oid: &str,
746 objects: &mut HashMap<String, Vec<u8>>,
747 base_tree: &HashTree<S>,
748 base_objects_cid: &Cid,
749 ) -> Result<bool> {
750 if objects.contains_key(oid) {
751 return Ok(true);
752 }
753 let Some(compressed) = self
754 .load_base_compressed_object(oid, base_tree, base_objects_cid)
755 .await?
756 else {
757 return Ok(false);
758 };
759
760 objects.insert(oid.to_string(), compressed);
761 Ok(true)
762 }
763
764 fn seed_missing_object_from_base_boxed<'a, S: Store + 'a>(
765 &'a self,
766 oid: &'a str,
767 objects: &'a mut HashMap<String, Vec<u8>>,
768 base_tree: &'a HashTree<S>,
769 base_objects_cid: &'a Cid,
770 ) -> BoxFuture<'a, Result<bool>> {
771 Box::pin(self.import_object_from_base(oid, objects, base_tree, base_objects_cid))
772 }
773
774 fn peel_tag_target(&self, oid: &str, objects: &HashMap<String, Vec<u8>>) -> Option<String> {
775 let (obj_type, content) = self.get_object_content(oid, objects)?;
776 if obj_type != ObjectType::Tag {
777 return Some(oid.to_string());
778 }
779
780 let target = std::str::from_utf8(&content)
781 .ok()?
782 .lines()
783 .find_map(|line| line.strip_prefix("object "))
784 .map(str::trim)?
785 .to_string();
786
787 match self.get_object_content(&target, objects)?.0 {
788 ObjectType::Tag => self.peel_tag_target(&target, objects),
789 _ => Some(target),
790 }
791 }
792
793 fn build_info_refs_content(
794 &self,
795 refs: &HashMap<String, String>,
796 objects: &HashMap<String, Vec<u8>>,
797 ) -> String {
798 let mut lines = Vec::new();
799
800 for (name, value) in refs {
801 if name == "HEAD" {
802 continue;
803 }
804
805 let oid = value.trim().to_string();
806 lines.push((name.clone(), oid.clone()));
807
808 if name.starts_with("refs/tags/") {
809 if let Some(peeled) = self.peel_tag_target(&oid, objects) {
810 if peeled != oid {
811 lines.push((format!("{}^{{}}", name), peeled));
812 }
813 }
814 }
815 }
816
817 lines.sort_by(|a, b| a.0.cmp(&b.0));
818
819 let mut content = String::new();
820 for (name, oid) in lines {
821 content.push_str(&oid);
822 content.push('\t');
823 content.push_str(&name);
824 content.push('\n');
825 }
826 content
827 }
828
829 async fn build_info_dir(
830 &self,
831 refs: &HashMap<String, String>,
832 objects: &HashMap<String, Vec<u8>>,
833 ) -> Result<Cid> {
834 let info_refs = self.build_info_refs_content(refs, objects);
835 let (info_refs_cid, info_refs_size) = self
836 .tree
837 .put(info_refs.as_bytes())
838 .await
839 .map_err(|e| Error::StorageError(format!("put info/refs: {}", e)))?;
840
841 self.tree
842 .put_directory(vec![
843 DirEntry::from_cid("refs", &info_refs_cid).with_size(info_refs_size)
844 ])
845 .await
846 .map_err(|e| Error::StorageError(format!("put info dir: {}", e)))
847 }
848
849 pub fn build_tree(&self) -> Result<Cid> {
851 self.build_tree_with_base_objects::<LocalStore>(None, None, None)
852 }
853
854 pub fn build_tree_with_progress(&self, progress: &RepoTreeBuildProgress) -> Result<Cid> {
855 self.build_tree_with_base_objects_internal::<LocalStore>(None, None, None, Some(progress))
856 }
857
858 fn is_safe_pack_name(name: &str) -> bool {
859 name.len() == "pack-".len() + 40 + ".pack".len()
860 && name.starts_with("pack-")
861 && name.ends_with(".pack")
862 && name["pack-".len()..name.len() - ".pack".len()]
863 .bytes()
864 .all(|byte| byte.is_ascii_hexdigit())
865 }
866
867 pub fn tree_root_has_git_pack_checkpoint<S: Store>(
868 &self,
869 tree: &HashTree<S>,
870 root_cid: &Cid,
871 ) -> Result<bool> {
872 let Some(info_packs_cid) = self
873 .runtime
874 .block_on(tree.resolve_path(root_cid, ".git/objects/info/packs"))
875 .map_err(|e| Error::StorageError(format!("resolve .git/objects/info/packs: {}", e)))?
876 else {
877 return Ok(false);
878 };
879
880 let Some(info_packs_bytes) = self
881 .runtime
882 .block_on(tree.get(&info_packs_cid, None))
883 .map_err(|e| Error::StorageError(format!("read .git/objects/info/packs: {}", e)))?
884 else {
885 return Ok(false);
886 };
887
888 let info_packs = String::from_utf8_lossy(&info_packs_bytes);
889 for line in info_packs.lines().map(str::trim) {
890 let Some(pack_name) = line.strip_prefix("P ") else {
891 continue;
892 };
893 if !Self::is_safe_pack_name(pack_name) {
894 continue;
895 }
896
897 let idx_name = format!("{}.idx", pack_name.trim_end_matches(".pack"));
898 let pack_path = format!(".git/objects/pack/{pack_name}");
899 let idx_path = format!(".git/objects/pack/{idx_name}");
900 let pack_exists = self
901 .runtime
902 .block_on(tree.resolve_path(root_cid, &pack_path))
903 .map_err(|e| Error::StorageError(format!("resolve {}: {}", pack_path, e)))?
904 .is_some();
905 let idx_exists = self
906 .runtime
907 .block_on(tree.resolve_path(root_cid, &idx_path))
908 .map_err(|e| Error::StorageError(format!("resolve {}: {}", idx_path, e)))?
909 .is_some();
910 if pack_exists && idx_exists {
911 return Ok(true);
912 }
913 }
914
915 Ok(false)
916 }
917
918 fn root_has_git_pack_checkpoint(&self, root_cid: &Cid) -> Result<bool> {
919 self.tree_root_has_git_pack_checkpoint(&self.tree, root_cid)
920 }
921
922 pub fn validate_root_contains_direct_refs(&self, root_cid: &Cid) -> Result<()> {
923 let direct_refs: Vec<String> = {
924 let refs = self
925 .refs
926 .read()
927 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
928 refs.values()
929 .filter(|value| {
930 !value.starts_with("ref: ")
931 && value.len() == 40
932 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
933 })
934 .cloned()
935 .collect()
936 };
937
938 if direct_refs.is_empty() {
939 return Ok(());
940 }
941
942 let objects_dir = self
943 .runtime
944 .block_on(self.tree.resolve_path(root_cid, ".git/objects"))
945 .map_err(|e| Error::StorageError(format!("resolve .git/objects: {}", e)))?;
946 if objects_dir.is_none() {
947 return Err(Error::StorageError(
948 "built root is missing .git/objects".to_string(),
949 ));
950 }
951
952 let pack_checkpoint_available = self.root_has_git_pack_checkpoint(root_cid)?;
953 for oid in direct_refs {
954 let object_path = format!(".git/objects/{}/{}", &oid[..2], &oid[2..]);
955 let object_cid = self
956 .runtime
957 .block_on(self.tree.resolve_path(root_cid, &object_path))
958 .map_err(|e| Error::StorageError(format!("resolve {}: {}", object_path, e)))?;
959 if object_cid.is_none() && !pack_checkpoint_available {
960 return Err(Error::ObjectNotFound(oid));
961 }
962 }
963
964 Ok(())
965 }
966
967 pub fn build_tree_with_base_objects<S: Store>(
968 &self,
969 base_tree: Option<&HashTree<S>>,
970 base_root: Option<&Cid>,
971 base_tree_sha: Option<&str>,
972 ) -> Result<Cid> {
973 self.build_tree_with_base_objects_internal(base_tree, base_root, base_tree_sha, None)
974 }
975
976 pub fn build_tree_with_base_objects_with_progress<S: Store>(
977 &self,
978 base_tree: Option<&HashTree<S>>,
979 base_root: Option<&Cid>,
980 base_tree_sha: Option<&str>,
981 progress: &RepoTreeBuildProgress,
982 ) -> Result<Cid> {
983 self.build_tree_with_base_objects_internal(
984 base_tree,
985 base_root,
986 base_tree_sha,
987 Some(progress),
988 )
989 }
990
991 fn build_tree_with_base_objects_internal<S: Store>(
992 &self,
993 base_tree: Option<&HashTree<S>>,
994 base_root: Option<&Cid>,
995 base_tree_sha: Option<&str>,
996 progress: Option<&RepoTreeBuildProgress>,
997 ) -> Result<Cid> {
998 if let Ok(root) = self.root_cid.read() {
1000 if let Some(ref cid) = *root {
1001 if let Some(progress) = progress {
1002 progress.mark_done();
1003 }
1004 return Ok(cid.clone());
1005 }
1006 }
1007
1008 if let Err(err) = self.evict_if_needed() {
1009 debug!("pre-build eviction skipped: {}", err);
1010 }
1011
1012 let objects = self
1013 .objects
1014 .read()
1015 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
1016 let refs = self
1017 .refs
1018 .read()
1019 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
1020
1021 let (default_branch, commit_sha) = if let Some(head) = refs.get("HEAD") {
1023 let branch = head.strip_prefix("ref: ").map(String::from);
1024 let sha = branch.as_ref().and_then(|b| refs.get(b)).cloned();
1025 (branch, sha)
1026 } else {
1027 let mut branch_info: Option<(String, String)> = None;
1029 for (ref_name, sha) in refs.iter() {
1030 if ref_name.starts_with("refs/heads/") {
1031 branch_info = Some((ref_name.clone(), sha.clone()));
1032 break;
1033 }
1034 }
1035 match branch_info {
1036 Some((branch, sha)) => (Some(branch), Some(sha)),
1037 None => (None, None),
1038 }
1039 };
1040
1041 let tree_sha = commit_sha
1043 .as_ref()
1044 .and_then(|sha| self.get_commit_tree(sha, &objects));
1045
1046 let mut objects_clone = objects.clone();
1048
1049 let base_objects_cid = if let (Some(base_tree), Some(base_root)) = (base_tree, base_root) {
1050 self.runtime
1051 .block_on(base_tree.resolve_path(base_root, ".git/objects"))
1052 .map_err(|e| Error::StorageError(format!("resolve base .git/objects: {}", e)))?
1053 } else {
1054 None
1055 };
1056
1057 let base_tree_sha = base_tree_sha.map(str::to_string);
1058
1059 let base_root_entries = if let (Some(base_tree), Some(base_root)) = (base_tree, base_root) {
1060 Some(
1061 self.runtime
1062 .block_on(base_tree.list_directory(base_root))
1063 .map_err(|e| Error::StorageError(format!("list base root: {}", e)))?,
1064 )
1065 } else {
1066 None
1067 };
1068
1069 let root_cid = loop {
1070 let build_result = self.runtime.block_on(async {
1071 let objects_cid = self
1073 .build_objects_dir_with_base(
1074 &objects_clone,
1075 base_tree,
1076 base_objects_cid.as_ref(),
1077 progress,
1078 )
1079 .await?;
1080
1081 if let Some(progress) = progress {
1083 progress.start_phase(RepoTreeBuildPhase::Refs, Some(refs.len()));
1084 }
1085 let refs_cid = self.build_refs_dir(&refs, progress).await?;
1086
1087 if let Some(progress) = progress {
1089 progress.start_phase(RepoTreeBuildPhase::Info, Some(1));
1090 }
1091 let info_cid = self.build_info_dir(&refs, &objects_clone).await?;
1092 if let Some(progress) = progress {
1093 progress.increment_processed();
1094 }
1095
1096 if let Some(progress) = progress {
1099 progress.start_phase(RepoTreeBuildPhase::Head, Some(1));
1100 }
1101 let head_content = refs.get("HEAD")
1102 .map(|h| if h.ends_with('\n') { h.clone() } else { format!("{}\n", h) })
1103 .or_else(|| default_branch.as_ref().map(|b| format!("ref: {}\n", b)))
1104 .unwrap_or_else(|| "ref: refs/heads/main\n".to_string());
1105 debug!("HEAD content: {:?}", head_content);
1106 let (head_cid, head_size) = self.tree.put(head_content.as_bytes()).await
1107 .map_err(|e| Error::StorageError(format!("put HEAD: {}", e)))?;
1108 debug!("HEAD hash: {}", hex::encode(head_cid.hash));
1109 if let Some(progress) = progress {
1110 progress.increment_processed();
1111 }
1112
1113 let mut git_entries = vec![
1115 DirEntry::from_cid("HEAD", &head_cid).with_size(head_size),
1116 DirEntry::from_cid("info", &info_cid).with_link_type(LinkType::Dir),
1117 DirEntry::from_cid("objects", &objects_cid).with_link_type(LinkType::Dir),
1118 DirEntry::from_cid("refs", &refs_cid).with_link_type(LinkType::Dir),
1119 ];
1120
1121 if let Some(ref branch) = default_branch {
1123 let config = format!(
1124 "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n[init]\n\tdefaultBranch = {}\n",
1125 branch.trim_start_matches("refs/heads/")
1126 );
1127 let (config_cid, config_size) = self.tree.put(config.as_bytes()).await
1128 .map_err(|e| Error::StorageError(format!("put config: {}", e)))?;
1129 git_entries.push(DirEntry::from_cid("config", &config_cid).with_size(config_size));
1130 }
1131
1132 if let Some(ref tree_oid) = tree_sha {
1134 if let Some(progress) = progress {
1135 progress.start_phase(RepoTreeBuildPhase::Index, None);
1136 }
1137 let index_result = if let (Some(base_tree), Some(base_objects_cid), Some(base_tree_sha)) =
1138 (base_tree, base_objects_cid.as_ref(), base_tree_sha.as_deref())
1139 {
1140 self.build_index_file_with_base(
1141 tree_oid,
1142 &objects_clone,
1143 base_tree,
1144 base_objects_cid,
1145 base_tree_sha,
1146 base_root_entries.as_ref(),
1147 progress,
1148 )
1149 .await
1150 } else {
1151 self.build_index_file(tree_oid, &objects_clone, progress)
1152 };
1153 match index_result {
1154 Ok(index_data) => {
1155 let (index_cid, index_size) = self.tree.put(&index_data).await
1156 .map_err(|e| Error::StorageError(format!("put index: {}", e)))?;
1157 git_entries.push(DirEntry::from_cid("index", &index_cid).with_size(index_size));
1158 info!("Added git index file ({} bytes)", index_data.len());
1159 }
1160 Err(e) => {
1161 debug!("Failed to build git index file: {} - continuing without index", e);
1162 }
1163 }
1164 }
1165
1166 if let Some(progress) = progress {
1167 progress.start_phase(RepoTreeBuildPhase::GitDir, Some(1));
1168 }
1169 let git_cid = self.tree.put_directory(git_entries).await
1170 .map_err(|e| Error::StorageError(format!("put .git: {}", e)))?;
1171 if let Some(progress) = progress {
1172 progress.increment_processed();
1173 }
1174
1175 let mut root_entries = vec![DirEntry::from_cid(".git", &git_cid).with_link_type(LinkType::Dir)];
1178
1179 if let Some(ref tree_oid) = tree_sha {
1181 if let Some(progress) = progress {
1182 progress.start_phase(RepoTreeBuildPhase::WorkingTree, None);
1183 }
1184 let working_tree_entries = if let (Some(base_tree), Some(base_objects_cid), Some(base_tree_sha)) =
1185 (base_tree, base_objects_cid.as_ref(), base_tree_sha.as_deref())
1186 {
1187 self.build_working_tree_entries_with_base(
1188 tree_oid,
1189 &objects_clone,
1190 base_tree,
1191 base_objects_cid,
1192 base_tree_sha,
1193 base_root_entries.as_ref(),
1194 progress,
1195 )
1196 .await?
1197 } else {
1198 self.build_working_tree_entries(tree_oid, &objects_clone, progress).await?
1199 };
1200 root_entries.extend(working_tree_entries);
1201 info!("Added {} working tree entries to root", root_entries.len() - 1);
1202 }
1203
1204 root_entries.sort_by(|a, b| a.name.cmp(&b.name));
1206
1207 if let Some(progress) = progress {
1208 progress.start_phase(RepoTreeBuildPhase::Root, Some(1));
1209 }
1210 let root_cid = self.tree.put_directory(root_entries).await
1211 .map_err(|e| Error::StorageError(format!("put root: {}", e)))?;
1212 if let Some(progress) = progress {
1213 progress.increment_processed();
1214 }
1215
1216 info!("Built hashtree root: {} (encrypted: {}) (.git dir: {})",
1217 hex::encode(root_cid.hash),
1218 root_cid.key.is_some(),
1219 hex::encode(git_cid.hash));
1220
1221 Ok::<Cid, Error>(root_cid)
1222 });
1223
1224 match build_result {
1225 Ok(root_cid) => break root_cid,
1226 Err(Error::ObjectNotFound(oid))
1227 if base_tree.is_some() && base_objects_cid.is_some() =>
1228 {
1229 let imported =
1230 self.runtime
1231 .block_on(self.seed_missing_object_from_base_boxed(
1232 &oid,
1233 &mut objects_clone,
1234 base_tree.expect("checked is_some"),
1235 base_objects_cid.as_ref().expect("checked is_some"),
1236 ))?;
1237 if imported {
1238 continue;
1239 }
1240 return Err(Error::ObjectNotFound(oid).into());
1241 }
1242 Err(err) => return Err(err.into()),
1243 }
1244 };
1245
1246 if let Ok(mut root) = self.root_cid.write() {
1248 *root = Some(root_cid.clone());
1249 }
1250 if let Some(progress) = progress {
1251 progress.mark_done();
1252 }
1253
1254 Ok(root_cid)
1255 }
1256
1257 async fn build_working_tree_entries(
1259 &self,
1260 tree_oid: &str,
1261 objects: &HashMap<String, Vec<u8>>,
1262 progress: Option<&RepoTreeBuildProgress>,
1263 ) -> Result<Vec<DirEntry>> {
1264 let mut entries = Vec::new();
1265
1266 let (obj_type, content) = self
1268 .get_object_content(tree_oid, objects)
1269 .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
1270
1271 if obj_type != ObjectType::Tree {
1272 return Err(Error::InvalidObjectType(format!(
1273 "expected tree, got {:?}",
1274 obj_type
1275 )));
1276 }
1277
1278 let tree_entries = parse_tree(&content)?;
1280
1281 for entry in tree_entries {
1282 let oid_hex = entry.oid.to_hex();
1283
1284 if entry.is_tree() {
1285 let sub_entries = self
1287 .build_working_tree_entries_boxed(&oid_hex, objects, progress)
1288 .await?;
1289
1290 let dir_cid =
1292 self.tree.put_directory(sub_entries).await.map_err(|e| {
1293 Error::StorageError(format!("put dir {}: {}", entry.name, e))
1294 })?;
1295
1296 entries
1298 .push(DirEntry::from_cid(&entry.name, &dir_cid).with_link_type(LinkType::Dir));
1299 if let Some(progress) = progress {
1300 progress.record_working_dir(false);
1301 }
1302 } else {
1303 if let Some((ObjectType::Blob, blob_content)) =
1305 self.get_object_content(&oid_hex, objects)
1306 {
1307 let (cid, size) = self.tree.put(&blob_content).await.map_err(|e| {
1309 Error::StorageError(format!("put blob {}: {}", entry.name, e))
1310 })?;
1311
1312 entries.push(DirEntry::from_cid(&entry.name, &cid).with_size(size));
1314 if let Some(progress) = progress {
1315 progress.record_working_file(false);
1316 }
1317 }
1318 }
1319 }
1320
1321 entries.sort_by(|a, b| a.name.cmp(&b.name));
1323
1324 Ok(entries)
1325 }
1326
1327 fn build_working_tree_entries_boxed<'a>(
1329 &'a self,
1330 tree_oid: &'a str,
1331 objects: &'a HashMap<String, Vec<u8>>,
1332 progress: Option<&'a RepoTreeBuildProgress>,
1333 ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1334 Box::pin(self.build_working_tree_entries(tree_oid, objects, progress))
1335 }
1336
1337 fn build_working_tree_entries_with_base_recursive_boxed<'a, S: Store + 'a>(
1338 &'a self,
1339 tree_oid: &'a str,
1340 objects: &'a HashMap<String, Vec<u8>>,
1341 base_tree: &'a HashTree<S>,
1342 base_objects_cid: &'a Cid,
1343 old_tree_oid: Option<&'a str>,
1344 old_dir_entries: Option<&'a Vec<hashtree_core::TreeEntry>>,
1345 progress: Option<&'a RepoTreeBuildProgress>,
1346 ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1347 Box::pin(async move {
1348 let tree_entries = self
1349 .get_tree_entries_from_sources(
1350 tree_oid,
1351 objects,
1352 Some(base_tree),
1353 Some(base_objects_cid),
1354 )
1355 .await?;
1356 let old_tree_entries = if let Some(old_tree_oid) = old_tree_oid {
1357 match self
1358 .get_tree_entries_from_sources(
1359 old_tree_oid,
1360 objects,
1361 Some(base_tree),
1362 Some(base_objects_cid),
1363 )
1364 .await
1365 {
1366 Ok(entries) => Some(entries),
1367 Err(Error::ObjectNotFound(_)) if old_dir_entries.is_some() => None,
1368 Err(err) => return Err(err),
1369 }
1370 } else {
1371 None
1372 };
1373 let old_tree_available = old_tree_entries.is_some();
1374
1375 let mut old_tree_entry_map = old_tree_entries
1376 .unwrap_or_default()
1377 .into_iter()
1378 .map(|entry| (entry.name.clone(), entry))
1379 .collect::<HashMap<_, _>>();
1380 let mut old_dir_entry_map = old_dir_entries
1381 .cloned()
1382 .unwrap_or_default()
1383 .into_iter()
1384 .map(|entry| (entry.name.clone(), entry))
1385 .collect::<HashMap<_, _>>();
1386
1387 let mut entries = Vec::new();
1388 for entry in tree_entries {
1389 let oid_hex = entry.oid.to_hex();
1390 let old_tree_entry = old_tree_entry_map.remove(&entry.name);
1391 let old_dir_entry = old_dir_entry_map.remove(&entry.name);
1392
1393 if entry.is_tree() {
1394 if let (Some(old_tree_entry), Some(old_dir_entry)) =
1395 (old_tree_entry.as_ref(), old_dir_entry.as_ref())
1396 {
1397 if old_tree_entry.is_tree()
1398 && old_tree_entry.oid == entry.oid
1399 && old_dir_entry.link_type == LinkType::Dir
1400 {
1401 entries.push(Self::tree_entry_to_dir_entry(old_dir_entry));
1402 if let Some(progress) = progress {
1403 progress.record_working_dir(true);
1404 }
1405 continue;
1406 }
1407 }
1408
1409 let old_subtree_oid = old_tree_entry
1410 .as_ref()
1411 .filter(|old| old.is_tree())
1412 .map(|old| old.oid.to_hex());
1413 let old_subdir_entries = if let Some(old_dir_entry) = old_dir_entry.as_ref() {
1414 if old_dir_entry.link_type == LinkType::Dir {
1415 Some(
1416 base_tree
1417 .list_directory(&Cid {
1418 hash: old_dir_entry.hash,
1419 key: old_dir_entry.key,
1420 })
1421 .await
1422 .map_err(|e| {
1423 Error::StorageError(format!(
1424 "list base working dir {}: {}",
1425 entry.name, e
1426 ))
1427 })?,
1428 )
1429 } else {
1430 None
1431 }
1432 } else {
1433 None
1434 };
1435
1436 let sub_entries = self
1437 .build_working_tree_entries_with_base_recursive_boxed(
1438 &oid_hex,
1439 objects,
1440 base_tree,
1441 base_objects_cid,
1442 old_subtree_oid.as_deref(),
1443 old_subdir_entries.as_ref(),
1444 progress,
1445 )
1446 .await?;
1447 let dir_cid = self.tree.put_directory(sub_entries).await.map_err(|e| {
1448 Error::StorageError(format!("put dir {}: {}", entry.name, e))
1449 })?;
1450 entries.push(
1451 DirEntry::from_cid(&entry.name, &dir_cid).with_link_type(LinkType::Dir),
1452 );
1453 if let Some(progress) = progress {
1454 progress.record_working_dir(false);
1455 }
1456 continue;
1457 }
1458
1459 if let (Some(old_tree_entry), Some(old_dir_entry)) =
1460 (old_tree_entry.as_ref(), old_dir_entry.as_ref())
1461 {
1462 if !old_tree_entry.is_tree()
1463 && old_tree_entry.oid == entry.oid
1464 && old_dir_entry.link_type != LinkType::Dir
1465 {
1466 entries.push(Self::tree_entry_to_dir_entry(old_dir_entry));
1467 if let Some(progress) = progress {
1468 progress.record_working_file(true);
1469 }
1470 continue;
1471 }
1472 }
1473
1474 let blob_content = match self.get_object_content(&oid_hex, objects) {
1475 Some((ObjectType::Blob, blob_content)) => blob_content,
1476 Some((obj_type, _)) => {
1477 return Err(Error::InvalidObjectType(format!(
1478 "expected blob, got {:?}",
1479 obj_type
1480 )));
1481 }
1482 None if !old_tree_available => {
1483 if let Some(old_dir_entry) = old_dir_entry.as_ref() {
1484 if old_dir_entry.link_type != LinkType::Dir {
1485 entries.push(Self::tree_entry_to_dir_entry(old_dir_entry));
1486 if let Some(progress) = progress {
1487 progress.record_working_file(true);
1488 }
1489 continue;
1490 }
1491 }
1492 return Err(Error::ObjectNotFound(oid_hex));
1493 }
1494 None => match self
1495 .get_object_content_from_base(&oid_hex, base_tree, base_objects_cid)
1496 .await?
1497 {
1498 Some((ObjectType::Blob, blob_content)) => blob_content,
1499 Some((obj_type, _)) => {
1500 return Err(Error::InvalidObjectType(format!(
1501 "expected blob, got {:?}",
1502 obj_type
1503 )));
1504 }
1505 None => return Err(Error::ObjectNotFound(oid_hex)),
1506 },
1507 };
1508
1509 let (cid, size) =
1510 self.tree.put(&blob_content).await.map_err(|e| {
1511 Error::StorageError(format!("put blob {}: {}", entry.name, e))
1512 })?;
1513 entries.push(DirEntry::from_cid(&entry.name, &cid).with_size(size));
1514 if let Some(progress) = progress {
1515 progress.record_working_file(false);
1516 }
1517 }
1518
1519 entries.sort_by(|a, b| a.name.cmp(&b.name));
1520 Ok(entries)
1521 })
1522 }
1523
1524 async fn build_working_tree_entries_with_base<S: Store>(
1525 &self,
1526 tree_oid: &str,
1527 objects: &HashMap<String, Vec<u8>>,
1528 base_tree: &HashTree<S>,
1529 base_objects_cid: &Cid,
1530 base_tree_oid: &str,
1531 base_root_entries: Option<&Vec<hashtree_core::TreeEntry>>,
1532 progress: Option<&RepoTreeBuildProgress>,
1533 ) -> Result<Vec<DirEntry>> {
1534 let root_entries = base_root_entries.map(|entries| {
1535 entries
1536 .iter()
1537 .filter(|entry| entry.name != ".git")
1538 .cloned()
1539 .collect::<Vec<_>>()
1540 });
1541
1542 self.build_working_tree_entries_with_base_recursive_boxed(
1543 tree_oid,
1544 objects,
1545 base_tree,
1546 base_objects_cid,
1547 Some(base_tree_oid),
1548 root_entries.as_ref(),
1549 progress,
1550 )
1551 .await
1552 }
1553
1554 async fn build_objects_prefix_dir(
1555 &self,
1556 prefix: &str,
1557 old_entries: Option<Vec<hashtree_core::TreeEntry>>,
1558 new_objects: &[(String, Vec<u8>)],
1559 progress: Option<&RepoTreeBuildProgress>,
1560 ) -> Result<Cid> {
1561 let mut sub_entries: BTreeMap<String, DirEntry> = old_entries
1562 .unwrap_or_default()
1563 .into_iter()
1564 .map(|entry| (entry.name.clone(), Self::tree_entry_to_dir_entry(&entry)))
1565 .collect();
1566
1567 for (suffix, data) in new_objects {
1568 let (cid, size) = self.tree.put(data).await.map_err(|e| {
1569 Error::StorageError(format!("put object {}{}: {}", prefix, suffix, e))
1570 })?;
1571 sub_entries.insert(
1572 suffix.clone(),
1573 DirEntry::from_cid(suffix, &cid).with_size(size),
1574 );
1575 if let Some(progress) = progress {
1576 progress.record_object_blob();
1577 }
1578 }
1579
1580 let cid = self
1581 .tree
1582 .put_directory(sub_entries.into_values().collect())
1583 .await
1584 .map_err(|e| Error::StorageError(format!("put objects/{}: {}", prefix, e)))?;
1585 if let Some(progress) = progress {
1586 progress.record_object_prefix();
1587 }
1588 Ok(cid)
1589 }
1590
1591 async fn build_objects_dir_with_base<S: Store>(
1594 &self,
1595 objects: &HashMap<String, Vec<u8>>,
1596 base_tree: Option<&HashTree<S>>,
1597 base_objects_cid: Option<&Cid>,
1598 progress: Option<&RepoTreeBuildProgress>,
1599 ) -> Result<Cid> {
1600 if let Some(progress) = progress {
1601 progress.start_phase(RepoTreeBuildPhase::Objects, Some(objects.len()));
1602 }
1603
1604 let pack_files = self
1605 .pack_files
1606 .read()
1607 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?
1608 .clone();
1609 let packed_object_ids = self
1610 .packed_object_ids
1611 .read()
1612 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?
1613 .clone();
1614 let packs_replace_loose_objects =
1615 !packed_object_ids.is_empty() && (!pack_files.is_empty() || base_objects_cid.is_some());
1616 let mut buckets: BTreeMap<String, Vec<(String, Vec<u8>)>> = BTreeMap::new();
1617 for (oid, data) in objects {
1618 if packs_replace_loose_objects && packed_object_ids.contains(oid) {
1619 continue;
1620 }
1621 let prefix = &oid[..2];
1622 let suffix = &oid[2..];
1623 buckets
1624 .entry(prefix.to_string())
1625 .or_default()
1626 .push((suffix.to_string(), data.clone()));
1627 }
1628
1629 let mut top_entries: BTreeMap<String, DirEntry> = BTreeMap::new();
1630 let mut merged_prefixes = std::collections::HashSet::new();
1631 let mut inherited_pack_entries: BTreeMap<String, DirEntry> = BTreeMap::new();
1632 let empty_objects: Vec<(String, Vec<u8>)> = Vec::new();
1633
1634 if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
1635 for entry in base_tree
1636 .list_directory(base_objects_cid)
1637 .await
1638 .map_err(|e| Error::StorageError(format!("list base objects dir: {}", e)))?
1639 {
1640 if entry.name == "info" || entry.name == "pack" {
1641 if pack_files.is_empty() {
1642 top_entries
1643 .insert(entry.name.clone(), Self::tree_entry_to_dir_entry(&entry));
1644 } else if entry.name == "pack" && entry.link_type == LinkType::Dir {
1645 let pack_cid = Cid {
1646 hash: entry.hash,
1647 key: entry.key,
1648 };
1649 for pack_entry in
1650 base_tree.list_directory(&pack_cid).await.map_err(|e| {
1651 Error::StorageError(format!("list base objects/pack dir: {}", e))
1652 })?
1653 {
1654 inherited_pack_entries.insert(
1655 pack_entry.name.clone(),
1656 Self::tree_entry_to_dir_entry(&pack_entry),
1657 );
1658 }
1659 }
1660 continue;
1661 }
1662
1663 let Some(delta_objects) = buckets.get(&entry.name) else {
1664 if !packs_replace_loose_objects {
1665 top_entries
1666 .insert(entry.name.clone(), Self::tree_entry_to_dir_entry(&entry));
1667 continue;
1668 }
1669
1670 if entry.link_type != LinkType::Dir || entry.name.len() != 2 {
1671 top_entries
1672 .insert(entry.name.clone(), Self::tree_entry_to_dir_entry(&entry));
1673 continue;
1674 }
1675
1676 let prefix_cid = Cid {
1677 hash: entry.hash,
1678 key: entry.key,
1679 };
1680 let old_prefix_entries = base_tree
1681 .list_directory(&prefix_cid)
1682 .await
1683 .map_err(|e| {
1684 Error::StorageError(format!(
1685 "list base objects/{} dir: {}",
1686 entry.name, e
1687 ))
1688 })?
1689 .into_iter()
1690 .filter(|old_entry| {
1691 !packed_object_ids
1692 .contains(&format!("{}{}", entry.name, old_entry.name))
1693 })
1694 .collect::<Vec<_>>();
1695 if old_prefix_entries.is_empty() {
1696 continue;
1697 }
1698
1699 let merged_cid = self
1700 .build_objects_prefix_dir(
1701 &entry.name,
1702 Some(old_prefix_entries),
1703 &empty_objects,
1704 progress,
1705 )
1706 .await?;
1707 top_entries.insert(
1708 entry.name.clone(),
1709 DirEntry::from_cid(&entry.name, &merged_cid).with_link_type(LinkType::Dir),
1710 );
1711 merged_prefixes.insert(entry.name);
1712 continue;
1713 };
1714
1715 let prefix_cid = Cid {
1716 hash: entry.hash,
1717 key: entry.key,
1718 };
1719 let old_prefix_entries = if entry.link_type == LinkType::Dir {
1720 let mut entries = base_tree.list_directory(&prefix_cid).await.map_err(|e| {
1721 Error::StorageError(format!("list base objects/{} dir: {}", entry.name, e))
1722 })?;
1723 if packs_replace_loose_objects {
1724 entries.retain(|old_entry| {
1725 !packed_object_ids
1726 .contains(&format!("{}{}", entry.name, old_entry.name))
1727 });
1728 }
1729 Some(entries)
1730 } else {
1731 None
1732 };
1733 let merged_cid = self
1734 .build_objects_prefix_dir(
1735 &entry.name,
1736 old_prefix_entries,
1737 delta_objects,
1738 progress,
1739 )
1740 .await?;
1741 top_entries.insert(
1742 entry.name.clone(),
1743 DirEntry::from_cid(&entry.name, &merged_cid).with_link_type(LinkType::Dir),
1744 );
1745 merged_prefixes.insert(entry.name);
1746 }
1747 }
1748
1749 for (prefix, objs) in &buckets {
1750 if merged_prefixes.contains(prefix) {
1751 continue;
1752 }
1753 let sub_cid = self
1754 .build_objects_prefix_dir(prefix, None, objs, progress)
1755 .await?;
1756 top_entries.insert(
1757 prefix.clone(),
1758 DirEntry::from_cid(prefix, &sub_cid).with_link_type(LinkType::Dir),
1759 );
1760 }
1761
1762 if !pack_files.is_empty() {
1763 let mut pack_entries_by_name = inherited_pack_entries;
1764 for (name, data) in pack_files {
1765 let (cid, size) = self.tree.put(&data).await.map_err(|e| {
1766 Error::StorageError(format!("put objects/pack/{}: {}", name, e))
1767 })?;
1768 pack_entries_by_name.insert(
1769 name.clone(),
1770 DirEntry::from_cid(&name, &cid).with_size(size),
1771 );
1772 }
1773 let mut pack_names_for_info_packs = pack_entries_by_name
1774 .keys()
1775 .filter(|name| name.ends_with(".pack"))
1776 .cloned()
1777 .collect::<Vec<_>>();
1778 pack_names_for_info_packs.sort();
1779
1780 let pack_dir_cid = self
1781 .tree
1782 .put_directory(pack_entries_by_name.into_values().collect())
1783 .await
1784 .map_err(|e| Error::StorageError(format!("put objects/pack: {}", e)))?;
1785 top_entries.insert(
1786 "pack".to_string(),
1787 DirEntry::from_cid("pack", &pack_dir_cid).with_link_type(LinkType::Dir),
1788 );
1789
1790 let packs_content = pack_names_for_info_packs
1791 .iter()
1792 .map(|name| format!("P {}\n", name))
1793 .collect::<String>();
1794 let (packs_cid, packs_size) = self
1795 .tree
1796 .put(packs_content.as_bytes())
1797 .await
1798 .map_err(|e| Error::StorageError(format!("put objects/info/packs: {}", e)))?;
1799 let info_cid = self
1800 .tree
1801 .put_directory(vec![
1802 DirEntry::from_cid("packs", &packs_cid).with_size(packs_size)
1803 ])
1804 .await
1805 .map_err(|e| Error::StorageError(format!("put objects/info: {}", e)))?;
1806 top_entries.insert(
1807 "info".to_string(),
1808 DirEntry::from_cid("info", &info_cid).with_link_type(LinkType::Dir),
1809 );
1810 }
1811
1812 if !top_entries.contains_key("info") {
1813 let (packs_cid, packs_size) = self
1814 .tree
1815 .put(b"")
1816 .await
1817 .map_err(|e| Error::StorageError(format!("put objects/info/packs: {}", e)))?;
1818 let info_cid = self
1819 .tree
1820 .put_directory(vec![
1821 DirEntry::from_cid("packs", &packs_cid).with_size(packs_size)
1822 ])
1823 .await
1824 .map_err(|e| Error::StorageError(format!("put objects/info: {}", e)))?;
1825 top_entries.insert(
1826 "info".to_string(),
1827 DirEntry::from_cid("info", &info_cid).with_link_type(LinkType::Dir),
1828 );
1829 }
1830
1831 let entry_count = top_entries.len();
1832 let cid = self
1833 .tree
1834 .put_directory(top_entries.into_values().collect())
1835 .await
1836 .map_err(|e| Error::StorageError(format!("put objects dir: {}", e)))?;
1837
1838 debug!(
1839 "Built objects dir with {} entries: {}",
1840 entry_count,
1841 hex::encode(cid.hash)
1842 );
1843 Ok(cid)
1844 }
1845
1846 async fn build_refs_dir(
1848 &self,
1849 refs: &HashMap<String, String>,
1850 progress: Option<&RepoTreeBuildProgress>,
1851 ) -> Result<Cid> {
1852 let mut root = RefDirectory::default();
1853
1854 for (ref_name, value) in refs {
1855 let parts: Vec<&str> = ref_name.split('/').collect();
1856 if parts.len() >= 3 && parts[0] == "refs" {
1857 root.insert(&parts[1..], value.clone());
1858 }
1859 }
1860
1861 let mut ref_entries = self
1862 .build_ref_entries_recursive(&root, "refs", progress)
1863 .await?;
1864
1865 if ref_entries.is_empty() {
1866 let empty_cid = self
1868 .tree
1869 .put_directory(vec![])
1870 .await
1871 .map_err(|e| Error::StorageError(format!("put empty refs: {}", e)))?;
1872 return Ok(empty_cid);
1873 }
1874
1875 ref_entries.sort_by(|a, b| a.name.cmp(&b.name));
1876
1877 let refs_cid = self
1878 .tree
1879 .put_directory(ref_entries)
1880 .await
1881 .map_err(|e| Error::StorageError(format!("put refs dir: {}", e)))?;
1882 debug!("refs dir -> {}", hex::encode(refs_cid.hash));
1883 Ok(refs_cid)
1884 }
1885
1886 fn build_ref_entries_recursive<'a>(
1887 &'a self,
1888 dir: &'a RefDirectory,
1889 prefix: &'a str,
1890 progress: Option<&'a RepoTreeBuildProgress>,
1891 ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1892 Box::pin(async move {
1893 let mut entries = Vec::new();
1894
1895 for (name, value) in &dir.files {
1896 let (cid, size) = self
1897 .tree
1898 .put(value.as_bytes())
1899 .await
1900 .map_err(|e| Error::StorageError(format!("put ref: {}", e)))?;
1901 debug!("{}/{} -> blob {}", prefix, name, hex::encode(cid.hash));
1902 entries.push(DirEntry::from_cid(name, &cid).with_size(size));
1903 if let Some(progress) = progress {
1904 progress.increment_processed();
1905 }
1906 }
1907
1908 for (name, child) in &dir.dirs {
1909 let child_prefix = format!("{prefix}/{name}");
1910 let child_entries = self
1911 .build_ref_entries_recursive(child, &child_prefix, progress)
1912 .await?;
1913 let child_cid =
1914 self.tree.put_directory(child_entries).await.map_err(|e| {
1915 Error::StorageError(format!("put {child_prefix} dir: {}", e))
1916 })?;
1917 debug!("{} dir -> {}", child_prefix, hex::encode(child_cid.hash));
1918 entries.push(DirEntry::from_cid(name, &child_cid).with_link_type(LinkType::Dir));
1919 }
1920
1921 entries.sort_by(|a, b| a.name.cmp(&b.name));
1922 Ok(entries)
1923 })
1924 }
1925
1926 fn build_index_file(
1929 &self,
1930 tree_oid: &str,
1931 objects: &HashMap<String, Vec<u8>>,
1932 progress: Option<&RepoTreeBuildProgress>,
1933 ) -> Result<Vec<u8>> {
1934 let mut entries: Vec<(String, [u8; 20], u32, u32)> = Vec::new(); self.collect_tree_entries_for_index(tree_oid, objects, "", &mut entries, progress)?;
1937
1938 self.build_index_bytes(entries)
1939 }
1940
1941 async fn build_index_file_with_base<S: Store>(
1942 &self,
1943 tree_oid: &str,
1944 objects: &HashMap<String, Vec<u8>>,
1945 base_tree: &HashTree<S>,
1946 base_objects_cid: &Cid,
1947 base_tree_oid: &str,
1948 base_root_entries: Option<&Vec<hashtree_core::TreeEntry>>,
1949 progress: Option<&RepoTreeBuildProgress>,
1950 ) -> Result<Vec<u8>> {
1951 let mut entries: Vec<(String, [u8; 20], u32, u32)> = Vec::new();
1952 let root_entries = base_root_entries.map(|entries| {
1953 entries
1954 .iter()
1955 .filter(|entry| entry.name != ".git")
1956 .cloned()
1957 .collect::<Vec<_>>()
1958 });
1959
1960 self.collect_tree_entries_for_index_with_base_boxed(
1961 tree_oid,
1962 objects,
1963 base_tree,
1964 base_objects_cid,
1965 Some(base_tree_oid),
1966 root_entries.as_ref(),
1967 "",
1968 &mut entries,
1969 progress,
1970 )
1971 .await?;
1972
1973 self.build_index_bytes(entries)
1974 }
1975
1976 fn build_index_bytes(&self, mut entries: Vec<(String, [u8; 20], u32, u32)>) -> Result<Vec<u8>> {
1977 entries.sort_by(|a, b| a.0.cmp(&b.0));
1979
1980 let entry_count = entries.len() as u32;
1981 debug!("Building git index with {} entries", entry_count);
1982
1983 let mut index_data = Vec::new();
1985
1986 index_data.extend_from_slice(b"DIRC");
1988 index_data.extend_from_slice(&2u32.to_be_bytes()); index_data.extend_from_slice(&entry_count.to_be_bytes());
1990
1991 let now_sec = std::time::SystemTime::now()
1993 .duration_since(std::time::UNIX_EPOCH)
1994 .unwrap_or_default()
1995 .as_secs() as u32;
1996
1997 for (path, sha1, mode, size) in &entries {
1998 let entry_start = index_data.len();
1999
2000 index_data.extend_from_slice(&now_sec.to_be_bytes());
2002 index_data.extend_from_slice(&0u32.to_be_bytes());
2003 index_data.extend_from_slice(&now_sec.to_be_bytes());
2005 index_data.extend_from_slice(&0u32.to_be_bytes());
2006 index_data.extend_from_slice(&0u32.to_be_bytes());
2008 index_data.extend_from_slice(&0u32.to_be_bytes());
2009 index_data.extend_from_slice(&mode.to_be_bytes());
2011 index_data.extend_from_slice(&0u32.to_be_bytes());
2013 index_data.extend_from_slice(&0u32.to_be_bytes());
2014 index_data.extend_from_slice(&size.to_be_bytes());
2016 index_data.extend_from_slice(sha1);
2018 let path_len = std::cmp::min(path.len(), 0xFFF) as u16;
2020 index_data.extend_from_slice(&path_len.to_be_bytes());
2021 index_data.extend_from_slice(path.as_bytes());
2023 index_data.push(0); let entry_len = index_data.len() - entry_start;
2027 let padding = (8 - (entry_len % 8)) % 8;
2028 index_data.extend(std::iter::repeat_n(0, padding));
2029 }
2030
2031 let mut hasher = Sha1::new();
2033 hasher.update(&index_data);
2034 let checksum = hasher.finalize();
2035 index_data.extend_from_slice(&checksum);
2036
2037 debug!(
2038 "Built git index: {} bytes, {} entries",
2039 index_data.len(),
2040 entry_count
2041 );
2042 Ok(index_data)
2043 }
2044
2045 fn collect_tree_entries_for_index(
2047 &self,
2048 tree_oid: &str,
2049 objects: &HashMap<String, Vec<u8>>,
2050 prefix: &str,
2051 entries: &mut Vec<(String, [u8; 20], u32, u32)>,
2052 progress: Option<&RepoTreeBuildProgress>,
2053 ) -> Result<()> {
2054 let (obj_type, content) = self
2055 .get_object_content(tree_oid, objects)
2056 .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
2057
2058 if obj_type != ObjectType::Tree {
2059 return Err(Error::InvalidObjectType(format!(
2060 "expected tree, got {:?}",
2061 obj_type
2062 )));
2063 }
2064
2065 let tree_entries = parse_tree(&content)?;
2066
2067 for entry in tree_entries {
2068 let path = if prefix.is_empty() {
2069 entry.name.clone()
2070 } else {
2071 format!("{}/{}", prefix, entry.name)
2072 };
2073
2074 let oid_hex = entry.oid.to_hex();
2075
2076 if entry.is_tree() {
2077 self.collect_tree_entries_for_index(&oid_hex, objects, &path, entries, progress)?;
2079 } else {
2080 if let Some((ObjectType::Blob, blob_content)) =
2082 self.get_object_content(&oid_hex, objects)
2083 {
2084 let mut sha1_bytes = [0u8; 20];
2086 if let Ok(bytes) = hex::decode(&oid_hex) {
2087 if bytes.len() == 20 {
2088 sha1_bytes.copy_from_slice(&bytes);
2089 }
2090 }
2091
2092 let mode = entry.mode;
2094 let size = blob_content.len() as u32;
2095
2096 entries.push((path, sha1_bytes, mode, size));
2097 if let Some(progress) = progress {
2098 progress.record_index_entry();
2099 }
2100 }
2101 }
2102 }
2103
2104 Ok(())
2105 }
2106
2107 fn collect_tree_entries_for_index_with_base_boxed<'a, S: Store + 'a>(
2108 &'a self,
2109 tree_oid: &'a str,
2110 objects: &'a HashMap<String, Vec<u8>>,
2111 base_tree: &'a HashTree<S>,
2112 base_objects_cid: &'a Cid,
2113 old_tree_oid: Option<&'a str>,
2114 old_dir_entries: Option<&'a Vec<hashtree_core::TreeEntry>>,
2115 prefix: &'a str,
2116 entries: &'a mut Vec<(String, [u8; 20], u32, u32)>,
2117 progress: Option<&'a RepoTreeBuildProgress>,
2118 ) -> BoxFuture<'a, Result<()>> {
2119 Box::pin(async move {
2120 let tree_entries = self
2121 .get_tree_entries_from_sources(
2122 tree_oid,
2123 objects,
2124 Some(base_tree),
2125 Some(base_objects_cid),
2126 )
2127 .await?;
2128 let old_tree_entries = if let Some(old_tree_oid) = old_tree_oid {
2129 Some(
2130 self.get_tree_entries_from_sources(
2131 old_tree_oid,
2132 objects,
2133 Some(base_tree),
2134 Some(base_objects_cid),
2135 )
2136 .await?,
2137 )
2138 } else {
2139 None
2140 };
2141 let mut old_tree_entry_map = old_tree_entries
2142 .unwrap_or_default()
2143 .into_iter()
2144 .map(|entry| (entry.name.clone(), entry))
2145 .collect::<HashMap<_, _>>();
2146 let mut old_dir_entry_map = old_dir_entries
2147 .cloned()
2148 .unwrap_or_default()
2149 .into_iter()
2150 .map(|entry| (entry.name.clone(), entry))
2151 .collect::<HashMap<_, _>>();
2152
2153 for entry in tree_entries {
2154 let path = if prefix.is_empty() {
2155 entry.name.clone()
2156 } else {
2157 format!("{}/{}", prefix, entry.name)
2158 };
2159 let oid_hex = entry.oid.to_hex();
2160 let old_tree_entry = old_tree_entry_map.remove(&entry.name);
2161 let old_dir_entry = old_dir_entry_map.remove(&entry.name);
2162
2163 if entry.is_tree() {
2164 let old_subtree_oid = old_tree_entry
2165 .as_ref()
2166 .filter(|old| old.is_tree())
2167 .map(|old| old.oid.to_hex());
2168 let old_subdir_entries = if let Some(old_dir_entry) = old_dir_entry.as_ref() {
2169 if old_dir_entry.link_type == LinkType::Dir {
2170 Some(
2171 base_tree
2172 .list_directory(&Cid {
2173 hash: old_dir_entry.hash,
2174 key: old_dir_entry.key,
2175 })
2176 .await
2177 .map_err(|e| {
2178 Error::StorageError(format!(
2179 "list base working dir {}: {}",
2180 path, e
2181 ))
2182 })?,
2183 )
2184 } else {
2185 None
2186 }
2187 } else {
2188 None
2189 };
2190
2191 self.collect_tree_entries_for_index_with_base_boxed(
2192 &oid_hex,
2193 objects,
2194 base_tree,
2195 base_objects_cid,
2196 old_subtree_oid.as_deref(),
2197 old_subdir_entries.as_ref(),
2198 &path,
2199 entries,
2200 progress,
2201 )
2202 .await?;
2203 continue;
2204 }
2205
2206 let mut sha1_bytes = [0u8; 20];
2207 if let Ok(bytes) = hex::decode(&oid_hex) {
2208 if bytes.len() == 20 {
2209 sha1_bytes.copy_from_slice(&bytes);
2210 }
2211 }
2212
2213 let size = if let (Some(old_tree_entry), Some(old_dir_entry)) =
2214 (old_tree_entry.as_ref(), old_dir_entry.as_ref())
2215 {
2216 if !old_tree_entry.is_tree()
2217 && old_tree_entry.oid == entry.oid
2218 && old_dir_entry.link_type != LinkType::Dir
2219 {
2220 old_dir_entry.size as u32
2221 } else {
2222 self.blob_size_from_sources(
2223 &oid_hex,
2224 objects,
2225 Some(base_tree),
2226 Some(base_objects_cid),
2227 )
2228 .await? as u32
2229 }
2230 } else {
2231 self.blob_size_from_sources(
2232 &oid_hex,
2233 objects,
2234 Some(base_tree),
2235 Some(base_objects_cid),
2236 )
2237 .await? as u32
2238 };
2239
2240 entries.push((path, sha1_bytes, entry.mode, size));
2241 if let Some(progress) = progress {
2242 progress.record_index_entry();
2243 }
2244 }
2245
2246 Ok(())
2247 })
2248 }
2249
2250 async fn blob_size_from_sources<S: Store>(
2251 &self,
2252 oid: &str,
2253 objects: &HashMap<String, Vec<u8>>,
2254 base_tree: Option<&HashTree<S>>,
2255 base_objects_cid: Option<&Cid>,
2256 ) -> Result<usize> {
2257 let object = match self.get_object_content(oid, objects) {
2258 Some(object) => Some(object),
2259 None => {
2260 if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
2261 self.get_object_content_from_base(oid, base_tree, base_objects_cid)
2262 .await?
2263 } else {
2264 None
2265 }
2266 }
2267 }
2268 .ok_or_else(|| Error::ObjectNotFound(oid.to_string()))?;
2269
2270 if object.0 != ObjectType::Blob {
2271 return Err(Error::InvalidObjectType(format!(
2272 "expected blob, got {:?}",
2273 object.0
2274 )));
2275 }
2276
2277 Ok(object.1.len())
2278 }
2279
2280 pub fn store(&self) -> &Arc<LocalStore> {
2282 &self.store
2283 }
2284
2285 #[allow(dead_code)]
2287 pub fn hashtree(&self) -> &HashTree<LocalStore> {
2288 &self.tree
2289 }
2290
2291 #[allow(dead_code)]
2293 pub fn push_to_file_servers(
2294 &self,
2295 blossom: &hashtree_blossom::BlossomClient,
2296 ) -> Result<(usize, usize)> {
2297 let hashes = self
2298 .store
2299 .list()
2300 .map_err(|e| Error::StorageError(format!("list hashes: {}", e)))?;
2301
2302 info!("Pushing {} blobs to file servers", hashes.len());
2303
2304 let mut uploaded = 0;
2305 let mut existed = 0;
2306
2307 self.runtime.block_on(async {
2308 for hash in &hashes {
2309 let hex_hash = hex::encode(hash);
2310 let data = match self.store.get_sync(hash) {
2311 Ok(Some(d)) => d,
2312 _ => continue,
2313 };
2314
2315 match blossom.upload_if_missing(&data).await {
2316 Ok((_, true)) => {
2317 debug!("Uploaded {}", &hex_hash[..12]);
2318 uploaded += 1;
2319 }
2320 Ok((_, false)) => {
2321 existed += 1;
2322 }
2323 Err(e) => {
2324 debug!("Failed to upload {}: {}", &hex_hash[..12], e);
2325 }
2326 }
2327 }
2328 });
2329
2330 info!(
2331 "Upload complete: {} new, {} already existed",
2332 uploaded, existed
2333 );
2334 Ok((uploaded, existed))
2335 }
2336
2337 #[allow(dead_code)]
2339 pub fn clear(&self) -> Result<()> {
2340 let mut objects = self
2341 .objects
2342 .write()
2343 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
2344 let mut refs = self
2345 .refs
2346 .write()
2347 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
2348 let mut root = self
2349 .root_cid
2350 .write()
2351 .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
2352
2353 objects.clear();
2354 refs.clear();
2355 *root = None;
2356 Ok(())
2357 }
2358}
2359
2360#[cfg(test)]
2361mod tests {
2362 use super::*;
2363 use hashtree_core::store::Store;
2364 use hashtree_core::{sha256, LinkType};
2365 use std::io::{Read, Write};
2366 use std::net::{TcpListener, TcpStream};
2367 use std::path::Path;
2368 use std::process::{Child, Command, Stdio};
2369 #[cfg(feature = "lmdb")]
2370 use std::sync::atomic::{AtomicUsize, Ordering};
2371 use std::time::{Duration, Instant};
2372 use tempfile::TempDir;
2373
2374 fn create_test_storage() -> (GitStorage, TempDir) {
2375 let temp_dir = TempDir::new().unwrap();
2376 let storage =
2377 GitStorage::open_with_backend_and_max_bytes(temp_dir.path(), StorageBackend::Fs, 0)
2378 .unwrap();
2379 (storage, temp_dir)
2380 }
2381
2382 fn create_test_storage_with_limit(max_size_bytes: u64) -> (GitStorage, TempDir) {
2383 let temp_dir = TempDir::new().unwrap();
2384 let storage = GitStorage::open_with_backend_and_max_bytes(
2385 temp_dir.path(),
2386 StorageBackend::Fs,
2387 max_size_bytes,
2388 )
2389 .unwrap();
2390 (storage, temp_dir)
2391 }
2392
2393 fn local_total_bytes(storage: &GitStorage) -> u64 {
2394 match storage.store().as_ref() {
2395 LocalStore::Fs(store) => store.stats().unwrap().total_bytes,
2396 #[cfg(feature = "lmdb")]
2397 LocalStore::Lmdb(store) => store.stats().unwrap().total_bytes,
2398 }
2399 }
2400
2401 fn write_test_commit(storage: &GitStorage) -> ObjectId {
2402 let blob_oid = storage
2403 .write_raw_object(ObjectType::Blob, b"hello from hashtree\n")
2404 .unwrap();
2405
2406 let mut tree_content = Vec::new();
2407 tree_content.extend_from_slice(b"100644 README.md\0");
2408 tree_content.extend_from_slice(&hex::decode(blob_oid.to_hex()).unwrap());
2409 let tree_oid = storage
2410 .write_raw_object(ObjectType::Tree, &tree_content)
2411 .unwrap();
2412
2413 let commit_content = format!(
2414 "tree {}\nauthor Test User <test@example.com> 0 +0000\ncommitter Test User <test@example.com> 0 +0000\n\nInitial commit\n",
2415 tree_oid.to_hex()
2416 );
2417 storage
2418 .write_raw_object(ObjectType::Commit, commit_content.as_bytes())
2419 .unwrap()
2420 }
2421
2422 fn write_root_tree(storage: &GitStorage, blobs: &[(&str, ObjectId)]) -> ObjectId {
2423 let mut sorted = blobs.to_vec();
2424 sorted.sort_by(|a, b| a.0.cmp(b.0));
2425
2426 let mut tree_content = Vec::new();
2427 for (name, oid) in sorted {
2428 tree_content.extend_from_slice(format!("100644 {name}\0").as_bytes());
2429 tree_content.extend_from_slice(&hex::decode(oid.to_hex()).unwrap());
2430 }
2431
2432 storage
2433 .write_raw_object(ObjectType::Tree, &tree_content)
2434 .unwrap()
2435 }
2436
2437 fn write_commit_for_tree(storage: &GitStorage, tree_oid: ObjectId, message: &str) -> ObjectId {
2438 let commit_content = format!(
2439 "tree {}\nauthor Test User <test@example.com> 0 +0000\ncommitter Test User <test@example.com> 0 +0000\n\n{}\n",
2440 tree_oid.to_hex(),
2441 message
2442 );
2443 storage
2444 .write_raw_object(ObjectType::Commit, commit_content.as_bytes())
2445 .unwrap()
2446 }
2447
2448 fn export_tree_to_fs<S: Store>(
2449 runtime: &RuntimeExecutor,
2450 tree: &HashTree<S>,
2451 cid: &Cid,
2452 dst: &Path,
2453 ) {
2454 std::fs::create_dir_all(dst).unwrap();
2455 let entries = runtime.block_on(tree.list_directory(cid)).unwrap();
2456 for entry in entries {
2457 let entry_cid = Cid {
2458 hash: entry.hash,
2459 key: entry.key,
2460 };
2461 let path = dst.join(&entry.name);
2462 match entry.link_type {
2463 LinkType::Dir | LinkType::Fanout => {
2464 export_tree_to_fs(runtime, tree, &entry_cid, &path)
2465 }
2466 LinkType::Blob | LinkType::File => {
2467 let data = runtime
2468 .block_on(tree.get(&entry_cid, None))
2469 .unwrap()
2470 .unwrap();
2471 if let Some(parent) = path.parent() {
2472 std::fs::create_dir_all(parent).unwrap();
2473 }
2474 std::fs::write(path, data).unwrap();
2475 }
2476 }
2477 }
2478 }
2479
2480 fn spawn_http_server(root: &Path, port: u16) -> Child {
2481 Command::new("python3")
2482 .args([
2483 "-m",
2484 "http.server",
2485 &port.to_string(),
2486 "--bind",
2487 "127.0.0.1",
2488 ])
2489 .current_dir(root)
2490 .stdout(Stdio::null())
2491 .stderr(Stdio::null())
2492 .spawn()
2493 .expect("spawn python http server")
2494 }
2495
2496 fn wait_for_http_server(server: &mut Child, port: u16, path: &str) {
2497 let deadline = Instant::now() + Duration::from_secs(5);
2498
2499 loop {
2500 if let Some(status) = server.try_wait().expect("check http server status") {
2501 panic!("python http server exited before becoming ready: {status}");
2502 }
2503
2504 if let Ok(mut stream) = TcpStream::connect(("127.0.0.1", port)) {
2505 stream
2506 .set_read_timeout(Some(Duration::from_millis(200)))
2507 .expect("set read timeout");
2508 stream
2509 .set_write_timeout(Some(Duration::from_millis(200)))
2510 .expect("set write timeout");
2511 let request =
2512 format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n");
2513 if stream.write_all(request.as_bytes()).is_ok() {
2514 let mut response = String::new();
2515 if stream.read_to_string(&mut response).is_ok()
2516 && response.starts_with("HTTP/1.0 200")
2517 {
2518 return;
2519 }
2520 }
2521 }
2522
2523 if Instant::now() >= deadline {
2524 panic!("python http server did not become ready on port {port}");
2525 }
2526 std::thread::sleep(Duration::from_millis(50));
2527 }
2528 }
2529
2530 #[test]
2531 fn test_import_ref() {
2532 let (storage, _temp) = create_test_storage();
2533
2534 storage
2536 .import_ref("refs/heads/main", "abc123def456")
2537 .unwrap();
2538
2539 assert!(storage.has_ref("refs/heads/main").unwrap());
2541
2542 let refs = storage.list_refs().unwrap();
2544 assert_eq!(
2545 refs.get("refs/heads/main"),
2546 Some(&"abc123def456".to_string())
2547 );
2548 }
2549
2550 #[cfg(feature = "lmdb")]
2551 #[test]
2552 fn test_local_store_falls_back_to_fs_when_lmdb_open_returns_enosys() {
2553 let temp_dir = TempDir::new().unwrap();
2554 let fs_calls = AtomicUsize::new(0);
2555 let lmdb_calls = AtomicUsize::new(0);
2556
2557 let store = LocalStore::new_for_backend_with_openers(
2558 temp_dir.path(),
2559 StorageBackend::Lmdb,
2560 0,
2561 |path, max_bytes| {
2562 fs_calls.fetch_add(1, Ordering::SeqCst);
2563 LocalStore::open_fs_store(path, max_bytes)
2564 },
2565 |_path, _max_bytes| {
2566 lmdb_calls.fetch_add(1, Ordering::SeqCst);
2567 Err(StoreError::Io(std::io::Error::from_raw_os_error(
2568 libc::ENOSYS,
2569 )))
2570 },
2571 )
2572 .unwrap();
2573
2574 assert!(matches!(store, LocalStore::Fs(_)));
2575 assert_eq!(lmdb_calls.load(Ordering::SeqCst), 1);
2576 assert_eq!(fs_calls.load(Ordering::SeqCst), 1);
2577 }
2578
2579 #[cfg(feature = "lmdb")]
2580 #[test]
2581 fn test_local_store_does_not_fallback_on_unrelated_lmdb_errors() {
2582 let temp_dir = TempDir::new().unwrap();
2583 let fs_calls = AtomicUsize::new(0);
2584
2585 let result = LocalStore::new_for_backend_with_openers(
2586 temp_dir.path(),
2587 StorageBackend::Lmdb,
2588 0,
2589 |path, max_bytes| {
2590 fs_calls.fetch_add(1, Ordering::SeqCst);
2591 LocalStore::open_fs_store(path, max_bytes)
2592 },
2593 |_path, _max_bytes| {
2594 Err(StoreError::Io(std::io::Error::from_raw_os_error(
2595 libc::EACCES,
2596 )))
2597 },
2598 );
2599
2600 assert!(
2601 matches!(result, Err(StoreError::Io(io_error)) if io_error.raw_os_error() == Some(libc::EACCES))
2602 );
2603 assert_eq!(fs_calls.load(Ordering::SeqCst), 0);
2604 }
2605
2606 #[test]
2607 fn test_import_multiple_refs_preserves_all() {
2608 let (storage, _temp) = create_test_storage();
2609
2610 storage.import_ref("refs/heads/main", "sha_main").unwrap();
2612 storage.import_ref("refs/heads/dev", "sha_dev").unwrap();
2613 storage
2614 .import_ref("refs/heads/feature", "sha_feature")
2615 .unwrap();
2616
2617 assert!(storage.has_ref("refs/heads/main").unwrap());
2619 assert!(storage.has_ref("refs/heads/dev").unwrap());
2620 assert!(storage.has_ref("refs/heads/feature").unwrap());
2621
2622 storage
2624 .write_ref(
2625 "refs/heads/new-branch",
2626 &Ref::Direct(
2627 ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2628 ),
2629 )
2630 .unwrap();
2631
2632 let refs = storage.list_refs().unwrap();
2634 assert_eq!(refs.len(), 4);
2635 assert!(refs.contains_key("refs/heads/main"));
2636 assert!(refs.contains_key("refs/heads/dev"));
2637 assert!(refs.contains_key("refs/heads/feature"));
2638 assert!(refs.contains_key("refs/heads/new-branch"));
2639 }
2640
2641 #[test]
2642 fn test_import_compressed_object() {
2643 let (storage, _temp) = create_test_storage();
2644
2645 let fake_compressed = vec![0x78, 0x9c, 0x01, 0x02, 0x03]; storage
2649 .import_compressed_object("abc123def456", fake_compressed.clone())
2650 .unwrap();
2651
2652 assert_eq!(storage.object_count().unwrap(), 1);
2654 }
2655
2656 #[test]
2657 fn test_write_ref_overwrites_imported() {
2658 let (storage, _temp) = create_test_storage();
2659
2660 storage.import_ref("refs/heads/main", "old_sha").unwrap();
2662
2663 storage
2665 .write_ref(
2666 "refs/heads/main",
2667 &Ref::Direct(
2668 ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2669 ),
2670 )
2671 .unwrap();
2672
2673 let refs = storage.list_refs().unwrap();
2675 assert_eq!(
2676 refs.get("refs/heads/main"),
2677 Some(&"0123456789abcdef0123456789abcdef01234567".to_string())
2678 );
2679 }
2680
2681 #[test]
2682 fn test_delete_ref_preserves_others() {
2683 let (storage, _temp) = create_test_storage();
2684
2685 storage.import_ref("refs/heads/main", "sha_main").unwrap();
2687 storage.import_ref("refs/heads/dev", "sha_dev").unwrap();
2688
2689 storage.delete_ref("refs/heads/dev").unwrap();
2691
2692 assert!(storage.has_ref("refs/heads/main").unwrap());
2694 assert!(!storage.has_ref("refs/heads/dev").unwrap());
2695 }
2696
2697 #[test]
2698 fn test_clear_removes_all() {
2699 let (storage, _temp) = create_test_storage();
2700
2701 storage.import_ref("refs/heads/main", "sha_main").unwrap();
2703 storage
2704 .import_compressed_object("obj1", vec![1, 2, 3])
2705 .unwrap();
2706
2707 storage.clear().unwrap();
2709
2710 assert!(!storage.has_ref("refs/heads/main").unwrap());
2712 assert_eq!(storage.object_count().unwrap(), 0);
2713 }
2714
2715 #[test]
2716 fn test_evict_if_needed_respects_configured_limit() {
2717 let (storage, _temp) = create_test_storage_with_limit(1_024);
2718
2719 storage
2720 .write_raw_object(ObjectType::Blob, &vec![b'a'; 900])
2721 .unwrap();
2722 storage
2723 .write_raw_object(ObjectType::Blob, &vec![b'b'; 900])
2724 .unwrap();
2725 storage
2726 .write_ref(
2727 "refs/heads/main",
2728 &Ref::Direct(
2729 ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2730 ),
2731 )
2732 .unwrap();
2733
2734 storage.build_tree().unwrap();
2735
2736 let before = local_total_bytes(&storage);
2737 assert!(before > 1_024);
2738
2739 let freed = storage.evict_if_needed().unwrap();
2740 assert!(freed > 0);
2741
2742 let after = local_total_bytes(&storage);
2743 assert!(after <= 1_024);
2744 }
2745
2746 #[test]
2747 fn test_build_tree_evicts_stale_blobs_before_writing_new_tree() {
2748 let max_size_bytes = 16 * 1024;
2749 let (storage, _temp) = create_test_storage_with_limit(max_size_bytes);
2750
2751 let stale_blobs = vec![
2752 vec![b'x'; 7 * 1024],
2753 vec![b'y'; 7 * 1024],
2754 vec![b'z'; 7 * 1024],
2755 ];
2756 let stale_hashes: Vec<Hash> = stale_blobs.iter().map(|blob| sha256(blob)).collect();
2757
2758 for (hash, blob) in stale_hashes.iter().zip(stale_blobs) {
2759 storage
2760 .runtime
2761 .block_on(storage.store().put(*hash, blob))
2762 .unwrap();
2763 }
2764
2765 let before = local_total_bytes(&storage);
2766 assert!(before > max_size_bytes);
2767
2768 let commit_oid = write_test_commit(&storage);
2769 storage
2770 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2771 .unwrap();
2772 storage
2773 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2774 .unwrap();
2775
2776 storage.build_tree().unwrap();
2777
2778 let evicted_stale = stale_hashes
2779 .iter()
2780 .filter(|hash| !storage.runtime.block_on(storage.store().has(hash)).unwrap())
2781 .count();
2782
2783 assert!(
2784 evicted_stale > 0,
2785 "expected build_tree preflight eviction to remove stale blobs before writing"
2786 );
2787 }
2788
2789 #[test]
2790 fn test_build_tree_progress_tracks_repo_tree_work() {
2791 let (storage, _temp) = create_test_storage();
2792 let commit_oid = write_test_commit(&storage);
2793 storage
2794 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2795 .unwrap();
2796 storage
2797 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2798 .unwrap();
2799
2800 let progress = RepoTreeBuildProgress::new();
2801 storage.build_tree_with_progress(&progress).unwrap();
2802
2803 let snapshot = progress.snapshot();
2804 assert_eq!(snapshot.phase, RepoTreeBuildPhase::Done);
2805 assert_eq!(snapshot.object_blobs, 3);
2806 assert_eq!(snapshot.files, 1);
2807 assert_eq!(snapshot.dirs, 0);
2808 assert_eq!(snapshot.reused, 0);
2809 assert_eq!(
2810 snapshot.format_for_label("Building repo tree"),
2811 " Building repo tree: done (3 object blobs, 1 files, 0 dirs, 0 reused)"
2812 );
2813 }
2814
2815 #[test]
2816 fn test_build_tree_adds_dumb_http_metadata() {
2817 let (storage, _temp) = create_test_storage();
2818 let commit_oid = write_test_commit(&storage);
2819 let tag_content = format!(
2820 "object {}\ntype commit\ntag v1.0.0\ntagger Test User <test@example.com> 0 +0000\n\nrelease\n",
2821 commit_oid.to_hex()
2822 );
2823 let tag_oid = storage
2824 .write_raw_object(ObjectType::Tag, tag_content.as_bytes())
2825 .unwrap();
2826
2827 storage
2828 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2829 .unwrap();
2830 storage
2831 .write_ref("refs/tags/v1.0.0", &Ref::Direct(tag_oid))
2832 .unwrap();
2833 storage
2834 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2835 .unwrap();
2836
2837 let root_cid = storage.build_tree().unwrap();
2838
2839 let info_refs_cid = storage
2840 .runtime
2841 .block_on(storage.tree.resolve_path(&root_cid, ".git/info/refs"))
2842 .unwrap()
2843 .expect("info/refs exists");
2844 let info_refs = storage
2845 .runtime
2846 .block_on(storage.tree.get(&info_refs_cid, None))
2847 .unwrap()
2848 .unwrap();
2849 let info_refs = String::from_utf8(info_refs).unwrap();
2850
2851 assert_eq!(
2852 info_refs,
2853 format!(
2854 "{commit}\trefs/heads/main\n{tag}\trefs/tags/v1.0.0\n{commit}\trefs/tags/v1.0.0^{{}}\n",
2855 commit = commit_oid.to_hex(),
2856 tag = tag_oid.to_hex()
2857 )
2858 );
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!(packs.is_empty(), "objects/info/packs should be empty");
2875 }
2876
2877 #[test]
2878 fn test_build_tree_writes_git_objects_info_packs_for_pack_files() {
2879 let (storage, _temp) = create_test_storage();
2880 let commit_oid = write_test_commit(&storage);
2881 storage
2882 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2883 .unwrap();
2884 storage
2885 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2886 .unwrap();
2887
2888 let pack_hash = "0123456789abcdef0123456789abcdef01234567";
2889 let pack_name = format!("pack-{pack_hash}.pack");
2890 let idx_name = format!("pack-{pack_hash}.idx");
2891 let mut pack_files = BTreeMap::new();
2892 pack_files.insert(pack_name.clone(), b"pack bytes".to_vec());
2893 pack_files.insert(idx_name.clone(), b"idx bytes".to_vec());
2894 storage.set_pack_files(pack_files).unwrap();
2895
2896 let root_cid = storage.build_tree().unwrap();
2897
2898 let packs_cid = storage
2899 .runtime
2900 .block_on(
2901 storage
2902 .tree
2903 .resolve_path(&root_cid, ".git/objects/info/packs"),
2904 )
2905 .unwrap()
2906 .expect("objects/info/packs exists");
2907 let packs = storage
2908 .runtime
2909 .block_on(storage.tree.get(&packs_cid, None))
2910 .unwrap()
2911 .unwrap();
2912 assert_eq!(
2913 String::from_utf8(packs).unwrap(),
2914 format!("P {pack_name}\n")
2915 );
2916
2917 for path in [
2918 format!(".git/objects/pack/{pack_name}"),
2919 format!(".git/objects/pack/{idx_name}"),
2920 ] {
2921 let cid = storage
2922 .runtime
2923 .block_on(storage.tree.resolve_path(&root_cid, &path))
2924 .unwrap()
2925 .unwrap_or_else(|| panic!("{path} should exist"));
2926 let content = storage
2927 .runtime
2928 .block_on(storage.tree.get(&cid, None))
2929 .unwrap()
2930 .unwrap();
2931 assert!(!content.is_empty(), "{path} should have content");
2932 }
2933 }
2934
2935 #[test]
2936 fn test_pack_checkpoint_can_replace_loose_git_objects() {
2937 let (storage, _temp) = create_test_storage();
2938 let commit_oid = write_test_commit(&storage);
2939 storage
2940 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2941 .unwrap();
2942 storage
2943 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2944 .unwrap();
2945
2946 let pack_hash = "0123456789abcdef0123456789abcdef01234567";
2947 let pack_name = format!("pack-{pack_hash}.pack");
2948 let idx_name = format!("pack-{pack_hash}.idx");
2949 let mut pack_files = BTreeMap::new();
2950 pack_files.insert(pack_name.clone(), b"pack bytes".to_vec());
2951 pack_files.insert(idx_name, b"idx bytes".to_vec());
2952 storage
2953 .set_pack_checkpoint_files(pack_files, HashSet::from([commit_oid.to_hex()]))
2954 .unwrap();
2955
2956 let root_cid = storage.build_tree().unwrap();
2957 let loose_path = format!(
2958 ".git/objects/{}/{}",
2959 &commit_oid.to_hex()[..2],
2960 &commit_oid.to_hex()[2..]
2961 );
2962 let loose_cid = storage
2963 .runtime
2964 .block_on(storage.tree.resolve_path(&root_cid, &loose_path))
2965 .unwrap();
2966 assert!(
2967 loose_cid.is_none(),
2968 "pack checkpoint should omit duplicated loose objects"
2969 );
2970
2971 storage
2972 .validate_root_contains_direct_refs(&root_cid)
2973 .expect("pack checkpoint should satisfy ref validation");
2974
2975 let packs_cid = storage
2976 .runtime
2977 .block_on(
2978 storage
2979 .tree
2980 .resolve_path(&root_cid, ".git/objects/info/packs"),
2981 )
2982 .unwrap()
2983 .expect("objects/info/packs exists");
2984 let packs = storage
2985 .runtime
2986 .block_on(storage.tree.get(&packs_cid, None))
2987 .unwrap()
2988 .unwrap();
2989 assert_eq!(
2990 String::from_utf8(packs).unwrap(),
2991 format!("P {pack_name}\n")
2992 );
2993 }
2994
2995 #[test]
2996 fn test_inherited_pack_checkpoint_can_replace_base_loose_git_objects() {
2997 let (storage, _temp) = create_test_storage();
2998 let commit_oid = write_test_commit(&storage);
2999 storage
3000 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
3001 .unwrap();
3002 storage
3003 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
3004 .unwrap();
3005
3006 let pack_hash = "0123456789abcdef0123456789abcdef01234567";
3007 let pack_name = format!("pack-{pack_hash}.pack");
3008 let idx_name = format!("pack-{pack_hash}.idx");
3009 let mut pack_files = BTreeMap::new();
3010 pack_files.insert(pack_name.clone(), b"pack bytes".to_vec());
3011 pack_files.insert(idx_name, b"idx bytes".to_vec());
3012 storage
3013 .set_pack_checkpoint_files(pack_files, HashSet::from([commit_oid.to_hex()]))
3014 .unwrap();
3015 let base_root_cid = storage.build_tree().unwrap();
3016
3017 storage
3018 .set_pack_checkpoint_files(BTreeMap::new(), HashSet::from([commit_oid.to_hex()]))
3019 .unwrap();
3020
3021 let root_cid = storage
3022 .build_tree_with_base_objects(Some(&storage.tree), Some(&base_root_cid), None)
3023 .unwrap();
3024 let loose_path = format!(
3025 ".git/objects/{}/{}",
3026 &commit_oid.to_hex()[..2],
3027 &commit_oid.to_hex()[2..]
3028 );
3029 let loose_cid = storage
3030 .runtime
3031 .block_on(storage.tree.resolve_path(&root_cid, &loose_path))
3032 .unwrap();
3033 assert!(
3034 loose_cid.is_none(),
3035 "inherited pack-covered objects should not be rewritten loose"
3036 );
3037
3038 let packs_cid = storage
3039 .runtime
3040 .block_on(
3041 storage
3042 .tree
3043 .resolve_path(&root_cid, ".git/objects/info/packs"),
3044 )
3045 .unwrap()
3046 .expect("inherited objects/info/packs exists");
3047 let packs = storage
3048 .runtime
3049 .block_on(storage.tree.get(&packs_cid, None))
3050 .unwrap()
3051 .unwrap();
3052 assert_eq!(
3053 String::from_utf8(packs).unwrap(),
3054 format!("P {pack_name}\n")
3055 );
3056 }
3057
3058 #[test]
3059 fn test_pack_only_base_reuses_visible_working_files_without_loose_old_tree() {
3060 let (storage, _temp) = create_test_storage();
3061 let readme_oid = storage
3062 .write_raw_object(ObjectType::Blob, b"base readme\n")
3063 .unwrap();
3064 let base_tree_oid = write_root_tree(&storage, &[("README.md", readme_oid)]);
3065 let base_commit_oid = write_commit_for_tree(&storage, base_tree_oid, "base");
3066 storage
3067 .write_ref("refs/heads/main", &Ref::Direct(base_commit_oid))
3068 .unwrap();
3069 storage
3070 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
3071 .unwrap();
3072
3073 let pack_hash = "fedcba9876543210fedcba9876543210fedcba98";
3074 let pack_name = format!("pack-{pack_hash}.pack");
3075 let idx_name = format!("pack-{pack_hash}.idx");
3076 let mut pack_files = BTreeMap::new();
3077 pack_files.insert(pack_name, b"pack bytes".to_vec());
3078 pack_files.insert(idx_name, b"idx bytes".to_vec());
3079 storage
3080 .set_pack_checkpoint_files(
3081 pack_files,
3082 HashSet::from([
3083 base_commit_oid.to_hex(),
3084 base_tree_oid.to_hex(),
3085 readme_oid.to_hex(),
3086 ]),
3087 )
3088 .unwrap();
3089 let base_root_cid = storage.build_tree().unwrap();
3090
3091 let new_blob_oid = storage
3092 .write_raw_object(ObjectType::Blob, b"new file\n")
3093 .unwrap();
3094 let current_tree_oid = write_root_tree(
3095 &storage,
3096 &[("README.md", readme_oid), ("new.txt", new_blob_oid)],
3097 );
3098 let current_commit_oid = write_commit_for_tree(&storage, current_tree_oid, "delta");
3099 storage
3100 .write_ref("refs/heads/main", &Ref::Direct(current_commit_oid))
3101 .unwrap();
3102 storage
3103 .set_pack_checkpoint_files(BTreeMap::new(), HashSet::new())
3104 .unwrap();
3105
3106 {
3107 let mut objects = storage.objects.write().unwrap();
3108 objects.remove(&base_commit_oid.to_hex());
3109 objects.remove(&base_tree_oid.to_hex());
3110 objects.remove(&readme_oid.to_hex());
3111 }
3112
3113 let root_cid = storage
3114 .build_tree_with_base_objects(
3115 Some(&storage.tree),
3116 Some(&base_root_cid),
3117 Some(&base_tree_oid.to_hex()),
3118 )
3119 .unwrap();
3120
3121 for (path, expected) in [("README.md", "base readme\n"), ("new.txt", "new file\n")] {
3122 let cid = storage
3123 .runtime
3124 .block_on(storage.tree.resolve_path(&root_cid, path))
3125 .unwrap()
3126 .unwrap_or_else(|| panic!("{path} should exist"));
3127 let content = storage
3128 .runtime
3129 .block_on(storage.tree.get(&cid, None))
3130 .unwrap()
3131 .unwrap();
3132 assert_eq!(String::from_utf8(content).unwrap(), expected, "{path}");
3133 }
3134 }
3135
3136 #[test]
3137 fn test_build_tree_materializes_loose_refs_at_git_paths() {
3138 let (storage, _temp) = create_test_storage();
3139 let commit_oid = write_test_commit(&storage);
3140
3141 storage
3142 .write_ref("refs/heads/master", &Ref::Direct(commit_oid))
3143 .unwrap();
3144 storage
3145 .write_ref("refs/heads/codex/meshrouter-prod", &Ref::Direct(commit_oid))
3146 .unwrap();
3147 storage
3148 .write_ref("refs/tags/v1.0.0", &Ref::Direct(commit_oid))
3149 .unwrap();
3150 storage
3151 .write_ref("HEAD", &Ref::Symbolic("refs/heads/master".to_string()))
3152 .unwrap();
3153
3154 let root_cid = storage.build_tree().unwrap();
3155
3156 for path in [
3157 ".git/refs/heads/master",
3158 ".git/refs/heads/codex/meshrouter-prod",
3159 ".git/refs/tags/v1.0.0",
3160 ] {
3161 let ref_cid = storage
3162 .runtime
3163 .block_on(storage.tree.resolve_path(&root_cid, path))
3164 .unwrap()
3165 .unwrap_or_else(|| panic!("{path} should exist"));
3166 let ref_value = storage
3167 .runtime
3168 .block_on(storage.tree.get(&ref_cid, None))
3169 .unwrap()
3170 .unwrap();
3171 assert_eq!(
3172 String::from_utf8(ref_value).unwrap(),
3173 commit_oid.to_hex(),
3174 "{path} should contain the ref target",
3175 );
3176 }
3177 }
3178
3179 #[test]
3180 fn test_materialized_tree_supports_static_http_clone_from_git_dir() {
3181 let (storage, _temp) = create_test_storage();
3182 let commit_oid = write_test_commit(&storage);
3183 storage
3184 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
3185 .unwrap();
3186 storage
3187 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
3188 .unwrap();
3189
3190 let root_cid = storage.build_tree().unwrap();
3191 let export_dir = TempDir::new().unwrap();
3192 let repo_dir = export_dir.path().join("repo");
3193 export_tree_to_fs(&storage.runtime, &storage.tree, &root_cid, &repo_dir);
3194
3195 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
3196 let port = listener.local_addr().unwrap().port();
3197 drop(listener);
3198
3199 let mut server = spawn_http_server(export_dir.path(), port);
3200 wait_for_http_server(&mut server, port, "/repo/.git/HEAD");
3201
3202 let clone_dir = TempDir::new().unwrap();
3203 let clone_path = clone_dir.path().join("clone");
3204 let output = Command::new("git")
3205 .current_dir(clone_dir.path())
3206 .args([
3207 "clone",
3208 &format!("http://127.0.0.1:{port}/repo/.git", port = port),
3209 clone_path.to_str().unwrap(),
3210 ])
3211 .output()
3212 .unwrap();
3213
3214 let _ = server.kill();
3215 let _ = server.wait();
3216
3217 assert!(
3218 output.status.success(),
3219 "git clone failed: {}",
3220 String::from_utf8_lossy(&output.stderr)
3221 );
3222 assert_eq!(
3223 std::fs::read_to_string(clone_path.join("README.md")).unwrap(),
3224 "hello from hashtree\n"
3225 );
3226 }
3227
3228 #[test]
3229 fn test_validate_root_contains_direct_refs_rejects_missing_tip_object() {
3230 let (storage, _temp) = create_test_storage();
3231 let commit_oid = write_test_commit(&storage);
3232 storage
3233 .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
3234 .unwrap();
3235 storage
3236 .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
3237 .unwrap();
3238
3239 let empty_objects_dir = storage
3240 .runtime
3241 .block_on(storage.tree.put_directory(vec![]))
3242 .unwrap();
3243 let refs_dir = storage
3244 .runtime
3245 .block_on(storage.tree.put_directory(vec![]))
3246 .unwrap();
3247 let info_dir = storage
3248 .runtime
3249 .block_on(storage.tree.put_directory(vec![]))
3250 .unwrap();
3251 let git_dir = storage
3252 .runtime
3253 .block_on(storage.tree.put_directory(vec![
3254 DirEntry::from_cid("HEAD", &info_dir).with_size(0),
3255 DirEntry::from_cid("info", &info_dir).with_link_type(LinkType::Dir),
3256 DirEntry::from_cid("objects", &empty_objects_dir).with_link_type(LinkType::Dir),
3257 DirEntry::from_cid("refs", &refs_dir).with_link_type(LinkType::Dir),
3258 ]))
3259 .unwrap();
3260 let root_cid = storage
3261 .runtime
3262 .block_on(storage.tree.put_directory(vec![
3263 DirEntry::from_cid(".git", &git_dir).with_link_type(LinkType::Dir),
3264 ]))
3265 .unwrap();
3266
3267 let err = storage
3268 .validate_root_contains_direct_refs(&root_cid)
3269 .expect_err("missing ref-tip object should fail validation");
3270 assert!(
3271 err.to_string().contains(&commit_oid.to_hex()),
3272 "validation error should mention missing commit oid: {}",
3273 err
3274 );
3275 }
3276}