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}