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