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