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}