Skip to main content

weavatrix_git/repository/
mod.rs

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