Skip to main content

grit_lib/odb/
commit.rs

1//! Reading commit objects by id.
2//!
3//! Callers that need "the commit at this id" or "the root tree of this commit" share these
4//! [`Odb`] methods instead of reading the object, checking its kind and parsing it by hand.
5
6use crate::error::{Error, Result};
7use crate::objects::{parse_commit, CommitData, CommitRef, ObjectId, ObjectKind};
8
9use super::Odb;
10
11impl Odb {
12    /// Read `oid` and parse it as a commit.
13    ///
14    /// Annotated tags are not followed; peel first with [`Self::peel_to_commit`] when the id
15    /// may name a tag.
16    ///
17    /// # Parameters
18    ///
19    /// - `oid`: the commit to read.
20    ///
21    /// # Returns
22    ///
23    /// The owned, parsed commit headers and message.
24    ///
25    /// # Errors
26    ///
27    /// - [`Error::ObjectNotFound`] when `oid` is missing.
28    /// - [`Error::UnexpectedObjectKind`] when `oid` names a tree, blob or tag.
29    /// - [`Error::CorruptObject`] when the commit's required headers are missing or malformed.
30    /// - Any other error [`Self::read`] reports.
31    pub fn read_commit(&self, oid: &ObjectId) -> Result<CommitData> {
32        let object = self.read(oid)?;
33        expect_commit(oid, object.kind)?;
34        parse_commit(&object.data)
35    }
36
37    /// The root tree of the commit `oid`.
38    ///
39    /// Only the commit headers are decoded; the author, committer and message are not copied.
40    ///
41    /// # Parameters
42    ///
43    /// - `oid`: the commit whose tree to return.
44    ///
45    /// # Errors
46    ///
47    /// Same as [`Self::read_commit`].
48    pub fn commit_tree(&self, oid: &ObjectId) -> Result<ObjectId> {
49        let object = self.read(oid)?;
50        expect_commit(oid, object.kind)?;
51        Ok(CommitRef::parse(&object.data)?.tree())
52    }
53}
54
55fn expect_commit(oid: &ObjectId, actual: ObjectKind) -> Result<()> {
56    if actual == ObjectKind::Commit {
57        return Ok(());
58    }
59    Err(Error::UnexpectedObjectKind {
60        oid: *oid,
61        expected: ObjectKind::Commit,
62        actual,
63    })
64}
65
66#[cfg(test)]
67mod tests {
68    #![allow(clippy::expect_used, clippy::unwrap_used)]
69
70    use super::*;
71    use tempfile::TempDir;
72
73    fn commit_with_parent(odb: &Odb, tree: ObjectId, parent: Option<ObjectId>) -> ObjectId {
74        let parent = parent.map(|p| format!("parent {p}\n")).unwrap_or_default();
75        let body = format!(
76            "tree {tree}\n{parent}author A <a@example.com> 1 +0000\ncommitter C <c@example.com> 2 +0100\n\nsubject\n\nbody\n"
77        );
78        odb.write(ObjectKind::Commit, body.as_bytes()).unwrap()
79    }
80
81    #[test]
82    fn read_commit_matches_parse_commit() {
83        let dir = TempDir::new().unwrap();
84        let odb = Odb::new(dir.path());
85        let tree = odb.write(ObjectKind::Tree, b"").unwrap();
86        let root = commit_with_parent(&odb, tree, None);
87        let child = commit_with_parent(&odb, tree, Some(root));
88
89        let read = odb.read_commit(&child).unwrap();
90        let parsed = parse_commit(&odb.read(&child).unwrap().data).unwrap();
91        assert_eq!(read, parsed);
92        assert_eq!(read.tree, tree);
93        assert_eq!(read.parents, vec![root]);
94        assert_eq!(odb.commit_tree(&child).unwrap(), tree);
95    }
96
97    #[test]
98    fn non_commits_are_unexpected_kinds() {
99        let dir = TempDir::new().unwrap();
100        let odb = Odb::new(dir.path());
101        let tree = odb.write(ObjectKind::Tree, b"").unwrap();
102        let blob = odb.write(ObjectKind::Blob, b"data").unwrap();
103        let commit = commit_with_parent(&odb, tree, None);
104        let tag = odb
105            .write(
106                ObjectKind::Tag,
107                format!("object {commit}\ntype commit\ntag t\n\nmsg\n").as_bytes(),
108            )
109            .unwrap();
110
111        for (oid, kind) in [
112            (tree, ObjectKind::Tree),
113            (blob, ObjectKind::Blob),
114            (tag, ObjectKind::Tag),
115        ] {
116            for err in [
117                odb.read_commit(&oid).unwrap_err(),
118                odb.commit_tree(&oid).unwrap_err(),
119            ] {
120                assert!(
121                    matches!(
122                        err,
123                        Error::UnexpectedObjectKind {
124                            oid: o,
125                            expected: ObjectKind::Commit,
126                            actual,
127                        } if o == oid && actual == kind
128                    ),
129                    "{err:?}"
130                );
131                assert_eq!(
132                    err.to_string(),
133                    format!("object {oid} is a {kind}, not a commit")
134                );
135            }
136        }
137    }
138
139    #[test]
140    fn missing_and_malformed_commits_are_errors() {
141        let dir = TempDir::new().unwrap();
142        let odb = Odb::new(dir.path());
143        let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
144        assert!(matches!(
145            odb.read_commit(&missing),
146            Err(Error::ObjectNotFound(_))
147        ));
148        assert!(matches!(
149            odb.commit_tree(&missing),
150            Err(Error::ObjectNotFound(_))
151        ));
152
153        let no_tree = odb
154            .write(
155                ObjectKind::Commit,
156                b"author A <a@example.com> 0 +0000\n\nno tree\n",
157            )
158            .unwrap();
159        assert!(matches!(
160            odb.read_commit(&no_tree),
161            Err(Error::CorruptObject(_))
162        ));
163        assert!(matches!(
164            odb.commit_tree(&no_tree),
165            Err(Error::CorruptObject(_))
166        ));
167    }
168}