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