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