Skip to main content

git_async/object/
mod.rs

1//! A module for working with git objects
2//!
3//! This module contains data types for all git objects. Objects are acquired
4//! from a [`Repo`] by looking them up using their [`ObjectId`], or from one of
5//! the `lookup_*` family of methods on existing objects.
6
7use 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/// The ID of a git object
43///
44/// `git-async` only supports SHA-1 repositories, so this is always 20 bytes or
45/// 40 hex characters
46#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Accessors)]
47pub struct ObjectId {
48    /// The object ID as an array of bytes
49    #[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    /// Construct an [`ObjectId`] from an array of bytes.
69    pub const fn from_bytes(id: [u8; 20]) -> Self {
70        Self { bytes: id }
71    }
72
73    /// Construct an [`ObjectId`] from a hex (byte)string.
74    ///
75    /// Returns `None` if the provided string was not 40 hexadecimal characters.
76    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/// A git object
98///
99/// This type encapsulates the four possible types of git object.
100#[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    /// The ID of the object
114    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    /// Get the object type as a plain (fieldless) enum.
125    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    /// Coerce the object to a [`Commit`].
136    ///
137    /// Returns `Err` if the object was not a commit.
138    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    /// Coerce the object to a [`Tag`].
151    ///
152    /// Returns `Err` if the object was not a tag.
153    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    /// Coerce the object to a [`Tree`]
166    ///
167    /// Returns `Err` if the object was not a tree.
168    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    /// Coerce the object to a [`Blob`]
181    ///
182    /// Returns `Err` if the object was not a blob.
183    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    /// Peel the object to a [`Commit`], if possible.
196    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    /// Peel the object to a [`Tree`], if possible.
212    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}