Skip to main content

grit_lib/objects/
handle.rs

1//! Repository-scoped object database access via [`Objects`].
2use crate::change_id::{set_change_id, ChangeIdPolicy};
3use crate::error::{Error, Result};
4use crate::fsck_standalone::{fsck_object, FsckObjectOptions};
5use crate::objects::write::canonical_tree_entries;
6use crate::objects::{
7    serialize_commit, serialize_tree, CommitData, NewCommit, Object, ObjectId, ObjectInfo,
8    ObjectKind, TreeEntry,
9};
10use crate::porcelain::commit::stamp_change_id_on_new_commit;
11use crate::repo::Repository;
12
13/// Read and write Git objects through a [`Repository`] handle.
14#[derive(Debug, Copy, Clone)]
15pub struct Objects<'repo> {
16    repo: &'repo Repository,
17}
18
19impl<'repo> Objects<'repo> {
20    /// Bind object operations to `repo`.
21    #[inline]
22    #[must_use]
23    pub fn new(repo: &'repo Repository) -> Self {
24        Self { repo }
25    }
26
27    /// Underlying repository.
28    #[inline]
29    #[must_use]
30    pub fn repository(&self) -> &'repo Repository {
31        self.repo
32    }
33
34    /// Returns whether `oid` is present in this repository's object database.
35    ///
36    /// # Errors
37    ///
38    /// Propagates a backend failure when no object source has `oid` (see
39    /// [`crate::odb::Odb::contains`]) instead of reporting it as missing.
40    ///
41    /// # Examples
42    ///
43    /// ```
44    /// # fn main() -> grit_lib::error::Result<()> {
45    /// use grit_lib::objects::ObjectKind;
46    /// use grit_lib::repo::{InitOptions, Repository};
47    /// let dir = std::env::temp_dir().join("grit-objects-handle-doc");
48    /// let repo = Repository::init(&dir, &InitOptions::default())?;
49    /// let oid = repo.objects().write(ObjectKind::Blob, b"hello")?;
50    /// assert!(repo.objects().contains(&oid)?);
51    /// # Ok(())
52    /// # }
53    /// ```
54    #[inline]
55    pub fn contains(&self, oid: &ObjectId) -> Result<bool> {
56        self.repo.odb().contains(oid)
57    }
58
59    /// Read the object named `oid`.
60    ///
61    /// # Errors
62    ///
63    /// Returns [`crate::error::Error::ObjectNotFound`] when the object is missing.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// # fn main() -> grit_lib::error::Result<()> {
69    /// use grit_lib::objects::ObjectKind;
70    /// use grit_lib::repo::{InitOptions, Repository};
71    /// let dir = std::env::temp_dir().join("grit-objects-read-doc");
72    /// let repo = Repository::init(&dir, &InitOptions::default())?;
73    /// let oid = repo.objects().write(ObjectKind::Blob, b"payload")?;
74    /// let obj = repo.objects().read(&oid)?;
75    /// assert_eq!(obj.data, b"payload");
76    /// # Ok(())
77    /// # }
78    /// ```
79    #[inline]
80    pub fn read(&self, oid: &ObjectId) -> Result<Object> {
81        self.repo.odb().read(oid)
82    }
83
84    /// Read object type and size without inflating full blob bodies when possible.
85    ///
86    /// # Errors
87    ///
88    /// Propagates object lookup failures from the ODB.
89    #[inline]
90    pub fn read_info(&self, oid: &ObjectId) -> Result<ObjectInfo> {
91        self.repo.odb().read_info(oid)
92    }
93
94    /// Read `oid` and parse it as a commit (annotated tags are not followed).
95    ///
96    /// # Errors
97    ///
98    /// See [`crate::objects::odb::Odb::read_commit`]: a missing object, an object that is not
99    /// a commit ([`crate::error::Error::UnexpectedObjectKind`]), or a malformed commit.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// # fn main() -> grit_lib::error::Result<()> {
105    /// use grit_lib::objects::ObjectKind;
106    /// use grit_lib::repo::{InitOptions, Repository};
107    /// let dir = std::env::temp_dir().join("grit-objects-read-commit-doc");
108    /// let repo = Repository::init(&dir, &InitOptions::default())?;
109    /// let tree = repo.objects().write(ObjectKind::Tree, b"")?;
110    /// let body = format!(
111    ///     "tree {tree}\nauthor A <a@example.com> 0 +0000\ncommitter A <a@example.com> 0 +0000\n\nfirst\n"
112    /// );
113    /// let commit = repo.objects().write(ObjectKind::Commit, body.as_bytes())?;
114    /// assert_eq!(repo.objects().read_commit(&commit)?.message, "first\n");
115    /// assert_eq!(repo.objects().commit_tree(&commit)?, tree);
116    /// assert!(repo.objects().read_commit(&tree).is_err());
117    /// # Ok(())
118    /// # }
119    /// ```
120    #[inline]
121    pub fn read_commit(&self, oid: &ObjectId) -> Result<CommitData> {
122        self.repo.odb().read_commit(oid)
123    }
124
125    /// The root tree of the commit `oid`, decoding only the commit headers.
126    ///
127    /// # Errors
128    ///
129    /// Same as [`Self::read_commit`].
130    #[inline]
131    pub fn commit_tree(&self, oid: &ObjectId) -> Result<ObjectId> {
132        self.repo.odb().commit_tree(oid)
133    }
134
135    /// Hash `data` as a Git object of `kind` without writing to disk.
136    ///
137    /// # Errors
138    ///
139    /// Returns [`crate::error::Error::HashCollision`] when sha1dc detects a chosen-prefix collision.
140    #[inline]
141    pub fn hash(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
142        self.repo.odb().hash(kind, data)
143    }
144
145    /// Validate and store an object, returning its id.
146    ///
147    /// Tree, commit and tag payloads must pass the same checks as `git hash-object -t <kind>`
148    /// (without `--literally`): sorted, duplicate-free tree entries with allowed modes and
149    /// names, and well-formed `tree`/`parent`/`author`/`committer`/`tagger` headers. Blobs
150    /// are stored as given. To build trees and commits from typed values instead of raw
151    /// bytes, use [`Self::write_tree`] and [`Self::write_commit`].
152    ///
153    /// # Errors
154    ///
155    /// - [`crate::error::Error::InvalidObject`] when the payload would fail `git fsck`;
156    ///   nothing is written.
157    /// - [`crate::error::Error::HashCollision`] and zlib or filesystem errors from the ODB.
158    ///
159    /// # Examples
160    ///
161    /// See also the [`guide_objects`](https://grit-scm.com/docs/library/objects/) example program.
162    pub fn write(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
163        fsck_object(
164            kind,
165            data,
166            FsckObjectOptions::new(self.repo.odb().hash_algo()),
167        )
168        .map_err(|check| Error::InvalidObject { kind, check })?;
169        self.repo.odb().write(kind, data)
170    }
171
172    /// Store `data` as an object of `kind` without validating its structure.
173    ///
174    /// The equivalent of `git hash-object --literally`: use it only to reproduce or test
175    /// corrupt objects. Repositories containing such objects fail `git fsck`.
176    ///
177    /// # Errors
178    ///
179    /// [`crate::error::Error::HashCollision`] and zlib or filesystem errors from the ODB.
180    #[inline]
181    pub fn write_literally(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
182        self.repo.odb().write(kind, data)
183    }
184
185    /// Write a tree object from `entries`, given in any order.
186    ///
187    /// Entries are sorted into Git's tree order. Entry objects are not required to exist
188    /// (as with `git mktree --missing`), so trees can be built bottom-up or point at
189    /// objects a partial clone has not fetched.
190    ///
191    /// # Errors
192    ///
193    /// - [`crate::error::Error::InvalidTreeEntry`] when an entry has an empty, `.`, `..`,
194    ///   `.git`-alias or slash/NUL-containing name, a mode other than `100644`, `100755`,
195    ///   `120000`, `40000` or `160000`, the null object id, an object id of the wrong hash
196    ///   algorithm, a name over 4096 bytes, or a name used by another entry.
197    /// - [`crate::error::Error::HashCollision`] and zlib or filesystem errors from the ODB.
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// # fn main() -> grit_lib::error::Result<()> {
203    /// use grit_lib::objects::{ObjectKind, TreeEntry};
204    /// use grit_lib::repo::{InitOptions, Repository};
205    /// let dir = tempfile::tempdir()?;
206    /// let repo = Repository::init(dir.path(), &InitOptions::new().with_bare(true))?;
207    /// let blob = repo.objects().write(ObjectKind::Blob, b"hello\n")?;
208    /// let entry = |name: &str| TreeEntry { mode: 0o100644, name: name.into(), oid: blob };
209    /// let tree = repo.objects().write_tree([entry("b"), entry("a")])?;
210    /// let names: Vec<_> = grit_lib::objects::parse_tree(&repo.objects().read(&tree)?.data)?
211    ///     .into_iter()
212    ///     .map(|e| e.name)
213    ///     .collect();
214    /// assert_eq!(names, [b"a".to_vec(), b"b".to_vec()]);
215    /// # Ok(())
216    /// # }
217    /// ```
218    pub fn write_tree(&self, entries: impl IntoIterator<Item = TreeEntry>) -> Result<ObjectId> {
219        let algo = self.repo.odb().hash_algo();
220        let data = serialize_tree(&canonical_tree_entries(entries, algo)?);
221        debug_assert!(
222            fsck_object(ObjectKind::Tree, &data, FsckObjectOptions::new(algo)).is_ok(),
223            "canonical_tree_entries let through a tree fsck rejects"
224        );
225        self.repo.odb().write(ObjectKind::Tree, &data)
226    }
227
228    /// Write the commit described by `commit` and return its id.
229    ///
230    /// The tree must exist and be a tree, and every parent must exist and be a commit
231    /// (the checks `git commit-tree` makes). When `grit.changeIds` is enabled (the
232    /// default) and no id was set with [`NewCommit::with_change_id`], the commit gets a
233    /// fresh `change-id` header. The commit is not signed and no ref is updated; use
234    /// [`crate::porcelain::commit::create_commit`] to commit the index onto a branch.
235    ///
236    /// # Errors
237    ///
238    /// - [`crate::error::Error::InvalidSignature`] when the author or committer has a
239    ///   `<`, `>` or control character in its name or email, a time before the Unix
240    ///   epoch, or a UTC offset with a seconds component.
241    /// - [`crate::error::Error::ObjectNotFound`] when the tree or a parent is missing, and
242    ///   [`crate::error::Error::UnexpectedObjectKind`] when one has the wrong kind.
243    /// - Config loading errors from [`Repository::config`] (read for `grit.changeIds`).
244    /// - The errors of [`Self::write`].
245    ///
246    /// # Examples
247    ///
248    /// See [`NewCommit`].
249    pub fn write_commit(&self, commit: &NewCommit) -> Result<ObjectId> {
250        for signature in commit.signatures() {
251            signature.validate()?;
252        }
253        let (tree, parents) = commit.tree_and_parents();
254        self.expect_kind(tree, ObjectKind::Tree)?;
255        for parent in parents {
256            self.expect_kind(parent, ObjectKind::Commit)?;
257        }
258        let mut data = commit.to_commit_data();
259        match commit.change_id() {
260            Some(id) => set_change_id(&mut data, id),
261            None => {
262                let policy = ChangeIdPolicy::from_config(self.repo.config()?.as_ref());
263                stamp_change_id_on_new_commit(self.repo, &policy, &mut data)?;
264            }
265        }
266        self.write(ObjectKind::Commit, &serialize_commit(&data))
267    }
268
269    fn expect_kind(&self, oid: &ObjectId, expected: ObjectKind) -> Result<()> {
270        let actual = self.read_info(oid)?.kind;
271        if actual != expected {
272            return Err(Error::UnexpectedObjectKind {
273                oid: *oid,
274                expected,
275                actual,
276            });
277        }
278        Ok(())
279    }
280}