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