Skip to main content

weavatrix_git/
repository.rs

1use std::{
2    path::{Path, PathBuf},
3    sync::Arc,
4};
5
6use crate::{
7    Commit, CommitMetadata, GitError, HashKind, Head, HistoryOptions, HistoryRecord, Object,
8    ObjectId, ObjectKind, Reference, Result, Tag, Tree, TreeChange,
9    commit_graph::CommitGraph,
10    diff,
11    error::invalid,
12    history, layout,
13    refs::{self, RefTarget},
14    store::ObjectStore,
15};
16
17#[derive(Clone, Copy, Debug)]
18pub struct Limits {
19    pub max_object_bytes: usize,
20    pub max_delta_depth: usize,
21    pub max_ref_depth: usize,
22    pub max_tree_depth: usize,
23    pub max_tree_entries: usize,
24    pub max_history_commits: usize,
25    pub max_parents: usize,
26    pub object_cache_bytes: usize,
27    pub delta_cache_bytes: usize,
28    pub max_bitmap_objects: usize,
29    pub max_reflog_entries: usize,
30    pub max_index_entries: usize,
31}
32
33impl Default for Limits {
34    fn default() -> Self {
35        Self {
36            max_object_bytes: 512 * 1024 * 1024,
37            max_delta_depth: 64,
38            max_ref_depth: 16,
39            max_tree_depth: 256,
40            max_tree_entries: 5_000_000,
41            max_history_commits: 1_000_000,
42            max_parents: 256,
43            object_cache_bytes: 32 * 1024 * 1024,
44            delta_cache_bytes: 16 * 1024 * 1024,
45            max_bitmap_objects: 10_000_000,
46            max_reflog_entries: 1_000_000,
47            max_index_entries: 10_000_000,
48        }
49    }
50}
51
52pub struct Repository {
53    work_dir: Option<PathBuf>,
54    git_dir: PathBuf,
55    common_dir: PathBuf,
56    hash: HashKind,
57    pub(crate) limits: Limits,
58    pub(crate) graph: Option<CommitGraph>,
59    pub(crate) store: ObjectStore,
60    pub(crate) backends: Vec<Arc<dyn crate::ObjectBackend>>,
61}
62
63impl Repository {
64    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
65        Self::open_with_limits(path, Limits::default())
66    }
67
68    pub fn open_with_limits(path: impl AsRef<Path>, limits: Limits) -> Result<Self> {
69        Self::open_with_backends(path, limits, Vec::new())
70    }
71
72    pub fn open_with_backends(
73        path: impl AsRef<Path>,
74        limits: Limits,
75        backends: Vec<Arc<dyn crate::ObjectBackend>>,
76    ) -> Result<Self> {
77        let (work_dir, git_dir) = layout::discover(path.as_ref())?;
78        let common_dir = layout::common_dir(&git_dir)?;
79        let hash = layout::hash_kind(&common_dir)?;
80        let graph = CommitGraph::open(&common_dir, hash)?;
81        let store = ObjectStore::open(
82            common_dir.join("objects"),
83            hash,
84            limits.object_cache_bytes,
85            limits.delta_cache_bytes,
86        )?;
87        Ok(Self {
88            work_dir,
89            git_dir,
90            common_dir,
91            hash,
92            limits,
93            graph,
94            store,
95            backends,
96        })
97    }
98
99    #[must_use]
100    pub fn work_dir(&self) -> Option<&Path> {
101        self.work_dir.as_deref()
102    }
103
104    #[must_use]
105    pub fn git_dir(&self) -> &Path {
106        &self.git_dir
107    }
108
109    #[must_use]
110    pub fn common_dir(&self) -> &Path {
111        &self.common_dir
112    }
113
114    #[must_use]
115    pub const fn hash_kind(&self) -> HashKind {
116        self.hash
117    }
118
119    #[must_use]
120    pub const fn limits(&self) -> &Limits {
121        &self.limits
122    }
123
124    #[must_use]
125    pub fn pack_count(&self) -> usize {
126        self.store.pack_count()
127    }
128
129    pub fn head(&self) -> Result<Head> {
130        let value = refs::read_text(&self.git_dir.join("HEAD"))?
131            .ok_or_else(|| GitError::NotRepository("HEAD is missing".to_owned()))?;
132        match refs::parse_target(&value, self.hash)? {
133            RefTarget::Direct(id) => Ok(Head {
134                symbolic: None,
135                target: Some(id),
136            }),
137            RefTarget::Symbolic(name) => Ok(Head {
138                target: self.resolve_ref_optional(&name, 0)?,
139                symbolic: Some(name),
140            }),
141        }
142    }
143
144    pub fn reference(&self, name: &str) -> Result<Reference> {
145        refs::validate_name(name)?;
146        Ok(Reference {
147            name: name.to_owned(),
148            target: self
149                .resolve_ref_optional(name, 0)?
150                .ok_or_else(|| refs::missing_ref(name))?,
151        })
152    }
153
154    pub fn resolve(&self, value: &str) -> Result<ObjectId> {
155        if value.len() == self.hash.hex_len() && value.bytes().all(|byte| byte.is_ascii_hexdigit())
156        {
157            return ObjectId::from_hex_for(value, self.hash);
158        }
159        if value == "HEAD" {
160            return self
161                .head()?
162                .target
163                .ok_or_else(|| GitError::NotFound("unborn HEAD".to_owned()));
164        }
165        for candidate in [
166            value.to_owned(),
167            format!("refs/heads/{value}"),
168            format!("refs/tags/{value}"),
169        ] {
170            if let Some(id) = self.resolve_ref_optional(&candidate, 0)? {
171                return Ok(id);
172            }
173        }
174        Err(refs::missing_ref(value))
175    }
176
177    pub fn object(&self, id: ObjectId) -> Result<Object> {
178        if id.kind() != self.hash {
179            return Err(invalid("object id hash kind differs from repository"));
180        }
181        if let Some(object) =
182            crate::backend::read(&self.backends, id, self.limits.max_object_bytes)?
183        {
184            return Ok(object);
185        }
186        self.store.read(
187            id,
188            self.limits.max_object_bytes,
189            self.limits.max_delta_depth,
190        )
191    }
192
193    #[must_use]
194    pub fn contains(&self, id: ObjectId) -> bool {
195        self.contains_checked(id).unwrap_or(false)
196    }
197
198    pub fn contains_checked(&self, id: ObjectId) -> Result<bool> {
199        if id.kind() != self.hash {
200            return Ok(false);
201        }
202        Ok(crate::backend::contains(&self.backends, id)? || self.store.contains(id))
203    }
204
205    pub fn commit(&self, id: ObjectId) -> Result<Commit> {
206        let object = self.object(id)?;
207        expect_kind(object.kind, ObjectKind::Commit)?;
208        Commit::parse(id, &object.data, self.limits.max_parents)
209    }
210
211    pub fn commit_metadata(&self, id: ObjectId) -> Result<CommitMetadata> {
212        if let Some(graph) = self
213            .graph
214            .as_ref()
215            .map_or(Ok(None), |graph| graph.find(id))?
216        {
217            return Ok(CommitMetadata {
218                id: graph.id,
219                tree: graph.tree,
220                parents: graph.parents,
221                committer_time: graph.time,
222            });
223        }
224        let commit = self.commit(id)?;
225        let committer_time = commit
226            .committer
227            .as_ref()
228            .or(commit.author.as_ref())
229            .map_or(0, |signature| signature.timestamp);
230        Ok(CommitMetadata {
231            id,
232            tree: commit.tree,
233            parents: commit.parents,
234            committer_time,
235        })
236    }
237
238    pub fn tree(&self, id: ObjectId) -> Result<Tree> {
239        let object = self.object(id)?;
240        expect_kind(object.kind, ObjectKind::Tree)?;
241        Tree::parse(&object.data, self.hash, self.limits.max_tree_entries)
242    }
243
244    pub fn tag(&self, id: ObjectId) -> Result<Tag> {
245        let object = self.object(id)?;
246        expect_kind(object.kind, ObjectKind::Tag)?;
247        Tag::parse(id, &object.data)
248    }
249
250    pub fn history(&self, start: ObjectId, options: HistoryOptions) -> Result<Vec<HistoryRecord>> {
251        history::walk(self, start, options)
252    }
253
254    pub fn history_ids(&self, start: ObjectId, options: HistoryOptions) -> Result<Vec<ObjectId>> {
255        history::walk_ids(self, start, options)
256    }
257
258    pub fn diff_trees(&self, old: ObjectId, new: ObjectId) -> Result<Vec<TreeChange>> {
259        diff::between(self, old, new)
260    }
261
262    pub fn diff_commits(&self, old: ObjectId, new: ObjectId) -> Result<Vec<TreeChange>> {
263        self.diff_trees(self.commit(old)?.tree, self.commit(new)?.tree)
264    }
265
266    fn resolve_ref_optional(&self, name: &str, depth: usize) -> Result<Option<ObjectId>> {
267        refs::validate_name(name)?;
268        if depth >= self.limits.max_ref_depth {
269            return Err(GitError::LimitExceeded {
270                resource: "symbolic ref depth",
271                limit: self.limits.max_ref_depth,
272            });
273        }
274        for root in [&self.git_dir, &self.common_dir] {
275            if let Some(value) = refs::read_text(&root.join(name))? {
276                return match refs::parse_target(&value, self.hash)? {
277                    RefTarget::Direct(id) => Ok(Some(id)),
278                    RefTarget::Symbolic(next) => self.resolve_ref_optional(&next, depth + 1),
279                };
280            }
281        }
282        for root in [&self.git_dir, &self.common_dir] {
283            if let Some(id) = refs::packed_target(&root.join("packed-refs"), name, self.hash)? {
284                return Ok(Some(id));
285            }
286        }
287        Ok(None)
288    }
289}
290
291fn expect_kind(actual: ObjectKind, expected: ObjectKind) -> Result<()> {
292    if actual != expected {
293        return Err(invalid(format!(
294            "expected {} object, found {}",
295            expected.as_str(),
296            actual.as_str()
297        )));
298    }
299    Ok(())
300}