Skip to main content

git_remote_htree/git/
storage.rs

1//! Hashtree-backed git object and ref storage with configurable persistence
2//!
3//! Stores git objects and refs in a hashtree merkle tree:
4//!   root/
5//!     .git/
6//!       HEAD -> "ref: refs/heads/main"
7//!       refs/heads/main -> <commit-sha1>
8//!       info/refs -> dumb-HTTP ref advertisement
9//!       objects/XX/YYYY... -> zlib-compressed loose object (standard git layout)
10//!       objects/info/packs -> dumb-HTTP pack advertisement
11//!
12//! The root hash (SHA-256) is the content-addressed identifier for the entire repo state.
13
14use 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};
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::refs::{validate_ref_name, Ref};
34use super::{Error, Result};
35
36/// Box type for async recursion
37type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
38
39#[derive(Default)]
40struct RefDirectory {
41    files: BTreeMap<String, String>,
42    dirs: BTreeMap<String, RefDirectory>,
43}
44
45impl RefDirectory {
46    fn insert(&mut self, parts: &[&str], value: String) {
47        let Some((name, rest)) = parts.split_first() else {
48            return;
49        };
50
51        if rest.is_empty() {
52            self.files.insert((*name).to_string(), value);
53        } else {
54            self.dirs
55                .entry((*name).to_string())
56                .or_default()
57                .insert(rest, value);
58        }
59    }
60}
61
62/// Runtime executor - either owns a runtime or reuses an existing one
63enum RuntimeExecutor {
64    Owned(Runtime),
65    Handle(Handle),
66}
67
68impl RuntimeExecutor {
69    fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
70        match self {
71            RuntimeExecutor::Owned(rt) => rt.block_on(f),
72            RuntimeExecutor::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
73        }
74    }
75}
76
77/// Local blob store - wraps either FsBlobStore or LmdbBlobStore
78pub enum LocalStore {
79    Fs(FsBlobStore),
80    #[cfg(feature = "lmdb")]
81    Lmdb(LmdbBlobStore),
82}
83
84impl LocalStore {
85    pub(crate) fn new_for_backend<P: AsRef<Path>>(
86        path: P,
87        backend: StorageBackend,
88        max_bytes: u64,
89    ) -> std::result::Result<Self, StoreError> {
90        let path = path.as_ref();
91        #[cfg(feature = "lmdb")]
92        {
93            return Self::new_for_backend_with_openers(
94                path,
95                backend,
96                max_bytes,
97                Self::open_fs_store,
98                Self::open_lmdb_store,
99            );
100        }
101
102        #[cfg(not(feature = "lmdb"))]
103        match backend {
104            StorageBackend::Fs => Self::open_fs_store(path, max_bytes),
105            #[cfg(not(feature = "lmdb"))]
106            StorageBackend::Lmdb => {
107                warn!(
108                    "LMDB backend requested but lmdb feature not enabled, using filesystem storage"
109                );
110                Self::open_fs_store(path, max_bytes)
111            }
112        }
113    }
114
115    fn open_fs_store(path: &Path, max_bytes: u64) -> std::result::Result<Self, StoreError> {
116        if max_bytes > 0 {
117            Ok(LocalStore::Fs(FsBlobStore::with_max_bytes(
118                path, max_bytes,
119            )?))
120        } else {
121            Ok(LocalStore::Fs(FsBlobStore::new(path)?))
122        }
123    }
124
125    #[cfg(feature = "lmdb")]
126    fn open_lmdb_store(path: &Path, max_bytes: u64) -> std::result::Result<Self, StoreError> {
127        if max_bytes > 0 {
128            Ok(LocalStore::Lmdb(LmdbBlobStore::with_max_bytes(
129                path, max_bytes,
130            )?))
131        } else {
132            Ok(LocalStore::Lmdb(LmdbBlobStore::new(path)?))
133        }
134    }
135
136    #[cfg(feature = "lmdb")]
137    fn new_for_backend_with_openers<FS, LMDB>(
138        path: &Path,
139        backend: StorageBackend,
140        max_bytes: u64,
141        fs_open: FS,
142        lmdb_open: LMDB,
143    ) -> std::result::Result<Self, StoreError>
144    where
145        FS: Fn(&Path, u64) -> std::result::Result<Self, StoreError>,
146        LMDB: Fn(&Path, u64) -> std::result::Result<Self, StoreError>,
147    {
148        match backend {
149            StorageBackend::Fs => fs_open(path, max_bytes),
150            StorageBackend::Lmdb => match lmdb_open(path, max_bytes) {
151                Ok(store) => Ok(store),
152                Err(err) if should_fallback_from_lmdb_error(&err) => {
153                    warn!(
154                        path = %path.display(),
155                        "LMDB backend is unsupported in this environment, falling back to filesystem storage"
156                    );
157                    fs_open(path, max_bytes)
158                }
159                Err(err) => Err(err),
160            },
161        }
162    }
163
164    /// Create a new local store based on config
165    pub fn new<P: AsRef<Path>>(path: P) -> std::result::Result<Self, StoreError> {
166        Self::new_with_max_bytes(path, 0)
167    }
168
169    /// Create a new local store based on config with an optional byte limit.
170    pub fn new_with_max_bytes<P: AsRef<Path>>(
171        path: P,
172        max_bytes: u64,
173    ) -> std::result::Result<Self, StoreError> {
174        let config = Config::load_or_default();
175        Self::new_for_backend(path, config.storage.backend, max_bytes)
176    }
177
178    /// List all hashes in the store
179    pub fn list(&self) -> std::result::Result<Vec<Hash>, StoreError> {
180        match self {
181            LocalStore::Fs(store) => store.list(),
182            #[cfg(feature = "lmdb")]
183            LocalStore::Lmdb(store) => store.list(),
184        }
185    }
186
187    /// Sync get operation
188    pub fn get_sync(&self, hash: &Hash) -> std::result::Result<Option<Vec<u8>>, StoreError> {
189        match self {
190            LocalStore::Fs(store) => store.get_sync(hash),
191            #[cfg(feature = "lmdb")]
192            LocalStore::Lmdb(store) => store.get_sync(hash),
193        }
194    }
195}
196
197#[cfg(feature = "lmdb")]
198pub(crate) fn should_fallback_from_lmdb_error(error: &StoreError) -> bool {
199    matches!(
200        error,
201        StoreError::Io(io_error) if io_error.raw_os_error() == Some(libc::ENOSYS)
202    )
203}
204
205#[async_trait::async_trait]
206impl Store for LocalStore {
207    async fn put(&self, hash: Hash, data: Vec<u8>) -> std::result::Result<bool, StoreError> {
208        match self {
209            LocalStore::Fs(store) => store.put(hash, data).await,
210            #[cfg(feature = "lmdb")]
211            LocalStore::Lmdb(store) => store.put(hash, data).await,
212        }
213    }
214
215    async fn get(&self, hash: &Hash) -> std::result::Result<Option<Vec<u8>>, StoreError> {
216        match self {
217            LocalStore::Fs(store) => store.get(hash).await,
218            #[cfg(feature = "lmdb")]
219            LocalStore::Lmdb(store) => store.get(hash).await,
220        }
221    }
222
223    async fn has(&self, hash: &Hash) -> std::result::Result<bool, StoreError> {
224        match self {
225            LocalStore::Fs(store) => store.has(hash).await,
226            #[cfg(feature = "lmdb")]
227            LocalStore::Lmdb(store) => store.has(hash).await,
228        }
229    }
230
231    async fn delete(&self, hash: &Hash) -> std::result::Result<bool, StoreError> {
232        match self {
233            LocalStore::Fs(store) => store.delete(hash).await,
234            #[cfg(feature = "lmdb")]
235            LocalStore::Lmdb(store) => store.delete(hash).await,
236        }
237    }
238
239    fn set_max_bytes(&self, max: u64) {
240        match self {
241            LocalStore::Fs(store) => store.set_max_bytes(max),
242            #[cfg(feature = "lmdb")]
243            LocalStore::Lmdb(store) => store.set_max_bytes(max),
244        }
245    }
246
247    fn max_bytes(&self) -> Option<u64> {
248        match self {
249            LocalStore::Fs(store) => store.max_bytes(),
250            #[cfg(feature = "lmdb")]
251            LocalStore::Lmdb(store) => store.max_bytes(),
252        }
253    }
254
255    async fn stats(&self) -> StoreStats {
256        match self {
257            LocalStore::Fs(store) => match store.stats() {
258                Ok(stats) => StoreStats {
259                    count: stats.count as u64,
260                    bytes: stats.total_bytes,
261                    pinned_count: stats.pinned_count as u64,
262                    pinned_bytes: stats.pinned_bytes,
263                },
264                Err(_) => StoreStats::default(),
265            },
266            #[cfg(feature = "lmdb")]
267            LocalStore::Lmdb(store) => match store.stats() {
268                Ok(stats) => StoreStats {
269                    count: stats.count as u64,
270                    bytes: stats.total_bytes,
271                    pinned_count: 0,
272                    pinned_bytes: 0,
273                },
274                Err(_) => StoreStats::default(),
275            },
276        }
277    }
278
279    async fn evict_if_needed(&self) -> std::result::Result<u64, StoreError> {
280        match self {
281            LocalStore::Fs(store) => store.evict_if_needed().await,
282            #[cfg(feature = "lmdb")]
283            LocalStore::Lmdb(store) => store.evict_if_needed().await,
284        }
285    }
286}
287
288/// Git storage backed by HashTree with configurable persistence
289pub struct GitStorage {
290    store: Arc<LocalStore>,
291    tree: HashTree<LocalStore>,
292    runtime: RuntimeExecutor,
293    /// In-memory state for the current session
294    objects: std::sync::RwLock<HashMap<String, Vec<u8>>>,
295    refs: std::sync::RwLock<HashMap<String, String>>,
296    /// Cached root CID (hash + encryption key)
297    root_cid: std::sync::RwLock<Option<Cid>>,
298}
299
300impl GitStorage {
301    /// Open or create a git storage at the given path
302    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
303        let config = Config::load_or_default();
304        let max_size_bytes = config
305            .storage
306            .max_size_gb
307            .saturating_mul(1024 * 1024 * 1024);
308        Self::open_with_max_bytes(path, max_size_bytes)
309    }
310
311    /// Open or create a git storage at the given path with an explicit byte limit.
312    pub fn open_with_max_bytes(path: impl AsRef<Path>, max_size_bytes: u64) -> Result<Self> {
313        let config = Config::load_or_default();
314        Self::open_with_backend_and_max_bytes(path, config.storage.backend, max_size_bytes)
315    }
316
317    pub fn open_with_backend_and_max_bytes(
318        path: impl AsRef<Path>,
319        backend: StorageBackend,
320        max_size_bytes: u64,
321    ) -> Result<Self> {
322        let runtime = match Handle::try_current() {
323            Ok(handle) => RuntimeExecutor::Handle(handle),
324            Err(_) => {
325                let rt = Runtime::new()
326                    .map_err(|e| Error::StorageError(format!("tokio runtime: {}", e)))?;
327                RuntimeExecutor::Owned(rt)
328            }
329        };
330
331        let store_path = path.as_ref().join("blobs");
332        let store = Arc::new(
333            LocalStore::new_for_backend(&store_path, backend, max_size_bytes)
334                .map_err(|e| Error::StorageError(format!("local store: {}", e)))?,
335        );
336
337        // Use encrypted mode (default) - blossom servers require encrypted data
338        let tree = HashTree::new(HashTreeConfig::new(store.clone()));
339
340        Ok(Self {
341            store,
342            tree,
343            runtime,
344            objects: std::sync::RwLock::new(HashMap::new()),
345            refs: std::sync::RwLock::new(HashMap::new()),
346            root_cid: std::sync::RwLock::new(None),
347        })
348    }
349
350    /// Evict old local blobs if storage is over the configured limit.
351    pub fn evict_if_needed(&self) -> Result<u64> {
352        self.runtime
353            .block_on(self.store.evict_if_needed())
354            .map_err(|e| Error::StorageError(format!("evict: {}", e)))
355    }
356
357    /// Write an object, returning its ID
358    fn write_object(&self, obj: &GitObject) -> Result<ObjectId> {
359        let oid = obj.id();
360        let key = oid.to_hex();
361
362        let loose = obj.to_loose_format();
363        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
364        encoder.write_all(&loose)?;
365        let compressed = encoder.finish()?;
366
367        let mut objects = self
368            .objects
369            .write()
370            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
371        objects.insert(key, compressed);
372
373        // Invalidate cached root
374        if let Ok(mut root) = self.root_cid.write() {
375            *root = None;
376        }
377
378        Ok(oid)
379    }
380
381    /// Write raw object data (type + content already parsed)
382    pub fn write_raw_object(&self, obj_type: ObjectType, content: &[u8]) -> Result<ObjectId> {
383        let obj = GitObject::new(obj_type, content.to_vec());
384        self.write_object(&obj)
385    }
386
387    /// Read an object by ID from in-memory cache
388    #[allow(dead_code)]
389    fn read_object(&self, oid: &ObjectId) -> Result<GitObject> {
390        let key = oid.to_hex();
391        let objects = self
392            .objects
393            .read()
394            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
395        let compressed = objects
396            .get(&key)
397            .ok_or_else(|| Error::ObjectNotFound(key.clone()))?;
398
399        let mut decoder = ZlibDecoder::new(compressed.as_slice());
400        let mut data = Vec::new();
401        decoder.read_to_end(&mut data)?;
402
403        GitObject::from_loose_format(&data)
404    }
405
406    /// Write a ref
407    pub fn write_ref(&self, name: &str, target: &Ref) -> Result<()> {
408        validate_ref_name(name)?;
409
410        let value = match target {
411            Ref::Direct(oid) => oid.to_hex(),
412            Ref::Symbolic(target) => format!("ref: {}", target),
413        };
414
415        let mut refs = self
416            .refs
417            .write()
418            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
419        refs.insert(name.to_string(), value);
420
421        // Invalidate cached root
422        if let Ok(mut root) = self.root_cid.write() {
423            *root = None;
424        }
425
426        Ok(())
427    }
428
429    /// Read a ref
430    #[allow(dead_code)]
431    pub fn read_ref(&self, name: &str) -> Result<Option<Ref>> {
432        let refs = self
433            .refs
434            .read()
435            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
436
437        match refs.get(name) {
438            Some(value) => {
439                if let Some(target) = value.strip_prefix("ref: ") {
440                    Ok(Some(Ref::Symbolic(target.to_string())))
441                } else {
442                    let oid = ObjectId::from_hex(value)
443                        .ok_or_else(|| Error::StorageError(format!("invalid ref: {}", value)))?;
444                    Ok(Some(Ref::Direct(oid)))
445                }
446            }
447            None => Ok(None),
448        }
449    }
450
451    /// List all refs
452    #[allow(dead_code)]
453    pub fn list_refs(&self) -> Result<HashMap<String, String>> {
454        let refs = self
455            .refs
456            .read()
457            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
458        Ok(refs.clone())
459    }
460
461    /// Delete a ref
462    pub fn delete_ref(&self, name: &str) -> Result<bool> {
463        let mut refs = self
464            .refs
465            .write()
466            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
467        let existed = refs.remove(name).is_some();
468
469        // Invalidate cached root
470        if let Ok(mut root) = self.root_cid.write() {
471            *root = None;
472        }
473
474        Ok(existed)
475    }
476
477    /// Import a raw git object (already in loose format, zlib compressed)
478    /// Used when fetching existing objects from remote before push
479    pub fn import_compressed_object(&self, oid: &str, compressed_data: Vec<u8>) -> Result<()> {
480        let mut objects = self
481            .objects
482            .write()
483            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
484        objects.insert(oid.to_string(), compressed_data);
485
486        // Invalidate cached root
487        if let Ok(mut root) = self.root_cid.write() {
488            *root = None;
489        }
490
491        Ok(())
492    }
493
494    /// Import a ref directly (used when loading existing refs from remote)
495    pub fn import_ref(&self, name: &str, value: &str) -> Result<()> {
496        let mut refs = self
497            .refs
498            .write()
499            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
500        refs.insert(name.to_string(), value.to_string());
501
502        // Invalidate cached root
503        if let Ok(mut root) = self.root_cid.write() {
504            *root = None;
505        }
506
507        Ok(())
508    }
509
510    /// Check if a ref exists
511    #[cfg(test)]
512    pub fn has_ref(&self, name: &str) -> Result<bool> {
513        let refs = self
514            .refs
515            .read()
516            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
517        Ok(refs.contains_key(name))
518    }
519
520    /// Get count of objects in storage
521    #[cfg(test)]
522    pub fn object_count(&self) -> Result<usize> {
523        let objects = self
524            .objects
525            .read()
526            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
527        Ok(objects.len())
528    }
529
530    /// Get the cached root CID (returns None if tree hasn't been built)
531    #[allow(dead_code)]
532    pub fn get_root_cid(&self) -> Result<Option<Cid>> {
533        let root = self
534            .root_cid
535            .read()
536            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
537        Ok(root.clone())
538    }
539
540    /// Get the default branch name
541    #[allow(dead_code)]
542    pub fn default_branch(&self) -> Result<Option<String>> {
543        let refs = self
544            .refs
545            .read()
546            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
547
548        if let Some(head) = refs.get("HEAD") {
549            if let Some(target) = head.strip_prefix("ref: ") {
550                return Ok(Some(target.to_string()));
551            }
552        }
553        Ok(None)
554    }
555
556    /// Get the tree SHA from a commit object
557    fn get_commit_tree(
558        &self,
559        commit_oid: &str,
560        objects: &HashMap<String, Vec<u8>>,
561    ) -> Option<String> {
562        let compressed = objects.get(commit_oid)?;
563
564        // Decompress the object
565        let mut decoder = ZlibDecoder::new(&compressed[..]);
566        let mut decompressed = Vec::new();
567        decoder.read_to_end(&mut decompressed).ok()?;
568
569        // Parse git object format: "type size\0content"
570        let null_pos = decompressed.iter().position(|&b| b == 0)?;
571        let content = &decompressed[null_pos + 1..];
572
573        // Parse commit content - first line is "tree <sha>"
574        let content_str = std::str::from_utf8(content).ok()?;
575        let first_line = content_str.lines().next()?;
576        first_line
577            .strip_prefix("tree ")
578            .map(|tree_hash| tree_hash.to_string())
579    }
580
581    /// Get git object content (decompressed, without header)
582    fn get_object_content(
583        &self,
584        oid: &str,
585        objects: &HashMap<String, Vec<u8>>,
586    ) -> Option<(ObjectType, Vec<u8>)> {
587        let compressed = objects.get(oid)?;
588        Self::parse_compressed_object(compressed)
589    }
590
591    fn parse_compressed_object(compressed: &[u8]) -> Option<(ObjectType, Vec<u8>)> {
592        let mut decoder = ZlibDecoder::new(compressed);
593        let mut decompressed = Vec::new();
594        decoder.read_to_end(&mut decompressed).ok()?;
595
596        // Parse git object format: "type size\0content"
597        let null_pos = decompressed.iter().position(|&b| b == 0)?;
598        let header = std::str::from_utf8(&decompressed[..null_pos]).ok()?;
599        let obj_type = if header.starts_with("blob") {
600            ObjectType::Blob
601        } else if header.starts_with("tree") {
602            ObjectType::Tree
603        } else if header.starts_with("commit") {
604            ObjectType::Commit
605        } else if header.starts_with("tag") {
606            ObjectType::Tag
607        } else {
608            return None;
609        };
610        let content = decompressed[null_pos + 1..].to_vec();
611        Some((obj_type, content))
612    }
613
614    async fn load_base_compressed_object<S: Store>(
615        &self,
616        oid: &str,
617        base_tree: &HashTree<S>,
618        base_objects_cid: &Cid,
619    ) -> Result<Option<Vec<u8>>> {
620        if oid.len() != 40 {
621            return Err(Error::ObjectNotFound(oid.to_string()));
622        }
623
624        let path = format!("{}/{}", &oid[..2], &oid[2..]);
625        let Some(object_cid) = base_tree
626            .resolve_path(base_objects_cid, &path)
627            .await
628            .map_err(|e| Error::StorageError(format!("resolve {} in base objects: {}", oid, e)))?
629        else {
630            return Ok(None);
631        };
632
633        base_tree
634            .get(&object_cid, None)
635            .await
636            .map_err(|e| Error::StorageError(format!("read {} from base objects: {}", oid, e)))
637    }
638
639    async fn get_object_content_from_base<S: Store>(
640        &self,
641        oid: &str,
642        base_tree: &HashTree<S>,
643        base_objects_cid: &Cid,
644    ) -> Result<Option<(ObjectType, Vec<u8>)>> {
645        let Some(compressed) = self
646            .load_base_compressed_object(oid, base_tree, base_objects_cid)
647            .await?
648        else {
649            return Ok(None);
650        };
651
652        Ok(Self::parse_compressed_object(&compressed))
653    }
654
655    async fn get_tree_entries_from_sources<S: Store>(
656        &self,
657        tree_oid: &str,
658        objects: &HashMap<String, Vec<u8>>,
659        base_tree: Option<&HashTree<S>>,
660        base_objects_cid: Option<&Cid>,
661    ) -> Result<Vec<super::object::TreeEntry>> {
662        let object = match self.get_object_content(tree_oid, objects) {
663            Some(object) => Some(object),
664            None => {
665                if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
666                    self.get_object_content_from_base(tree_oid, base_tree, base_objects_cid)
667                        .await?
668                } else {
669                    None
670                }
671            }
672        }
673        .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
674
675        if object.0 != ObjectType::Tree {
676            return Err(Error::InvalidObjectType(format!(
677                "expected tree, got {:?}",
678                object.0
679            )));
680        }
681
682        parse_tree(&object.1)
683    }
684
685    fn tree_entry_to_dir_entry(entry: &hashtree_core::TreeEntry) -> DirEntry {
686        DirEntry::from_cid(
687            &entry.name,
688            &Cid {
689                hash: entry.hash,
690                key: entry.key,
691            },
692        )
693        .with_size(entry.size)
694        .with_link_type(entry.link_type)
695    }
696
697    async fn import_object_from_base<S: Store>(
698        &self,
699        oid: &str,
700        objects: &mut HashMap<String, Vec<u8>>,
701        base_tree: &HashTree<S>,
702        base_objects_cid: &Cid,
703    ) -> Result<bool> {
704        if objects.contains_key(oid) {
705            return Ok(true);
706        }
707        let Some(compressed) = self
708            .load_base_compressed_object(oid, base_tree, base_objects_cid)
709            .await?
710        else {
711            return Ok(false);
712        };
713
714        objects.insert(oid.to_string(), compressed);
715        Ok(true)
716    }
717
718    fn seed_missing_object_from_base_boxed<'a, S: Store + 'a>(
719        &'a self,
720        oid: &'a str,
721        objects: &'a mut HashMap<String, Vec<u8>>,
722        base_tree: &'a HashTree<S>,
723        base_objects_cid: &'a Cid,
724    ) -> BoxFuture<'a, Result<bool>> {
725        Box::pin(self.import_object_from_base(oid, objects, base_tree, base_objects_cid))
726    }
727
728    fn peel_tag_target(&self, oid: &str, objects: &HashMap<String, Vec<u8>>) -> Option<String> {
729        let (obj_type, content) = self.get_object_content(oid, objects)?;
730        if obj_type != ObjectType::Tag {
731            return Some(oid.to_string());
732        }
733
734        let target = std::str::from_utf8(&content)
735            .ok()?
736            .lines()
737            .find_map(|line| line.strip_prefix("object "))
738            .map(str::trim)?
739            .to_string();
740
741        match self.get_object_content(&target, objects)?.0 {
742            ObjectType::Tag => self.peel_tag_target(&target, objects),
743            _ => Some(target),
744        }
745    }
746
747    fn build_info_refs_content(
748        &self,
749        refs: &HashMap<String, String>,
750        objects: &HashMap<String, Vec<u8>>,
751    ) -> String {
752        let mut lines = Vec::new();
753
754        for (name, value) in refs {
755            if name == "HEAD" {
756                continue;
757            }
758
759            let oid = value.trim().to_string();
760            lines.push((name.clone(), oid.clone()));
761
762            if name.starts_with("refs/tags/") {
763                if let Some(peeled) = self.peel_tag_target(&oid, objects) {
764                    if peeled != oid {
765                        lines.push((format!("{}^{{}}", name), peeled));
766                    }
767                }
768            }
769        }
770
771        lines.sort_by(|a, b| a.0.cmp(&b.0));
772
773        let mut content = String::new();
774        for (name, oid) in lines {
775            content.push_str(&oid);
776            content.push('\t');
777            content.push_str(&name);
778            content.push('\n');
779        }
780        content
781    }
782
783    async fn build_info_dir(
784        &self,
785        refs: &HashMap<String, String>,
786        objects: &HashMap<String, Vec<u8>>,
787    ) -> Result<Cid> {
788        let info_refs = self.build_info_refs_content(refs, objects);
789        let (info_refs_cid, info_refs_size) = self
790            .tree
791            .put(info_refs.as_bytes())
792            .await
793            .map_err(|e| Error::StorageError(format!("put info/refs: {}", e)))?;
794
795        self.tree
796            .put_directory(vec![
797                DirEntry::from_cid("refs", &info_refs_cid).with_size(info_refs_size)
798            ])
799            .await
800            .map_err(|e| Error::StorageError(format!("put info dir: {}", e)))
801    }
802
803    /// Build the hashtree and return the root CID (hash + encryption key)
804    pub fn build_tree(&self) -> Result<Cid> {
805        self.build_tree_with_base_objects::<LocalStore>(None, None, None)
806    }
807
808    pub fn validate_root_contains_direct_refs(&self, root_cid: &Cid) -> Result<()> {
809        let direct_refs: Vec<String> = {
810            let refs = self
811                .refs
812                .read()
813                .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
814            refs.values()
815                .filter(|value| {
816                    !value.starts_with("ref: ")
817                        && value.len() == 40
818                        && value.bytes().all(|byte| byte.is_ascii_hexdigit())
819                })
820                .cloned()
821                .collect()
822        };
823
824        if direct_refs.is_empty() {
825            return Ok(());
826        }
827
828        let objects_dir = self
829            .runtime
830            .block_on(self.tree.resolve_path(root_cid, ".git/objects"))
831            .map_err(|e| Error::StorageError(format!("resolve .git/objects: {}", e)))?;
832        if objects_dir.is_none() {
833            return Err(Error::StorageError(
834                "built root is missing .git/objects".to_string(),
835            ));
836        }
837
838        for oid in direct_refs {
839            let object_path = format!(".git/objects/{}/{}", &oid[..2], &oid[2..]);
840            let object_cid = self
841                .runtime
842                .block_on(self.tree.resolve_path(root_cid, &object_path))
843                .map_err(|e| Error::StorageError(format!("resolve {}: {}", object_path, e)))?;
844            if object_cid.is_none() {
845                return Err(Error::ObjectNotFound(oid));
846            }
847        }
848
849        Ok(())
850    }
851
852    pub fn build_tree_with_base_objects<S: Store>(
853        &self,
854        base_tree: Option<&HashTree<S>>,
855        base_root: Option<&Cid>,
856        base_commit_sha: Option<&str>,
857    ) -> Result<Cid> {
858        // Check if we have a cached root
859        if let Ok(root) = self.root_cid.read() {
860            if let Some(ref cid) = *root {
861                return Ok(cid.clone());
862            }
863        }
864
865        if let Err(err) = self.evict_if_needed() {
866            debug!("pre-build eviction skipped: {}", err);
867        }
868
869        let objects = self
870            .objects
871            .read()
872            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
873        let refs = self
874            .refs
875            .read()
876            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
877
878        // Get default branch from HEAD or find first branch ref
879        let (default_branch, commit_sha) = if let Some(head) = refs.get("HEAD") {
880            let branch = head.strip_prefix("ref: ").map(String::from);
881            let sha = branch.as_ref().and_then(|b| refs.get(b)).cloned();
882            (branch, sha)
883        } else {
884            // No HEAD ref - find first refs/heads/* ref directly
885            let mut branch_info: Option<(String, String)> = None;
886            for (ref_name, sha) in refs.iter() {
887                if ref_name.starts_with("refs/heads/") {
888                    branch_info = Some((ref_name.clone(), sha.clone()));
889                    break;
890                }
891            }
892            match branch_info {
893                Some((branch, sha)) => (Some(branch), Some(sha)),
894                None => (None, None),
895            }
896        };
897
898        // Get tree SHA from commit
899        let tree_sha = commit_sha
900            .as_ref()
901            .and_then(|sha| self.get_commit_tree(sha, &objects));
902
903        // Clone objects for async block
904        let mut objects_clone = objects.clone();
905
906        let base_objects_cid = if let (Some(base_tree), Some(base_root)) = (base_tree, base_root) {
907            self.runtime
908                .block_on(base_tree.resolve_path(base_root, ".git/objects"))
909                .map_err(|e| Error::StorageError(format!("resolve base .git/objects: {}", e)))?
910        } else {
911            None
912        };
913
914        let base_tree_sha =
915            if let (Some(base_tree), Some(base_objects_cid), Some(base_commit_sha)) =
916                (base_tree, base_objects_cid.as_ref(), base_commit_sha)
917            {
918                self.runtime.block_on(async {
919                    let Some((obj_type, content)) = self
920                        .get_object_content_from_base(base_commit_sha, base_tree, base_objects_cid)
921                        .await?
922                    else {
923                        return Ok(None);
924                    };
925
926                    if obj_type != ObjectType::Commit {
927                        return Err(Error::InvalidObjectType(format!(
928                            "expected commit, got {:?}",
929                            obj_type
930                        )));
931                    }
932
933                    let content = std::str::from_utf8(&content).map_err(|_| {
934                        Error::InvalidObjectFormat("commit: invalid utf-8".to_string())
935                    })?;
936                    Ok::<Option<String>, Error>(
937                        content
938                            .lines()
939                            .find_map(|line| line.strip_prefix("tree "))
940                            .map(str::trim)
941                            .map(str::to_string),
942                    )
943                })?
944            } else {
945                None
946            };
947
948        let base_root_entries = if let (Some(base_tree), Some(base_root)) = (base_tree, base_root) {
949            Some(
950                self.runtime
951                    .block_on(base_tree.list_directory(base_root))
952                    .map_err(|e| Error::StorageError(format!("list base root: {}", e)))?,
953            )
954        } else {
955            None
956        };
957
958        let root_cid = loop {
959            let build_result = self.runtime.block_on(async {
960                // Build objects directory
961                let objects_cid = self
962                    .build_objects_dir_with_base(&objects_clone, base_tree, base_objects_cid.as_ref())
963                    .await?;
964
965                // Build refs directory
966                let refs_cid = self.build_refs_dir(&refs).await?;
967
968                // Build dumb-HTTP info directory
969                let info_cid = self.build_info_dir(&refs, &objects_clone).await?;
970
971                // Build HEAD file - use default_branch if no explicit HEAD
972                // Git expects HEAD to end with newline, so add it if missing
973                let head_content = refs.get("HEAD")
974                    .map(|h| if h.ends_with('\n') { h.clone() } else { format!("{}\n", h) })
975                    .or_else(|| default_branch.as_ref().map(|b| format!("ref: {}\n", b)))
976                    .unwrap_or_else(|| "ref: refs/heads/main\n".to_string());
977                debug!("HEAD content: {:?}", head_content);
978                let (head_cid, head_size) = self.tree.put(head_content.as_bytes()).await
979                    .map_err(|e| Error::StorageError(format!("put HEAD: {}", e)))?;
980                debug!("HEAD hash: {}", hex::encode(head_cid.hash));
981
982                // Build .git directory - use from_cid to preserve encryption keys
983                let mut git_entries = vec![
984                    DirEntry::from_cid("HEAD", &head_cid).with_size(head_size),
985                    DirEntry::from_cid("info", &info_cid).with_link_type(LinkType::Dir),
986                    DirEntry::from_cid("objects", &objects_cid).with_link_type(LinkType::Dir),
987                    DirEntry::from_cid("refs", &refs_cid).with_link_type(LinkType::Dir),
988                ];
989
990                // Add config if we have a default branch
991                if let Some(ref branch) = default_branch {
992                    let config = format!(
993                        "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n[init]\n\tdefaultBranch = {}\n",
994                        branch.trim_start_matches("refs/heads/")
995                    );
996                    let (config_cid, config_size) = self.tree.put(config.as_bytes()).await
997                        .map_err(|e| Error::StorageError(format!("put config: {}", e)))?;
998                    git_entries.push(DirEntry::from_cid("config", &config_cid).with_size(config_size));
999                }
1000
1001                // Build and add index file if we have a tree SHA
1002                if let Some(ref tree_oid) = tree_sha {
1003                    let index_result = if let (Some(base_tree), Some(base_objects_cid), Some(base_tree_sha)) =
1004                        (base_tree, base_objects_cid.as_ref(), base_tree_sha.as_deref())
1005                    {
1006                        self.build_index_file_with_base(
1007                            tree_oid,
1008                            &objects_clone,
1009                            base_tree,
1010                            base_objects_cid,
1011                            base_tree_sha,
1012                            base_root_entries.as_ref(),
1013                        )
1014                        .await
1015                    } else {
1016                        self.build_index_file(tree_oid, &objects_clone)
1017                    };
1018                    match index_result {
1019                        Ok(index_data) => {
1020                            let (index_cid, index_size) = self.tree.put(&index_data).await
1021                                .map_err(|e| Error::StorageError(format!("put index: {}", e)))?;
1022                            git_entries.push(DirEntry::from_cid("index", &index_cid).with_size(index_size));
1023                            info!("Added git index file ({} bytes)", index_data.len());
1024                        }
1025                        Err(e) => {
1026                            debug!("Failed to build git index file: {} - continuing without index", e);
1027                        }
1028                    }
1029                }
1030
1031                let git_cid = self.tree.put_directory(git_entries).await
1032                    .map_err(|e| Error::StorageError(format!("put .git: {}", e)))?;
1033
1034                // Build root entries starting with .git
1035                // Use from_cid to preserve the encryption key
1036                let mut root_entries = vec![DirEntry::from_cid(".git", &git_cid).with_link_type(LinkType::Dir)];
1037
1038                // Add working tree files if we have a tree SHA
1039                if let Some(ref tree_oid) = tree_sha {
1040                    let working_tree_entries = if let (Some(base_tree), Some(base_objects_cid), Some(base_tree_sha)) =
1041                        (base_tree, base_objects_cid.as_ref(), base_tree_sha.as_deref())
1042                    {
1043                        self.build_working_tree_entries_with_base(
1044                            tree_oid,
1045                            &objects_clone,
1046                            base_tree,
1047                            base_objects_cid,
1048                            base_tree_sha,
1049                            base_root_entries.as_ref(),
1050                        )
1051                        .await?
1052                    } else {
1053                        self.build_working_tree_entries(tree_oid, &objects_clone).await?
1054                    };
1055                    root_entries.extend(working_tree_entries);
1056                    info!("Added {} working tree entries to root", root_entries.len() - 1);
1057                }
1058
1059                // Sort entries for deterministic ordering
1060                root_entries.sort_by(|a, b| a.name.cmp(&b.name));
1061
1062                let root_cid = self.tree.put_directory(root_entries).await
1063                    .map_err(|e| Error::StorageError(format!("put root: {}", e)))?;
1064
1065                info!("Built hashtree root: {} (encrypted: {}) (.git dir: {})",
1066                    hex::encode(root_cid.hash),
1067                    root_cid.key.is_some(),
1068                    hex::encode(git_cid.hash));
1069
1070                Ok::<Cid, Error>(root_cid)
1071            });
1072
1073            match build_result {
1074                Ok(root_cid) => break root_cid,
1075                Err(Error::ObjectNotFound(oid))
1076                    if base_tree.is_some() && base_objects_cid.is_some() =>
1077                {
1078                    let imported =
1079                        self.runtime
1080                            .block_on(self.seed_missing_object_from_base_boxed(
1081                                &oid,
1082                                &mut objects_clone,
1083                                base_tree.expect("checked is_some"),
1084                                base_objects_cid.as_ref().expect("checked is_some"),
1085                            ))?;
1086                    if imported {
1087                        continue;
1088                    }
1089                    return Err(Error::ObjectNotFound(oid).into());
1090                }
1091                Err(err) => return Err(err.into()),
1092            }
1093        };
1094
1095        // Cache the root CID
1096        if let Ok(mut root) = self.root_cid.write() {
1097            *root = Some(root_cid.clone());
1098        }
1099
1100        Ok(root_cid)
1101    }
1102
1103    /// Build working tree entries from a git tree object
1104    async fn build_working_tree_entries(
1105        &self,
1106        tree_oid: &str,
1107        objects: &HashMap<String, Vec<u8>>,
1108    ) -> Result<Vec<DirEntry>> {
1109        let mut entries = Vec::new();
1110
1111        // Get tree content
1112        let (obj_type, content) = self
1113            .get_object_content(tree_oid, objects)
1114            .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
1115
1116        if obj_type != ObjectType::Tree {
1117            return Err(Error::InvalidObjectType(format!(
1118                "expected tree, got {:?}",
1119                obj_type
1120            )));
1121        }
1122
1123        // Parse tree entries
1124        let tree_entries = parse_tree(&content)?;
1125
1126        for entry in tree_entries {
1127            let oid_hex = entry.oid.to_hex();
1128
1129            if entry.is_tree() {
1130                // Recursively build subdirectory
1131                let sub_entries = self
1132                    .build_working_tree_entries_boxed(&oid_hex, objects)
1133                    .await?;
1134
1135                // Create subdirectory in hashtree
1136                let dir_cid =
1137                    self.tree.put_directory(sub_entries).await.map_err(|e| {
1138                        Error::StorageError(format!("put dir {}: {}", entry.name, e))
1139                    })?;
1140
1141                // Use from_cid to preserve encryption key
1142                entries
1143                    .push(DirEntry::from_cid(&entry.name, &dir_cid).with_link_type(LinkType::Dir));
1144            } else {
1145                // Get blob content
1146                if let Some((ObjectType::Blob, blob_content)) =
1147                    self.get_object_content(&oid_hex, objects)
1148                {
1149                    // Use put() instead of put_blob() to chunk large files
1150                    let (cid, size) = self.tree.put(&blob_content).await.map_err(|e| {
1151                        Error::StorageError(format!("put blob {}: {}", entry.name, e))
1152                    })?;
1153
1154                    // Use from_cid to preserve encryption key
1155                    entries.push(DirEntry::from_cid(&entry.name, &cid).with_size(size));
1156                }
1157            }
1158        }
1159
1160        // Sort for deterministic ordering
1161        entries.sort_by(|a, b| a.name.cmp(&b.name));
1162
1163        Ok(entries)
1164    }
1165
1166    /// Boxed version for async recursion
1167    fn build_working_tree_entries_boxed<'a>(
1168        &'a self,
1169        tree_oid: &'a str,
1170        objects: &'a HashMap<String, Vec<u8>>,
1171    ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1172        Box::pin(self.build_working_tree_entries(tree_oid, objects))
1173    }
1174
1175    fn build_working_tree_entries_with_base_recursive_boxed<'a, S: Store + 'a>(
1176        &'a self,
1177        tree_oid: &'a str,
1178        objects: &'a HashMap<String, Vec<u8>>,
1179        base_tree: &'a HashTree<S>,
1180        base_objects_cid: &'a Cid,
1181        old_tree_oid: Option<&'a str>,
1182        old_dir_entries: Option<&'a Vec<hashtree_core::TreeEntry>>,
1183    ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1184        Box::pin(async move {
1185            let tree_entries = self
1186                .get_tree_entries_from_sources(
1187                    tree_oid,
1188                    objects,
1189                    Some(base_tree),
1190                    Some(base_objects_cid),
1191                )
1192                .await?;
1193            let old_tree_entries = if let Some(old_tree_oid) = old_tree_oid {
1194                Some(
1195                    self.get_tree_entries_from_sources(
1196                        old_tree_oid,
1197                        objects,
1198                        Some(base_tree),
1199                        Some(base_objects_cid),
1200                    )
1201                    .await?,
1202                )
1203            } else {
1204                None
1205            };
1206
1207            let mut old_tree_entry_map = old_tree_entries
1208                .unwrap_or_default()
1209                .into_iter()
1210                .map(|entry| (entry.name.clone(), entry))
1211                .collect::<HashMap<_, _>>();
1212            let mut old_dir_entry_map = old_dir_entries
1213                .cloned()
1214                .unwrap_or_default()
1215                .into_iter()
1216                .map(|entry| (entry.name.clone(), entry))
1217                .collect::<HashMap<_, _>>();
1218
1219            let mut entries = Vec::new();
1220            for entry in tree_entries {
1221                let oid_hex = entry.oid.to_hex();
1222                let old_tree_entry = old_tree_entry_map.remove(&entry.name);
1223                let old_dir_entry = old_dir_entry_map.remove(&entry.name);
1224
1225                if entry.is_tree() {
1226                    if let (Some(old_tree_entry), Some(old_dir_entry)) =
1227                        (old_tree_entry.as_ref(), old_dir_entry.as_ref())
1228                    {
1229                        if old_tree_entry.is_tree()
1230                            && old_tree_entry.oid == entry.oid
1231                            && old_dir_entry.link_type == LinkType::Dir
1232                        {
1233                            entries.push(Self::tree_entry_to_dir_entry(old_dir_entry));
1234                            continue;
1235                        }
1236                    }
1237
1238                    let old_subtree_oid = old_tree_entry
1239                        .as_ref()
1240                        .filter(|old| old.is_tree())
1241                        .map(|old| old.oid.to_hex());
1242                    let old_subdir_entries = if let Some(old_dir_entry) = old_dir_entry.as_ref() {
1243                        if old_dir_entry.link_type == LinkType::Dir {
1244                            Some(
1245                                base_tree
1246                                    .list_directory(&Cid {
1247                                        hash: old_dir_entry.hash,
1248                                        key: old_dir_entry.key,
1249                                    })
1250                                    .await
1251                                    .map_err(|e| {
1252                                        Error::StorageError(format!(
1253                                            "list base working dir {}: {}",
1254                                            entry.name, e
1255                                        ))
1256                                    })?,
1257                            )
1258                        } else {
1259                            None
1260                        }
1261                    } else {
1262                        None
1263                    };
1264
1265                    let sub_entries = self
1266                        .build_working_tree_entries_with_base_recursive_boxed(
1267                            &oid_hex,
1268                            objects,
1269                            base_tree,
1270                            base_objects_cid,
1271                            old_subtree_oid.as_deref(),
1272                            old_subdir_entries.as_ref(),
1273                        )
1274                        .await?;
1275                    let dir_cid = self.tree.put_directory(sub_entries).await.map_err(|e| {
1276                        Error::StorageError(format!("put dir {}: {}", entry.name, e))
1277                    })?;
1278                    entries.push(
1279                        DirEntry::from_cid(&entry.name, &dir_cid).with_link_type(LinkType::Dir),
1280                    );
1281                    continue;
1282                }
1283
1284                if let (Some(old_tree_entry), Some(old_dir_entry)) =
1285                    (old_tree_entry.as_ref(), old_dir_entry.as_ref())
1286                {
1287                    if !old_tree_entry.is_tree()
1288                        && old_tree_entry.oid == entry.oid
1289                        && old_dir_entry.link_type != LinkType::Dir
1290                    {
1291                        entries.push(Self::tree_entry_to_dir_entry(old_dir_entry));
1292                        continue;
1293                    }
1294                }
1295
1296                let blob_content = match self.get_object_content(&oid_hex, objects) {
1297                    Some((ObjectType::Blob, blob_content)) => blob_content,
1298                    Some((obj_type, _)) => {
1299                        return Err(Error::InvalidObjectType(format!(
1300                            "expected blob, got {:?}",
1301                            obj_type
1302                        )));
1303                    }
1304                    None => match self
1305                        .get_object_content_from_base(&oid_hex, base_tree, base_objects_cid)
1306                        .await?
1307                    {
1308                        Some((ObjectType::Blob, blob_content)) => blob_content,
1309                        Some((obj_type, _)) => {
1310                            return Err(Error::InvalidObjectType(format!(
1311                                "expected blob, got {:?}",
1312                                obj_type
1313                            )));
1314                        }
1315                        None => return Err(Error::ObjectNotFound(oid_hex)),
1316                    },
1317                };
1318
1319                let (cid, size) =
1320                    self.tree.put(&blob_content).await.map_err(|e| {
1321                        Error::StorageError(format!("put blob {}: {}", entry.name, e))
1322                    })?;
1323                entries.push(DirEntry::from_cid(&entry.name, &cid).with_size(size));
1324            }
1325
1326            entries.sort_by(|a, b| a.name.cmp(&b.name));
1327            Ok(entries)
1328        })
1329    }
1330
1331    async fn build_working_tree_entries_with_base<S: Store>(
1332        &self,
1333        tree_oid: &str,
1334        objects: &HashMap<String, Vec<u8>>,
1335        base_tree: &HashTree<S>,
1336        base_objects_cid: &Cid,
1337        base_tree_oid: &str,
1338        base_root_entries: Option<&Vec<hashtree_core::TreeEntry>>,
1339    ) -> Result<Vec<DirEntry>> {
1340        let root_entries = base_root_entries.map(|entries| {
1341            entries
1342                .iter()
1343                .filter(|entry| entry.name != ".git")
1344                .cloned()
1345                .collect::<Vec<_>>()
1346        });
1347
1348        self.build_working_tree_entries_with_base_recursive_boxed(
1349            tree_oid,
1350            objects,
1351            base_tree,
1352            base_objects_cid,
1353            Some(base_tree_oid),
1354            root_entries.as_ref(),
1355        )
1356        .await
1357    }
1358
1359    async fn build_objects_prefix_dir(
1360        &self,
1361        prefix: &str,
1362        old_entries: Option<Vec<hashtree_core::TreeEntry>>,
1363        new_objects: &[(String, Vec<u8>)],
1364    ) -> Result<Cid> {
1365        let mut sub_entries: BTreeMap<String, DirEntry> = old_entries
1366            .unwrap_or_default()
1367            .into_iter()
1368            .map(|entry| (entry.name.clone(), Self::tree_entry_to_dir_entry(&entry)))
1369            .collect();
1370
1371        for (suffix, data) in new_objects {
1372            let (cid, size) = self.tree.put(data).await.map_err(|e| {
1373                Error::StorageError(format!("put object {}{}: {}", prefix, suffix, e))
1374            })?;
1375            sub_entries.insert(
1376                suffix.clone(),
1377                DirEntry::from_cid(suffix, &cid).with_size(size),
1378            );
1379        }
1380
1381        self.tree
1382            .put_directory(sub_entries.into_values().collect())
1383            .await
1384            .map_err(|e| Error::StorageError(format!("put objects/{}: {}", prefix, e)))
1385    }
1386
1387    /// Build the objects directory using HashTree, reusing unchanged prefix directories
1388    /// from an older root when available.
1389    async fn build_objects_dir_with_base<S: Store>(
1390        &self,
1391        objects: &HashMap<String, Vec<u8>>,
1392        base_tree: Option<&HashTree<S>>,
1393        base_objects_cid: Option<&Cid>,
1394    ) -> Result<Cid> {
1395        let mut buckets: BTreeMap<String, Vec<(String, Vec<u8>)>> = BTreeMap::new();
1396        for (oid, data) in objects {
1397            let prefix = &oid[..2];
1398            let suffix = &oid[2..];
1399            buckets
1400                .entry(prefix.to_string())
1401                .or_default()
1402                .push((suffix.to_string(), data.clone()));
1403        }
1404
1405        let mut top_entries: BTreeMap<String, DirEntry> = BTreeMap::new();
1406        let mut merged_prefixes = std::collections::HashSet::new();
1407
1408        if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
1409            for entry in base_tree
1410                .list_directory(base_objects_cid)
1411                .await
1412                .map_err(|e| Error::StorageError(format!("list base objects dir: {}", e)))?
1413            {
1414                if entry.name == "info" {
1415                    top_entries.insert(entry.name.clone(), Self::tree_entry_to_dir_entry(&entry));
1416                    continue;
1417                }
1418
1419                let Some(delta_objects) = buckets.get(&entry.name) else {
1420                    top_entries.insert(entry.name.clone(), Self::tree_entry_to_dir_entry(&entry));
1421                    continue;
1422                };
1423
1424                let prefix_cid = Cid {
1425                    hash: entry.hash,
1426                    key: entry.key,
1427                };
1428                let old_prefix_entries = if entry.link_type == LinkType::Dir {
1429                    Some(base_tree.list_directory(&prefix_cid).await.map_err(|e| {
1430                        Error::StorageError(format!("list base objects/{} dir: {}", entry.name, e))
1431                    })?)
1432                } else {
1433                    None
1434                };
1435                let merged_cid = self
1436                    .build_objects_prefix_dir(&entry.name, old_prefix_entries, delta_objects)
1437                    .await?;
1438                top_entries.insert(
1439                    entry.name.clone(),
1440                    DirEntry::from_cid(&entry.name, &merged_cid).with_link_type(LinkType::Dir),
1441                );
1442                merged_prefixes.insert(entry.name);
1443            }
1444        }
1445
1446        for (prefix, objs) in &buckets {
1447            if merged_prefixes.contains(prefix) {
1448                continue;
1449            }
1450            let sub_cid = self.build_objects_prefix_dir(prefix, None, objs).await?;
1451            top_entries.insert(
1452                prefix.clone(),
1453                DirEntry::from_cid(prefix, &sub_cid).with_link_type(LinkType::Dir),
1454            );
1455        }
1456
1457        if !top_entries.contains_key("info") {
1458            let (packs_cid, packs_size) = self
1459                .tree
1460                .put(b"")
1461                .await
1462                .map_err(|e| Error::StorageError(format!("put objects/info/packs: {}", e)))?;
1463            let info_cid = self
1464                .tree
1465                .put_directory(vec![
1466                    DirEntry::from_cid("packs", &packs_cid).with_size(packs_size)
1467                ])
1468                .await
1469                .map_err(|e| Error::StorageError(format!("put objects/info: {}", e)))?;
1470            top_entries.insert(
1471                "info".to_string(),
1472                DirEntry::from_cid("info", &info_cid).with_link_type(LinkType::Dir),
1473            );
1474        }
1475
1476        let entry_count = top_entries.len();
1477        let cid = self
1478            .tree
1479            .put_directory(top_entries.into_values().collect())
1480            .await
1481            .map_err(|e| Error::StorageError(format!("put objects dir: {}", e)))?;
1482
1483        debug!(
1484            "Built objects dir with {} entries: {}",
1485            entry_count,
1486            hex::encode(cid.hash)
1487        );
1488        Ok(cid)
1489    }
1490
1491    /// Build the refs directory using HashTree
1492    async fn build_refs_dir(&self, refs: &HashMap<String, String>) -> Result<Cid> {
1493        let mut root = RefDirectory::default();
1494
1495        for (ref_name, value) in refs {
1496            let parts: Vec<&str> = ref_name.split('/').collect();
1497            if parts.len() >= 3 && parts[0] == "refs" {
1498                root.insert(&parts[1..], value.clone());
1499            }
1500        }
1501
1502        let mut ref_entries = self.build_ref_entries_recursive(&root, "refs").await?;
1503
1504        if ref_entries.is_empty() {
1505            // Return empty directory Cid
1506            let empty_cid = self
1507                .tree
1508                .put_directory(vec![])
1509                .await
1510                .map_err(|e| Error::StorageError(format!("put empty refs: {}", e)))?;
1511            return Ok(empty_cid);
1512        }
1513
1514        ref_entries.sort_by(|a, b| a.name.cmp(&b.name));
1515
1516        let refs_cid = self
1517            .tree
1518            .put_directory(ref_entries)
1519            .await
1520            .map_err(|e| Error::StorageError(format!("put refs dir: {}", e)))?;
1521        debug!("refs dir -> {}", hex::encode(refs_cid.hash));
1522        Ok(refs_cid)
1523    }
1524
1525    fn build_ref_entries_recursive<'a>(
1526        &'a self,
1527        dir: &'a RefDirectory,
1528        prefix: &'a str,
1529    ) -> BoxFuture<'a, Result<Vec<DirEntry>>> {
1530        Box::pin(async move {
1531            let mut entries = Vec::new();
1532
1533            for (name, value) in &dir.files {
1534                let (cid, size) = self
1535                    .tree
1536                    .put(value.as_bytes())
1537                    .await
1538                    .map_err(|e| Error::StorageError(format!("put ref: {}", e)))?;
1539                debug!("{}/{} -> blob {}", prefix, name, hex::encode(cid.hash));
1540                entries.push(DirEntry::from_cid(name, &cid).with_size(size));
1541            }
1542
1543            for (name, child) in &dir.dirs {
1544                let child_prefix = format!("{prefix}/{name}");
1545                let child_entries = self
1546                    .build_ref_entries_recursive(child, &child_prefix)
1547                    .await?;
1548                let child_cid =
1549                    self.tree.put_directory(child_entries).await.map_err(|e| {
1550                        Error::StorageError(format!("put {child_prefix} dir: {}", e))
1551                    })?;
1552                debug!("{} dir -> {}", child_prefix, hex::encode(child_cid.hash));
1553                entries.push(DirEntry::from_cid(name, &child_cid).with_link_type(LinkType::Dir));
1554            }
1555
1556            entries.sort_by(|a, b| a.name.cmp(&b.name));
1557            Ok(entries)
1558        })
1559    }
1560
1561    /// Build git index file from tree entries
1562    /// Returns the raw binary content of the index file
1563    fn build_index_file(
1564        &self,
1565        tree_oid: &str,
1566        objects: &HashMap<String, Vec<u8>>,
1567    ) -> Result<Vec<u8>> {
1568        // Collect all file entries from the tree (recursively)
1569        let mut entries: Vec<(String, [u8; 20], u32, u32)> = Vec::new(); // (path, sha1, mode, size)
1570        self.collect_tree_entries_for_index(tree_oid, objects, "", &mut entries)?;
1571
1572        self.build_index_bytes(entries)
1573    }
1574
1575    async fn build_index_file_with_base<S: Store>(
1576        &self,
1577        tree_oid: &str,
1578        objects: &HashMap<String, Vec<u8>>,
1579        base_tree: &HashTree<S>,
1580        base_objects_cid: &Cid,
1581        base_tree_oid: &str,
1582        base_root_entries: Option<&Vec<hashtree_core::TreeEntry>>,
1583    ) -> Result<Vec<u8>> {
1584        let mut entries: Vec<(String, [u8; 20], u32, u32)> = Vec::new();
1585        let root_entries = base_root_entries.map(|entries| {
1586            entries
1587                .iter()
1588                .filter(|entry| entry.name != ".git")
1589                .cloned()
1590                .collect::<Vec<_>>()
1591        });
1592
1593        self.collect_tree_entries_for_index_with_base_boxed(
1594            tree_oid,
1595            objects,
1596            base_tree,
1597            base_objects_cid,
1598            Some(base_tree_oid),
1599            root_entries.as_ref(),
1600            "",
1601            &mut entries,
1602        )
1603        .await?;
1604
1605        self.build_index_bytes(entries)
1606    }
1607
1608    fn build_index_bytes(&self, mut entries: Vec<(String, [u8; 20], u32, u32)>) -> Result<Vec<u8>> {
1609        // Sort entries by path (git index requirement)
1610        entries.sort_by(|a, b| a.0.cmp(&b.0));
1611
1612        let entry_count = entries.len() as u32;
1613        debug!("Building git index with {} entries", entry_count);
1614
1615        // Build index content
1616        let mut index_data = Vec::new();
1617
1618        // Header: DIRC + version 2 + entry count
1619        index_data.extend_from_slice(b"DIRC");
1620        index_data.extend_from_slice(&2u32.to_be_bytes()); // version 2
1621        index_data.extend_from_slice(&entry_count.to_be_bytes());
1622
1623        // Current time for ctime/mtime (doesn't matter much for our use case)
1624        let now_sec = std::time::SystemTime::now()
1625            .duration_since(std::time::UNIX_EPOCH)
1626            .unwrap_or_default()
1627            .as_secs() as u32;
1628
1629        for (path, sha1, mode, size) in &entries {
1630            let entry_start = index_data.len();
1631
1632            // ctime sec, nsec
1633            index_data.extend_from_slice(&now_sec.to_be_bytes());
1634            index_data.extend_from_slice(&0u32.to_be_bytes());
1635            // mtime sec, nsec
1636            index_data.extend_from_slice(&now_sec.to_be_bytes());
1637            index_data.extend_from_slice(&0u32.to_be_bytes());
1638            // dev, ino (use 0)
1639            index_data.extend_from_slice(&0u32.to_be_bytes());
1640            index_data.extend_from_slice(&0u32.to_be_bytes());
1641            // mode
1642            index_data.extend_from_slice(&mode.to_be_bytes());
1643            // uid, gid (use 0)
1644            index_data.extend_from_slice(&0u32.to_be_bytes());
1645            index_data.extend_from_slice(&0u32.to_be_bytes());
1646            // file size
1647            index_data.extend_from_slice(&size.to_be_bytes());
1648            // SHA-1
1649            index_data.extend_from_slice(sha1);
1650            // flags: path length (max 0xFFF) in low 12 bits
1651            let path_len = std::cmp::min(path.len(), 0xFFF) as u16;
1652            index_data.extend_from_slice(&path_len.to_be_bytes());
1653            // path (NUL-terminated)
1654            index_data.extend_from_slice(path.as_bytes());
1655            index_data.push(0); // NUL terminator
1656
1657            // Pad to 8-byte boundary relative to entry start
1658            let entry_len = index_data.len() - entry_start;
1659            let padding = (8 - (entry_len % 8)) % 8;
1660            index_data.extend(std::iter::repeat_n(0, padding));
1661        }
1662
1663        // Calculate SHA-1 checksum of everything and append
1664        let mut hasher = Sha1::new();
1665        hasher.update(&index_data);
1666        let checksum = hasher.finalize();
1667        index_data.extend_from_slice(&checksum);
1668
1669        debug!(
1670            "Built git index: {} bytes, {} entries",
1671            index_data.len(),
1672            entry_count
1673        );
1674        Ok(index_data)
1675    }
1676
1677    /// Collect file entries from a git tree for building the index
1678    fn collect_tree_entries_for_index(
1679        &self,
1680        tree_oid: &str,
1681        objects: &HashMap<String, Vec<u8>>,
1682        prefix: &str,
1683        entries: &mut Vec<(String, [u8; 20], u32, u32)>,
1684    ) -> Result<()> {
1685        let (obj_type, content) = self
1686            .get_object_content(tree_oid, objects)
1687            .ok_or_else(|| Error::ObjectNotFound(tree_oid.to_string()))?;
1688
1689        if obj_type != ObjectType::Tree {
1690            return Err(Error::InvalidObjectType(format!(
1691                "expected tree, got {:?}",
1692                obj_type
1693            )));
1694        }
1695
1696        let tree_entries = parse_tree(&content)?;
1697
1698        for entry in tree_entries {
1699            let path = if prefix.is_empty() {
1700                entry.name.clone()
1701            } else {
1702                format!("{}/{}", prefix, entry.name)
1703            };
1704
1705            let oid_hex = entry.oid.to_hex();
1706
1707            if entry.is_tree() {
1708                // Recursively process subdirectory
1709                self.collect_tree_entries_for_index(&oid_hex, objects, &path, entries)?;
1710            } else {
1711                // Get blob content for size and SHA-1
1712                if let Some((ObjectType::Blob, blob_content)) =
1713                    self.get_object_content(&oid_hex, objects)
1714                {
1715                    // Convert hex SHA to bytes
1716                    let mut sha1_bytes = [0u8; 20];
1717                    if let Ok(bytes) = hex::decode(&oid_hex) {
1718                        if bytes.len() == 20 {
1719                            sha1_bytes.copy_from_slice(&bytes);
1720                        }
1721                    }
1722
1723                    // Mode: use entry.mode or default to regular file
1724                    let mode = entry.mode;
1725                    let size = blob_content.len() as u32;
1726
1727                    entries.push((path, sha1_bytes, mode, size));
1728                }
1729            }
1730        }
1731
1732        Ok(())
1733    }
1734
1735    fn collect_tree_entries_for_index_with_base_boxed<'a, S: Store + 'a>(
1736        &'a self,
1737        tree_oid: &'a str,
1738        objects: &'a HashMap<String, Vec<u8>>,
1739        base_tree: &'a HashTree<S>,
1740        base_objects_cid: &'a Cid,
1741        old_tree_oid: Option<&'a str>,
1742        old_dir_entries: Option<&'a Vec<hashtree_core::TreeEntry>>,
1743        prefix: &'a str,
1744        entries: &'a mut Vec<(String, [u8; 20], u32, u32)>,
1745    ) -> BoxFuture<'a, Result<()>> {
1746        Box::pin(async move {
1747            let tree_entries = self
1748                .get_tree_entries_from_sources(
1749                    tree_oid,
1750                    objects,
1751                    Some(base_tree),
1752                    Some(base_objects_cid),
1753                )
1754                .await?;
1755            let old_tree_entries = if let Some(old_tree_oid) = old_tree_oid {
1756                Some(
1757                    self.get_tree_entries_from_sources(
1758                        old_tree_oid,
1759                        objects,
1760                        Some(base_tree),
1761                        Some(base_objects_cid),
1762                    )
1763                    .await?,
1764                )
1765            } else {
1766                None
1767            };
1768            let mut old_tree_entry_map = old_tree_entries
1769                .unwrap_or_default()
1770                .into_iter()
1771                .map(|entry| (entry.name.clone(), entry))
1772                .collect::<HashMap<_, _>>();
1773            let mut old_dir_entry_map = old_dir_entries
1774                .cloned()
1775                .unwrap_or_default()
1776                .into_iter()
1777                .map(|entry| (entry.name.clone(), entry))
1778                .collect::<HashMap<_, _>>();
1779
1780            for entry in tree_entries {
1781                let path = if prefix.is_empty() {
1782                    entry.name.clone()
1783                } else {
1784                    format!("{}/{}", prefix, entry.name)
1785                };
1786                let oid_hex = entry.oid.to_hex();
1787                let old_tree_entry = old_tree_entry_map.remove(&entry.name);
1788                let old_dir_entry = old_dir_entry_map.remove(&entry.name);
1789
1790                if entry.is_tree() {
1791                    let old_subtree_oid = old_tree_entry
1792                        .as_ref()
1793                        .filter(|old| old.is_tree())
1794                        .map(|old| old.oid.to_hex());
1795                    let old_subdir_entries = if let Some(old_dir_entry) = old_dir_entry.as_ref() {
1796                        if old_dir_entry.link_type == LinkType::Dir {
1797                            Some(
1798                                base_tree
1799                                    .list_directory(&Cid {
1800                                        hash: old_dir_entry.hash,
1801                                        key: old_dir_entry.key,
1802                                    })
1803                                    .await
1804                                    .map_err(|e| {
1805                                        Error::StorageError(format!(
1806                                            "list base working dir {}: {}",
1807                                            path, e
1808                                        ))
1809                                    })?,
1810                            )
1811                        } else {
1812                            None
1813                        }
1814                    } else {
1815                        None
1816                    };
1817
1818                    self.collect_tree_entries_for_index_with_base_boxed(
1819                        &oid_hex,
1820                        objects,
1821                        base_tree,
1822                        base_objects_cid,
1823                        old_subtree_oid.as_deref(),
1824                        old_subdir_entries.as_ref(),
1825                        &path,
1826                        entries,
1827                    )
1828                    .await?;
1829                    continue;
1830                }
1831
1832                let mut sha1_bytes = [0u8; 20];
1833                if let Ok(bytes) = hex::decode(&oid_hex) {
1834                    if bytes.len() == 20 {
1835                        sha1_bytes.copy_from_slice(&bytes);
1836                    }
1837                }
1838
1839                let size = if let (Some(old_tree_entry), Some(old_dir_entry)) =
1840                    (old_tree_entry.as_ref(), old_dir_entry.as_ref())
1841                {
1842                    if !old_tree_entry.is_tree()
1843                        && old_tree_entry.oid == entry.oid
1844                        && old_dir_entry.link_type != LinkType::Dir
1845                    {
1846                        old_dir_entry.size as u32
1847                    } else {
1848                        self.blob_size_from_sources(
1849                            &oid_hex,
1850                            objects,
1851                            Some(base_tree),
1852                            Some(base_objects_cid),
1853                        )
1854                        .await? as u32
1855                    }
1856                } else {
1857                    self.blob_size_from_sources(
1858                        &oid_hex,
1859                        objects,
1860                        Some(base_tree),
1861                        Some(base_objects_cid),
1862                    )
1863                    .await? as u32
1864                };
1865
1866                entries.push((path, sha1_bytes, entry.mode, size));
1867            }
1868
1869            Ok(())
1870        })
1871    }
1872
1873    async fn blob_size_from_sources<S: Store>(
1874        &self,
1875        oid: &str,
1876        objects: &HashMap<String, Vec<u8>>,
1877        base_tree: Option<&HashTree<S>>,
1878        base_objects_cid: Option<&Cid>,
1879    ) -> Result<usize> {
1880        let object = match self.get_object_content(oid, objects) {
1881            Some(object) => Some(object),
1882            None => {
1883                if let (Some(base_tree), Some(base_objects_cid)) = (base_tree, base_objects_cid) {
1884                    self.get_object_content_from_base(oid, base_tree, base_objects_cid)
1885                        .await?
1886                } else {
1887                    None
1888                }
1889            }
1890        }
1891        .ok_or_else(|| Error::ObjectNotFound(oid.to_string()))?;
1892
1893        if object.0 != ObjectType::Blob {
1894            return Err(Error::InvalidObjectType(format!(
1895                "expected blob, got {:?}",
1896                object.0
1897            )));
1898        }
1899
1900        Ok(object.1.len())
1901    }
1902
1903    /// Get the underlying store
1904    pub fn store(&self) -> &Arc<LocalStore> {
1905        &self.store
1906    }
1907
1908    /// Get the HashTree for direct access
1909    #[allow(dead_code)]
1910    pub fn hashtree(&self) -> &HashTree<LocalStore> {
1911        &self.tree
1912    }
1913
1914    /// Push all blobs to file servers
1915    #[allow(dead_code)]
1916    pub fn push_to_file_servers(
1917        &self,
1918        blossom: &hashtree_blossom::BlossomClient,
1919    ) -> Result<(usize, usize)> {
1920        let hashes = self
1921            .store
1922            .list()
1923            .map_err(|e| Error::StorageError(format!("list hashes: {}", e)))?;
1924
1925        info!("Pushing {} blobs to file servers", hashes.len());
1926
1927        let mut uploaded = 0;
1928        let mut existed = 0;
1929
1930        self.runtime.block_on(async {
1931            for hash in &hashes {
1932                let hex_hash = hex::encode(hash);
1933                let data = match self.store.get_sync(hash) {
1934                    Ok(Some(d)) => d,
1935                    _ => continue,
1936                };
1937
1938                match blossom.upload_if_missing(&data).await {
1939                    Ok((_, true)) => {
1940                        debug!("Uploaded {}", &hex_hash[..12]);
1941                        uploaded += 1;
1942                    }
1943                    Ok((_, false)) => {
1944                        existed += 1;
1945                    }
1946                    Err(e) => {
1947                        debug!("Failed to upload {}: {}", &hex_hash[..12], e);
1948                    }
1949                }
1950            }
1951        });
1952
1953        info!(
1954            "Upload complete: {} new, {} already existed",
1955            uploaded, existed
1956        );
1957        Ok((uploaded, existed))
1958    }
1959
1960    /// Clear all state (for testing or re-initialization)
1961    #[allow(dead_code)]
1962    pub fn clear(&self) -> Result<()> {
1963        let mut objects = self
1964            .objects
1965            .write()
1966            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
1967        let mut refs = self
1968            .refs
1969            .write()
1970            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
1971        let mut root = self
1972            .root_cid
1973            .write()
1974            .map_err(|e| Error::StorageError(format!("lock: {}", e)))?;
1975
1976        objects.clear();
1977        refs.clear();
1978        *root = None;
1979        Ok(())
1980    }
1981}
1982
1983#[cfg(test)]
1984mod tests {
1985    use super::*;
1986    use hashtree_core::store::Store;
1987    use hashtree_core::{sha256, LinkType};
1988    use std::io::{Read, Write};
1989    use std::net::{TcpListener, TcpStream};
1990    use std::path::Path;
1991    use std::process::{Child, Command, Stdio};
1992    #[cfg(feature = "lmdb")]
1993    use std::sync::atomic::{AtomicUsize, Ordering};
1994    use std::time::{Duration, Instant};
1995    use tempfile::TempDir;
1996
1997    fn create_test_storage() -> (GitStorage, TempDir) {
1998        let temp_dir = TempDir::new().unwrap();
1999        let storage = GitStorage::open(temp_dir.path()).unwrap();
2000        (storage, temp_dir)
2001    }
2002
2003    fn create_test_storage_with_limit(max_size_bytes: u64) -> (GitStorage, TempDir) {
2004        let temp_dir = TempDir::new().unwrap();
2005        let storage = GitStorage::open_with_backend_and_max_bytes(
2006            temp_dir.path(),
2007            StorageBackend::Fs,
2008            max_size_bytes,
2009        )
2010        .unwrap();
2011        (storage, temp_dir)
2012    }
2013
2014    fn local_total_bytes(storage: &GitStorage) -> u64 {
2015        match storage.store().as_ref() {
2016            LocalStore::Fs(store) => store.stats().unwrap().total_bytes,
2017            #[cfg(feature = "lmdb")]
2018            LocalStore::Lmdb(store) => store.stats().unwrap().total_bytes,
2019        }
2020    }
2021
2022    fn write_test_commit(storage: &GitStorage) -> ObjectId {
2023        let blob_oid = storage
2024            .write_raw_object(ObjectType::Blob, b"hello from hashtree\n")
2025            .unwrap();
2026
2027        let mut tree_content = Vec::new();
2028        tree_content.extend_from_slice(b"100644 README.md\0");
2029        tree_content.extend_from_slice(&hex::decode(blob_oid.to_hex()).unwrap());
2030        let tree_oid = storage
2031            .write_raw_object(ObjectType::Tree, &tree_content)
2032            .unwrap();
2033
2034        let commit_content = format!(
2035            "tree {}\nauthor Test User <test@example.com> 0 +0000\ncommitter Test User <test@example.com> 0 +0000\n\nInitial commit\n",
2036            tree_oid.to_hex()
2037        );
2038        storage
2039            .write_raw_object(ObjectType::Commit, commit_content.as_bytes())
2040            .unwrap()
2041    }
2042
2043    fn export_tree_to_fs<S: Store>(
2044        runtime: &RuntimeExecutor,
2045        tree: &HashTree<S>,
2046        cid: &Cid,
2047        dst: &Path,
2048    ) {
2049        std::fs::create_dir_all(dst).unwrap();
2050        let entries = runtime.block_on(tree.list_directory(cid)).unwrap();
2051        for entry in entries {
2052            let entry_cid = Cid {
2053                hash: entry.hash,
2054                key: entry.key,
2055            };
2056            let path = dst.join(&entry.name);
2057            match entry.link_type {
2058                LinkType::Dir => export_tree_to_fs(runtime, tree, &entry_cid, &path),
2059                LinkType::Blob | LinkType::File => {
2060                    let data = runtime
2061                        .block_on(tree.get(&entry_cid, None))
2062                        .unwrap()
2063                        .unwrap();
2064                    if let Some(parent) = path.parent() {
2065                        std::fs::create_dir_all(parent).unwrap();
2066                    }
2067                    std::fs::write(path, data).unwrap();
2068                }
2069            }
2070        }
2071    }
2072
2073    fn spawn_http_server(root: &Path, port: u16) -> Child {
2074        Command::new("python3")
2075            .args([
2076                "-m",
2077                "http.server",
2078                &port.to_string(),
2079                "--bind",
2080                "127.0.0.1",
2081            ])
2082            .current_dir(root)
2083            .stdout(Stdio::null())
2084            .stderr(Stdio::null())
2085            .spawn()
2086            .expect("spawn python http server")
2087    }
2088
2089    fn wait_for_http_server(server: &mut Child, port: u16, path: &str) {
2090        let deadline = Instant::now() + Duration::from_secs(5);
2091
2092        loop {
2093            if let Some(status) = server.try_wait().expect("check http server status") {
2094                panic!("python http server exited before becoming ready: {status}");
2095            }
2096
2097            if let Ok(mut stream) = TcpStream::connect(("127.0.0.1", port)) {
2098                stream
2099                    .set_read_timeout(Some(Duration::from_millis(200)))
2100                    .expect("set read timeout");
2101                stream
2102                    .set_write_timeout(Some(Duration::from_millis(200)))
2103                    .expect("set write timeout");
2104                let request =
2105                    format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n");
2106                if stream.write_all(request.as_bytes()).is_ok() {
2107                    let mut response = String::new();
2108                    if stream.read_to_string(&mut response).is_ok()
2109                        && response.starts_with("HTTP/1.0 200")
2110                    {
2111                        return;
2112                    }
2113                }
2114            }
2115
2116            if Instant::now() >= deadline {
2117                panic!("python http server did not become ready on port {port}");
2118            }
2119            std::thread::sleep(Duration::from_millis(50));
2120        }
2121    }
2122
2123    #[test]
2124    fn test_import_ref() {
2125        let (storage, _temp) = create_test_storage();
2126
2127        // Import a ref
2128        storage
2129            .import_ref("refs/heads/main", "abc123def456")
2130            .unwrap();
2131
2132        // Check it exists
2133        assert!(storage.has_ref("refs/heads/main").unwrap());
2134
2135        // Check value via list_refs
2136        let refs = storage.list_refs().unwrap();
2137        assert_eq!(
2138            refs.get("refs/heads/main"),
2139            Some(&"abc123def456".to_string())
2140        );
2141    }
2142
2143    #[cfg(feature = "lmdb")]
2144    #[test]
2145    fn test_local_store_falls_back_to_fs_when_lmdb_open_returns_enosys() {
2146        let temp_dir = TempDir::new().unwrap();
2147        let fs_calls = AtomicUsize::new(0);
2148        let lmdb_calls = AtomicUsize::new(0);
2149
2150        let store = LocalStore::new_for_backend_with_openers(
2151            temp_dir.path(),
2152            StorageBackend::Lmdb,
2153            0,
2154            |path, max_bytes| {
2155                fs_calls.fetch_add(1, Ordering::SeqCst);
2156                LocalStore::open_fs_store(path, max_bytes)
2157            },
2158            |_path, _max_bytes| {
2159                lmdb_calls.fetch_add(1, Ordering::SeqCst);
2160                Err(StoreError::Io(std::io::Error::from_raw_os_error(
2161                    libc::ENOSYS,
2162                )))
2163            },
2164        )
2165        .unwrap();
2166
2167        assert!(matches!(store, LocalStore::Fs(_)));
2168        assert_eq!(lmdb_calls.load(Ordering::SeqCst), 1);
2169        assert_eq!(fs_calls.load(Ordering::SeqCst), 1);
2170    }
2171
2172    #[cfg(feature = "lmdb")]
2173    #[test]
2174    fn test_local_store_does_not_fallback_on_unrelated_lmdb_errors() {
2175        let temp_dir = TempDir::new().unwrap();
2176        let fs_calls = AtomicUsize::new(0);
2177
2178        let result = LocalStore::new_for_backend_with_openers(
2179            temp_dir.path(),
2180            StorageBackend::Lmdb,
2181            0,
2182            |path, max_bytes| {
2183                fs_calls.fetch_add(1, Ordering::SeqCst);
2184                LocalStore::open_fs_store(path, max_bytes)
2185            },
2186            |_path, _max_bytes| {
2187                Err(StoreError::Io(std::io::Error::from_raw_os_error(
2188                    libc::EACCES,
2189                )))
2190            },
2191        );
2192
2193        assert!(
2194            matches!(result, Err(StoreError::Io(io_error)) if io_error.raw_os_error() == Some(libc::EACCES))
2195        );
2196        assert_eq!(fs_calls.load(Ordering::SeqCst), 0);
2197    }
2198
2199    #[test]
2200    fn test_import_multiple_refs_preserves_all() {
2201        let (storage, _temp) = create_test_storage();
2202
2203        // Import multiple refs (simulating loading from remote)
2204        storage.import_ref("refs/heads/main", "sha_main").unwrap();
2205        storage.import_ref("refs/heads/dev", "sha_dev").unwrap();
2206        storage
2207            .import_ref("refs/heads/feature", "sha_feature")
2208            .unwrap();
2209
2210        // All should exist
2211        assert!(storage.has_ref("refs/heads/main").unwrap());
2212        assert!(storage.has_ref("refs/heads/dev").unwrap());
2213        assert!(storage.has_ref("refs/heads/feature").unwrap());
2214
2215        // Now write a new ref (simulating push)
2216        storage
2217            .write_ref(
2218                "refs/heads/new-branch",
2219                &Ref::Direct(
2220                    ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2221                ),
2222            )
2223            .unwrap();
2224
2225        // Original refs should still exist
2226        let refs = storage.list_refs().unwrap();
2227        assert_eq!(refs.len(), 4);
2228        assert!(refs.contains_key("refs/heads/main"));
2229        assert!(refs.contains_key("refs/heads/dev"));
2230        assert!(refs.contains_key("refs/heads/feature"));
2231        assert!(refs.contains_key("refs/heads/new-branch"));
2232    }
2233
2234    #[test]
2235    fn test_import_compressed_object() {
2236        let (storage, _temp) = create_test_storage();
2237
2238        // Create a fake compressed object
2239        let fake_compressed = vec![0x78, 0x9c, 0x01, 0x02, 0x03]; // fake zlib data
2240
2241        storage
2242            .import_compressed_object("abc123def456", fake_compressed.clone())
2243            .unwrap();
2244
2245        // Check object count
2246        assert_eq!(storage.object_count().unwrap(), 1);
2247    }
2248
2249    #[test]
2250    fn test_write_ref_overwrites_imported() {
2251        let (storage, _temp) = create_test_storage();
2252
2253        // Import a ref
2254        storage.import_ref("refs/heads/main", "old_sha").unwrap();
2255
2256        // Write same ref with new value
2257        storage
2258            .write_ref(
2259                "refs/heads/main",
2260                &Ref::Direct(
2261                    ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2262                ),
2263            )
2264            .unwrap();
2265
2266        // Should have new value
2267        let refs = storage.list_refs().unwrap();
2268        assert_eq!(
2269            refs.get("refs/heads/main"),
2270            Some(&"0123456789abcdef0123456789abcdef01234567".to_string())
2271        );
2272    }
2273
2274    #[test]
2275    fn test_delete_ref_preserves_others() {
2276        let (storage, _temp) = create_test_storage();
2277
2278        // Import multiple refs
2279        storage.import_ref("refs/heads/main", "sha_main").unwrap();
2280        storage.import_ref("refs/heads/dev", "sha_dev").unwrap();
2281
2282        // Delete one
2283        storage.delete_ref("refs/heads/dev").unwrap();
2284
2285        // Other should still exist
2286        assert!(storage.has_ref("refs/heads/main").unwrap());
2287        assert!(!storage.has_ref("refs/heads/dev").unwrap());
2288    }
2289
2290    #[test]
2291    fn test_clear_removes_all() {
2292        let (storage, _temp) = create_test_storage();
2293
2294        // Import refs and objects
2295        storage.import_ref("refs/heads/main", "sha_main").unwrap();
2296        storage
2297            .import_compressed_object("obj1", vec![1, 2, 3])
2298            .unwrap();
2299
2300        // Clear
2301        storage.clear().unwrap();
2302
2303        // All gone
2304        assert!(!storage.has_ref("refs/heads/main").unwrap());
2305        assert_eq!(storage.object_count().unwrap(), 0);
2306    }
2307
2308    #[test]
2309    fn test_evict_if_needed_respects_configured_limit() {
2310        let (storage, _temp) = create_test_storage_with_limit(1_024);
2311
2312        storage
2313            .write_raw_object(ObjectType::Blob, &vec![b'a'; 900])
2314            .unwrap();
2315        storage
2316            .write_raw_object(ObjectType::Blob, &vec![b'b'; 900])
2317            .unwrap();
2318        storage
2319            .write_ref(
2320                "refs/heads/main",
2321                &Ref::Direct(
2322                    ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
2323                ),
2324            )
2325            .unwrap();
2326
2327        storage.build_tree().unwrap();
2328
2329        let before = local_total_bytes(&storage);
2330        assert!(before > 1_024);
2331
2332        let freed = storage.evict_if_needed().unwrap();
2333        assert!(freed > 0);
2334
2335        let after = local_total_bytes(&storage);
2336        assert!(after <= 1_024);
2337    }
2338
2339    #[test]
2340    fn test_build_tree_evicts_stale_blobs_before_writing_new_tree() {
2341        let max_size_bytes = 16 * 1024;
2342        let (storage, _temp) = create_test_storage_with_limit(max_size_bytes);
2343
2344        let stale_blobs = vec![
2345            vec![b'x'; 7 * 1024],
2346            vec![b'y'; 7 * 1024],
2347            vec![b'z'; 7 * 1024],
2348        ];
2349        let stale_hashes: Vec<Hash> = stale_blobs.iter().map(|blob| sha256(blob)).collect();
2350
2351        for (hash, blob) in stale_hashes.iter().zip(stale_blobs) {
2352            storage
2353                .runtime
2354                .block_on(storage.store().put(*hash, blob))
2355                .unwrap();
2356        }
2357
2358        let before = local_total_bytes(&storage);
2359        assert!(before > max_size_bytes);
2360
2361        let commit_oid = write_test_commit(&storage);
2362        storage
2363            .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2364            .unwrap();
2365        storage
2366            .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2367            .unwrap();
2368
2369        storage.build_tree().unwrap();
2370
2371        let evicted_stale = stale_hashes
2372            .iter()
2373            .filter(|hash| !storage.runtime.block_on(storage.store().has(hash)).unwrap())
2374            .count();
2375
2376        assert!(
2377            evicted_stale > 0,
2378            "expected build_tree preflight eviction to remove stale blobs before writing"
2379        );
2380    }
2381
2382    #[test]
2383    fn test_build_tree_adds_dumb_http_metadata() {
2384        let (storage, _temp) = create_test_storage();
2385        let commit_oid = write_test_commit(&storage);
2386        let tag_content = format!(
2387            "object {}\ntype commit\ntag v1.0.0\ntagger Test User <test@example.com> 0 +0000\n\nrelease\n",
2388            commit_oid.to_hex()
2389        );
2390        let tag_oid = storage
2391            .write_raw_object(ObjectType::Tag, tag_content.as_bytes())
2392            .unwrap();
2393
2394        storage
2395            .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2396            .unwrap();
2397        storage
2398            .write_ref("refs/tags/v1.0.0", &Ref::Direct(tag_oid))
2399            .unwrap();
2400        storage
2401            .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2402            .unwrap();
2403
2404        let root_cid = storage.build_tree().unwrap();
2405
2406        let info_refs_cid = storage
2407            .runtime
2408            .block_on(storage.tree.resolve_path(&root_cid, ".git/info/refs"))
2409            .unwrap()
2410            .expect("info/refs exists");
2411        let info_refs = storage
2412            .runtime
2413            .block_on(storage.tree.get(&info_refs_cid, None))
2414            .unwrap()
2415            .unwrap();
2416        let info_refs = String::from_utf8(info_refs).unwrap();
2417
2418        assert_eq!(
2419            info_refs,
2420            format!(
2421                "{commit}\trefs/heads/main\n{tag}\trefs/tags/v1.0.0\n{commit}\trefs/tags/v1.0.0^{{}}\n",
2422                commit = commit_oid.to_hex(),
2423                tag = tag_oid.to_hex()
2424            )
2425        );
2426
2427        let packs_cid = storage
2428            .runtime
2429            .block_on(
2430                storage
2431                    .tree
2432                    .resolve_path(&root_cid, ".git/objects/info/packs"),
2433            )
2434            .unwrap()
2435            .expect("objects/info/packs exists");
2436        let packs = storage
2437            .runtime
2438            .block_on(storage.tree.get(&packs_cid, None))
2439            .unwrap()
2440            .unwrap();
2441        assert!(packs.is_empty(), "objects/info/packs should be empty");
2442    }
2443
2444    #[test]
2445    fn test_build_tree_materializes_loose_refs_at_git_paths() {
2446        let (storage, _temp) = create_test_storage();
2447        let commit_oid = write_test_commit(&storage);
2448
2449        storage
2450            .write_ref("refs/heads/master", &Ref::Direct(commit_oid))
2451            .unwrap();
2452        storage
2453            .write_ref("refs/heads/codex/meshrouter-prod", &Ref::Direct(commit_oid))
2454            .unwrap();
2455        storage
2456            .write_ref("refs/tags/v1.0.0", &Ref::Direct(commit_oid))
2457            .unwrap();
2458        storage
2459            .write_ref("HEAD", &Ref::Symbolic("refs/heads/master".to_string()))
2460            .unwrap();
2461
2462        let root_cid = storage.build_tree().unwrap();
2463
2464        for path in [
2465            ".git/refs/heads/master",
2466            ".git/refs/heads/codex/meshrouter-prod",
2467            ".git/refs/tags/v1.0.0",
2468        ] {
2469            let ref_cid = storage
2470                .runtime
2471                .block_on(storage.tree.resolve_path(&root_cid, path))
2472                .unwrap()
2473                .unwrap_or_else(|| panic!("{path} should exist"));
2474            let ref_value = storage
2475                .runtime
2476                .block_on(storage.tree.get(&ref_cid, None))
2477                .unwrap()
2478                .unwrap();
2479            assert_eq!(
2480                String::from_utf8(ref_value).unwrap(),
2481                commit_oid.to_hex(),
2482                "{path} should contain the ref target",
2483            );
2484        }
2485    }
2486
2487    #[test]
2488    fn test_materialized_tree_supports_static_http_clone_from_git_dir() {
2489        let (storage, _temp) = create_test_storage();
2490        let commit_oid = write_test_commit(&storage);
2491        storage
2492            .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2493            .unwrap();
2494        storage
2495            .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2496            .unwrap();
2497
2498        let root_cid = storage.build_tree().unwrap();
2499        let export_dir = TempDir::new().unwrap();
2500        let repo_dir = export_dir.path().join("repo");
2501        export_tree_to_fs(&storage.runtime, &storage.tree, &root_cid, &repo_dir);
2502
2503        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2504        let port = listener.local_addr().unwrap().port();
2505        drop(listener);
2506
2507        let mut server = spawn_http_server(export_dir.path(), port);
2508        wait_for_http_server(&mut server, port, "/repo/.git/HEAD");
2509
2510        let clone_dir = TempDir::new().unwrap();
2511        let clone_path = clone_dir.path().join("clone");
2512        let output = Command::new("git")
2513            .args([
2514                "clone",
2515                &format!("http://127.0.0.1:{port}/repo/.git", port = port),
2516                clone_path.to_str().unwrap(),
2517            ])
2518            .output()
2519            .unwrap();
2520
2521        let _ = server.kill();
2522        let _ = server.wait();
2523
2524        assert!(
2525            output.status.success(),
2526            "git clone failed: {}",
2527            String::from_utf8_lossy(&output.stderr)
2528        );
2529        assert_eq!(
2530            std::fs::read_to_string(clone_path.join("README.md")).unwrap(),
2531            "hello from hashtree\n"
2532        );
2533    }
2534
2535    #[test]
2536    fn test_validate_root_contains_direct_refs_rejects_missing_tip_object() {
2537        let (storage, _temp) = create_test_storage();
2538        let commit_oid = write_test_commit(&storage);
2539        storage
2540            .write_ref("refs/heads/main", &Ref::Direct(commit_oid))
2541            .unwrap();
2542        storage
2543            .write_ref("HEAD", &Ref::Symbolic("refs/heads/main".to_string()))
2544            .unwrap();
2545
2546        let empty_objects_dir = storage
2547            .runtime
2548            .block_on(storage.tree.put_directory(vec![]))
2549            .unwrap();
2550        let refs_dir = storage
2551            .runtime
2552            .block_on(storage.tree.put_directory(vec![]))
2553            .unwrap();
2554        let info_dir = storage
2555            .runtime
2556            .block_on(storage.tree.put_directory(vec![]))
2557            .unwrap();
2558        let git_dir = storage
2559            .runtime
2560            .block_on(storage.tree.put_directory(vec![
2561                DirEntry::from_cid("HEAD", &info_dir).with_size(0),
2562                DirEntry::from_cid("info", &info_dir).with_link_type(LinkType::Dir),
2563                DirEntry::from_cid("objects", &empty_objects_dir).with_link_type(LinkType::Dir),
2564                DirEntry::from_cid("refs", &refs_dir).with_link_type(LinkType::Dir),
2565            ]))
2566            .unwrap();
2567        let root_cid = storage
2568            .runtime
2569            .block_on(storage.tree.put_directory(vec![
2570                DirEntry::from_cid(".git", &git_dir).with_link_type(LinkType::Dir),
2571            ]))
2572            .unwrap();
2573
2574        let err = storage
2575            .validate_root_contains_direct_refs(&root_cid)
2576            .expect_err("missing ref-tip object should fail validation");
2577        assert!(
2578            err.to_string().contains(&commit_oid.to_hex()),
2579            "validation error should mention missing commit oid: {}",
2580            err
2581        );
2582    }
2583}