weavatrix_git/repository/
mod.rs1mod 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}