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}