1use crate::{
8 error::{Error, GResult, InternalObjectError, UnexpectedObjectType, annotate_with_object_id},
9 file_system::FileSystem,
10 object_store::{
11 RawObject,
12 lookup::{lookup, lookup_size_type},
13 },
14 parsing::ParseResult,
15 repo::Repo,
16};
17use accessory::Accessors;
18use alloc::format;
19use chrono::{DateTime, FixedOffset};
20use nom::{
21 Parser,
22 branch::alt,
23 bytes::complete::{tag, take, take_until},
24 character::complete::{char, hex_digit0, i32, i64},
25 combinator::all_consuming,
26 sequence::terminated,
27};
28
29mod blob;
30mod commit;
31mod header;
32mod tag;
33mod tree;
34
35pub use crate::object::blob::Blob;
36pub use crate::object::commit::Commit;
37pub use crate::object::header::{ObjectHeader, ObjectHeaderIter};
38pub use crate::object::tag::Tag;
39pub use crate::object::tree::{Tree, TreeEntry, TreeEntryIter, TreeEntryType};
40pub use crate::object_store::{ObjectSize, ObjectType};
41
42#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Accessors)]
47pub struct ObjectId {
48 #[access(get)]
50 pub(crate) bytes: [u8; 20],
51}
52
53impl alloc::fmt::Display for ObjectId {
54 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55 let mut chars = [0u8; 40];
56 hex::encode_to_slice(self.bytes, &mut chars).unwrap();
57 write!(f, "{}", str::from_utf8(&chars).unwrap())
58 }
59}
60
61impl alloc::fmt::Debug for ObjectId {
62 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63 f.debug_tuple("ObjectId").field(&format!("{self}")).finish()
64 }
65}
66
67impl ObjectId {
68 pub const fn from_bytes(id: [u8; 20]) -> Self {
70 Self { bytes: id }
71 }
72
73 pub fn from_hex(s: &[u8]) -> Option<Self> {
77 let (_, oid) = all_consuming(Self::parse).parse(s).ok()?;
78 Some(oid)
79 }
80
81 pub(crate) const fn zero() -> Self {
82 Self { bytes: [0u8; 20] }
83 }
84
85 pub(crate) fn parse(input: &[u8]) -> ParseResult<&[u8], Self> {
86 take(40usize)
87 .and_then(all_consuming(hex_digit0))
88 .map_res(|hex_str| {
89 let mut buf = [0u8; 20];
90 hex::decode_to_slice(hex_str, &mut buf)?;
91 Ok::<ObjectId, hex::FromHexError>(ObjectId::from_bytes(buf))
92 })
93 .parse(input)
94 }
95}
96
97#[derive(Clone)]
101pub enum Object {
102 #[expect(missing_docs)]
103 Commit(Commit),
104 #[expect(missing_docs)]
105 Tree(Tree),
106 #[expect(missing_docs)]
107 Tag(Tag),
108 #[expect(missing_docs)]
109 Blob(Blob),
110}
111
112impl Object {
113 pub fn id(&self) -> ObjectId {
115 use Object::*;
116 match self {
117 Commit(c) => c.id(),
118 Tree(t) => t.id(),
119 Tag(t) => t.id(),
120 Blob(b) => b.id(),
121 }
122 }
123
124 pub fn object_type(&self) -> ObjectType {
126 use Object::*;
127 match self {
128 Commit(_) => ObjectType::Commit,
129 Tree(_) => ObjectType::Tree,
130 Tag(_) => ObjectType::Tag,
131 Blob(_) => ObjectType::Blob,
132 }
133 }
134
135 pub fn commit(self) -> Result<Commit, UnexpectedObjectType> {
139 use Object::*;
140 match self {
141 Commit(c) => Ok(c),
142 _ => Err(UnexpectedObjectType {
143 id: self.id(),
144 expected: ObjectType::Commit,
145 received: self.object_type(),
146 }),
147 }
148 }
149
150 pub fn tag(self) -> Result<Tag, UnexpectedObjectType> {
154 use Object::*;
155 match self {
156 Tag(t) => Ok(t),
157 _ => Err(UnexpectedObjectType {
158 id: self.id(),
159 expected: ObjectType::Tag,
160 received: self.object_type(),
161 }),
162 }
163 }
164
165 pub fn tree(self) -> Result<Tree, UnexpectedObjectType> {
169 use Object::*;
170 match self {
171 Tree(t) => Ok(t),
172 _ => Err(UnexpectedObjectType {
173 id: self.id(),
174 expected: ObjectType::Tree,
175 received: self.object_type(),
176 }),
177 }
178 }
179
180 pub fn blob(self) -> Result<Blob, UnexpectedObjectType> {
184 use Object::*;
185 match self {
186 Blob(b) => Ok(b),
187 _ => Err(UnexpectedObjectType {
188 id: self.id(),
189 expected: ObjectType::Blob,
190 received: self.object_type(),
191 }),
192 }
193 }
194
195 pub async fn peel_to_commit<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<Option<Commit>> {
197 use Object::*;
198 let mut obj: Object = self.clone();
199 loop {
200 match obj {
201 Commit(c) => return Ok(Some(c)),
202 Tag(t) => {
203 let target = repo.lookup_object(t.target()).await?;
204 obj = target;
205 }
206 _ => return Ok(None),
207 }
208 }
209 }
210
211 pub async fn peel_to_tree<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<Option<Tree>> {
213 use Object::*;
214 let mut obj: Object = self.clone();
215 loop {
216 match obj {
217 Tree(t) => return Ok(Some(t)),
218 Commit(c) => {
219 let tree = repo.lookup_object(c.tree()).await?;
220 obj = tree;
221 }
222 Tag(t) => {
223 let target = repo.lookup_object(t.target()).await?;
224 obj = target;
225 }
226 Blob(_) => return Ok(None),
227 }
228 }
229 }
230
231 pub(crate) async fn lookup<F: FileSystem>(repo: &Repo<F>, id: ObjectId) -> GResult<Self> {
232 let RawObject { object_type, body } = lookup(repo, id)
233 .await?
234 .ok_or_else(|| Error::MissingObject(id))?;
235
236 let object = match object_type {
237 ObjectType::Commit => Object::Commit(
238 Commit::parse(id, body)
239 .map_err(InternalObjectError::from)
240 .map_err(annotate_with_object_id(id))?,
241 ),
242 ObjectType::Tag => Object::Tag(
243 Tag::parse(id, body)
244 .map_err(InternalObjectError::from)
245 .map_err(annotate_with_object_id(id))?,
246 ),
247 ObjectType::Blob => Object::Blob(Blob::new(id, body)),
248 ObjectType::Tree => Object::Tree(
249 Tree::parse(id, body)
250 .map_err(InternalObjectError::from)
251 .map_err(annotate_with_object_id(id))?,
252 ),
253 };
254
255 Ok(object)
256 }
257
258 pub(crate) async fn lookup_size_type<F: FileSystem>(
259 repo: &Repo<F>,
260 id: ObjectId,
261 ) -> GResult<(ObjectSize, ObjectType)> {
262 lookup_size_type(repo, id)
263 .await?
264 .ok_or_else(|| Error::MissingObject(id))
265 }
266}
267
268#[allow(clippy::type_complexity)]
269fn parse_author_committer_tagger(
270 input: &[u8],
271) -> ParseResult<&[u8], (&[u8], &[u8], DateTime<FixedOffset>)> {
272 (
273 terminated(take_until(" <"), tag(" <")),
274 terminated(take_until("> "), tag("> ")),
275 (
276 terminated(i64, char(' ')),
277 alt((char('+').map(|_| 1), char('-').map(|_| -1))),
278 take(2usize).and_then(all_consuming(i32)),
279 take(2usize).and_then(all_consuming(i32)),
280 )
281 .map_opt(|(timestamp, tz_sign, tz_hour, tz_minute)| {
282 let date = DateTime::from_timestamp(timestamp, 0)?;
283 let offset = FixedOffset::east_opt(tz_sign * (3600 * tz_hour + 60 * tz_minute))?;
284 let author_date = date.with_timezone(&offset);
285 Some(author_date)
286 }),
287 )
288 .parse(input)
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::test::helpers::{make_basic_repo, make_similar_commits};
295 use futures::executor::block_on;
296
297 #[test]
298 fn lookup_commit() {
299 let test_repo = make_basic_repo().unwrap();
300 let commit_id = test_repo.run_git(["rev-parse", "HEAD"]).unwrap();
301 let commit_id = ObjectId::from_hex(commit_id.trim_ascii()).unwrap();
302
303 let repo = test_repo.repo();
304 let object = block_on(Object::lookup(&repo, commit_id)).unwrap();
305 assert_eq!(object.id(), commit_id);
306 assert!(matches!(object, Object::Commit(_)));
307 }
308
309 #[test]
310 fn lookup_packfile_object() {
311 let test_repo = make_basic_repo().unwrap();
312 make_similar_commits(&test_repo).unwrap();
313 test_repo.run_git(["gc"]).unwrap();
314 let repo = test_repo.repo();
315 let head = block_on(repo.head()).unwrap();
316 let oid = block_on(head.resolve_object_id(&repo)).unwrap();
317 let Object::Commit(commit) = block_on(repo.lookup_object(oid)).unwrap() else {
318 panic!()
319 };
320 let tree_id = commit.tree();
321 let Object::Tree(tree) = block_on(repo.lookup_object(tree_id)).unwrap() else {
322 panic!()
323 };
324 assert_eq!(tree.entries().len(), 1 + 26 - 2);
325 }
326
327 #[test]
328 fn parse_author_committer_line() {
329 let example = "an author <an-email-address> 0 +0000";
330 parse_author_committer_tagger(example.as_bytes()).unwrap();
331 }
332}