Skip to main content

cli/bridge/
git_import_tree.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Import Git trees as Heddle trees.
3
4use std::collections::HashMap;
5
6use objects::object::{Blob, ContentHash, FileMode, Tree, TreeEntry};
7use repo::Repository as HeddleRepository;
8
9use crate::bridge::git_core::{GitBridgeError, GitResult};
10
11const SUBMODULE_PREFIX: &str = "heddle-submodule:";
12
13pub struct GitTreeImporter<'a> {
14    heddle_repo: &'a HeddleRepository,
15    repo: &'a gix::Repository,
16    tree_cache: HashMap<gix::hash::ObjectId, ContentHash>,
17    blob_cache: HashMap<gix::hash::ObjectId, ContentHash>,
18}
19
20impl<'a> GitTreeImporter<'a> {
21    pub fn new(heddle_repo: &'a HeddleRepository, repo: &'a gix::Repository) -> Self {
22        Self {
23            heddle_repo,
24            repo,
25            tree_cache: HashMap::new(),
26            blob_cache: HashMap::new(),
27        }
28    }
29
30    pub fn import_tree(&mut self, tree_oid: gix::hash::ObjectId) -> GitResult<ContentHash> {
31        if let Some(hash) = self.tree_cache.get(&tree_oid) {
32            return Ok(*hash);
33        }
34
35        let git_tree = self
36            .repo
37            .find_tree(tree_oid)
38            .map_err(|err| GitBridgeError::Git(err.to_string()))?;
39
40        let mut entries = Vec::new();
41
42        for entry in git_tree.iter() {
43            let entry = entry.map_err(|err| GitBridgeError::Git(err.to_string()))?;
44            let name = String::from_utf8_lossy(entry.filename().as_ref()).into_owned();
45
46            match entry.kind() {
47                gix::object::tree::EntryKind::Blob
48                | gix::object::tree::EntryKind::BlobExecutable => {
49                    let hash = self.import_blob(entry.object_id())?;
50
51                    let mode =
52                        if matches!(entry.kind(), gix::object::tree::EntryKind::BlobExecutable) {
53                            FileMode::Executable
54                        } else {
55                            FileMode::Normal
56                        };
57
58                    entries.push(TreeEntry {
59                        name,
60                        mode,
61                        entry_type: objects::object::EntryType::Blob,
62                        hash,
63                    });
64                }
65                gix::object::tree::EntryKind::Link => {
66                    let hash = self.import_blob(entry.object_id())?;
67                    // Phase E: must be `EntryType::Symlink` so the
68                    // materialization planner reaches the symlink-write
69                    // branch. Previously this said `EntryType::Blob`,
70                    // which routed checkout through the regular file
71                    // path and wrote the symlink target as file content
72                    // (so e.g. ripgrep's `HomebrewFormula -> pkg/brew`
73                    // appeared on disk as an 8-byte text file containing
74                    // "pkg/brew" rather than a symlink).
75                    entries.push(TreeEntry {
76                        name,
77                        mode: FileMode::Symlink,
78                        entry_type: objects::object::EntryType::Symlink,
79                        hash,
80                    });
81                }
82                gix::object::tree::EntryKind::Tree => {
83                    let subtree_hash = self.import_tree(entry.object_id())?;
84                    entries.push(TreeEntry {
85                        name,
86                        mode: FileMode::Normal,
87                        entry_type: objects::object::EntryType::Tree,
88                        hash: subtree_hash,
89                    });
90                }
91                gix::object::tree::EntryKind::Commit => {
92                    let hash = self.import_gitlink(entry.object_id())?;
93                    entries.push(TreeEntry {
94                        name,
95                        mode: FileMode::Normal,
96                        entry_type: objects::object::EntryType::Blob,
97                        hash,
98                    });
99                }
100            }
101        }
102
103        let tree = Tree::from_entries(entries);
104        let hash = self.heddle_repo.store().put_tree(&tree)?;
105        self.tree_cache.insert(tree_oid, hash);
106        Ok(hash)
107    }
108
109    fn import_blob(&mut self, blob_oid: gix::hash::ObjectId) -> GitResult<ContentHash> {
110        if let Some(hash) = self.blob_cache.get(&blob_oid) {
111            return Ok(*hash);
112        }
113
114        let mut blob = self
115            .repo
116            .find_blob(blob_oid)
117            .map_err(|err| GitBridgeError::Git(err.to_string()))?;
118
119        let heddle_blob = Blob::new(blob.take_data());
120        let hash = self.heddle_repo.store().put_blob(&heddle_blob)?;
121        self.blob_cache.insert(blob_oid, hash);
122        Ok(hash)
123    }
124
125    fn import_gitlink(&mut self, oid: gix::hash::ObjectId) -> GitResult<ContentHash> {
126        if let Some(hash) = self.blob_cache.get(&oid) {
127            return Ok(*hash);
128        }
129
130        let blob = Blob::new(format!("{} {}", SUBMODULE_PREFIX, oid).into_bytes());
131        let hash = self.heddle_repo.store().put_blob(&blob)?;
132        self.blob_cache.insert(oid, hash);
133        Ok(hash)
134    }
135}
136
137/// Import a Git tree as a Heddle tree.
138pub fn import_git_tree(
139    heddle_repo: &HeddleRepository,
140    repo: &gix::Repository,
141    tree_oid: gix::hash::ObjectId,
142) -> GitResult<ContentHash> {
143    GitTreeImporter::new(heddle_repo, repo).import_tree(tree_oid)
144}