1use crate::error::{Error, Result};
7use crate::objects::{
8 commit_through_subject_len, parse_commit, CommitData, CommitRef, ObjectId, ObjectKind,
9};
10
11use super::Odb;
12
13impl Odb {
14 pub fn read_commit(&self, oid: &ObjectId) -> Result<CommitData> {
34 let object = self.read(oid)?;
35 expect_commit(oid, object.kind)?;
36 parse_commit(&object.data)
37 }
38
39 pub fn commit_tree(&self, oid: &ObjectId) -> Result<ObjectId> {
51 let object = self.read(oid)?;
52 expect_commit(oid, object.kind)?;
53 Ok(CommitRef::parse(&object.data)?.tree())
54 }
55
56 pub fn read_commit_through_subject(&self, oid: &ObjectId) -> Result<Vec<u8>> {
79 if self.custom_primary.is_none() {
80 if let Some(data) =
81 crate::pack::pack_cache::try_read_commit_through_subject_via_single_pack_reader(
82 &self.objects_dir,
83 oid,
84 )?
85 {
86 return Ok(through_subject(data));
87 }
88 }
89 let object = self.read(oid)?;
90 expect_commit(oid, object.kind)?;
91 Ok(through_subject(object.data))
92 }
93}
94
95fn through_subject(mut data: Vec<u8>) -> Vec<u8> {
96 if let Some(len) = commit_through_subject_len(&data) {
97 data.truncate(len);
98 }
99 data
100}
101
102fn expect_commit(oid: &ObjectId, actual: ObjectKind) -> Result<()> {
103 if actual == ObjectKind::Commit {
104 return Ok(());
105 }
106 Err(Error::UnexpectedObjectKind {
107 oid: *oid,
108 expected: ObjectKind::Commit,
109 actual,
110 })
111}
112
113#[cfg(test)]
114mod tests {
115 #![allow(clippy::expect_used, clippy::unwrap_used)]
116
117 use super::*;
118 use tempfile::TempDir;
119
120 fn commit_with_parent(odb: &Odb, tree: ObjectId, parent: Option<ObjectId>) -> ObjectId {
121 let parent = parent.map(|p| format!("parent {p}\n")).unwrap_or_default();
122 let body = format!(
123 "tree {tree}\n{parent}author A <a@example.com> 1 +0000\ncommitter C <c@example.com> 2 +0100\n\nsubject\n\nbody\n"
124 );
125 odb.write(ObjectKind::Commit, body.as_bytes()).unwrap()
126 }
127
128 #[test]
129 fn read_commit_matches_parse_commit() {
130 let dir = TempDir::new().unwrap();
131 let odb = Odb::new(dir.path());
132 let tree = odb.write(ObjectKind::Tree, b"").unwrap();
133 let root = commit_with_parent(&odb, tree, None);
134 let child = commit_with_parent(&odb, tree, Some(root));
135
136 let read = odb.read_commit(&child).unwrap();
137 let parsed = parse_commit(&odb.read(&child).unwrap().data).unwrap();
138 assert_eq!(read, parsed);
139 assert_eq!(read.tree, tree);
140 assert_eq!(read.parents, vec![root]);
141 assert_eq!(odb.commit_tree(&child).unwrap(), tree);
142 }
143
144 fn git(dir: &std::path::Path, args: &[&str], stdin: Option<&[u8]>) -> Vec<u8> {
145 use std::io::Write as _;
146 let mut child = std::process::Command::new("git")
147 .args(args)
148 .current_dir(dir)
149 .stdin(std::process::Stdio::piped())
150 .stdout(std::process::Stdio::piped())
151 .stderr(std::process::Stdio::piped())
152 .spawn()
153 .unwrap();
154 child
155 .stdin
156 .take()
157 .unwrap()
158 .write_all(stdin.unwrap_or_default())
159 .unwrap();
160 let out = child.wait_with_output().unwrap();
161 assert!(
162 out.status.success(),
163 "git {args:?}: {}",
164 String::from_utf8_lossy(&out.stderr)
165 );
166 out.stdout
167 }
168
169 fn git_commits_with_prefixes(dir: &std::path::Path) -> Vec<(ObjectId, Vec<u8>)> {
172 git(dir, &["init", "-q", "--bare", "."], None);
173 let tree = String::from_utf8(git(dir, &["mktree"], Some(b""))).unwrap();
174 let head = format!(
175 "tree {}\nauthor A U Thor <a@example.com> 1700000000 +0100\ncommitter C O Mitter <c@example.com> 1700000001 -0700\n",
176 tree.trim()
177 );
178 let body: String = (0..200)
179 .map(|i| format!("Body line {i} with some words.\n"))
180 .collect();
181 let signed_head = format!(
182 "{head}gpgsig -----BEGIN PGP SIGNATURE-----\n \n {}\n -----END PGP SIGNATURE-----\nchange-id kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk\n",
183 "A".repeat(64)
184 );
185 let cases: Vec<(Vec<u8>, usize)> = vec![
186 {
187 let prefix = format!("{head}\nSubject line\n");
188 (format!("{prefix}\n{body}").into_bytes(), prefix.len())
189 },
190 {
191 let prefix = format!("{head}\n\n \n\t Indented subject\n");
192 (
193 format!("{prefix}second line\n\n{body}").into_bytes(),
194 prefix.len(),
195 )
196 },
197 {
198 let prefix = format!("{signed_head}\nSigned subject\n");
199 (format!("{prefix}\n{body}").into_bytes(), prefix.len())
200 },
201 {
202 let prefix = format!("{head}\n\u{65e5}\u{672c}\n\u{a0}\nascii line\n");
203 (format!("{prefix}{body}").into_bytes(), prefix.len())
204 },
205 {
206 let mut raw = format!("{head}encoding ISO-8859-1\n\n").into_bytes();
207 raw.extend_from_slice(b"S\xe9ance\n\ncaf\xe9\n");
208 raw.extend_from_slice(body.as_bytes());
209 let len = raw.len();
210 (raw, len)
211 },
212 {
213 let raw = format!("{head}\nno trailing newline").into_bytes();
214 let len = raw.len();
215 (raw, len)
216 },
217 {
218 let raw = head.clone().into_bytes();
219 let len = raw.len();
220 (raw, len)
221 },
222 ];
223 cases
224 .into_iter()
225 .map(|(raw, prefix_len)| {
226 let hex = git(
227 dir,
228 &[
229 "hash-object",
230 "-t",
231 "commit",
232 "-w",
233 "--literally",
234 "--stdin",
235 ],
236 Some(&raw),
237 );
238 let oid = ObjectId::from_hex(std::str::from_utf8(&hex).unwrap().trim()).unwrap();
239 (oid, raw[..prefix_len].to_vec())
240 })
241 .collect()
242 }
243
244 fn assert_prefixes(odb: &Odb, dir: &std::path::Path, cases: &[(ObjectId, Vec<u8>)]) {
245 use crate::objects::read_commit_subject_line;
246 for (oid, expected) in cases {
247 let full = git(dir, &["cat-file", "commit", &oid.to_hex()], None);
248 let prefix = odb
249 .with_pack_read_context(|| odb.read_commit_through_subject(oid))
250 .unwrap();
251 assert_eq!(prefix, *expected, "{oid}");
252 assert!(full.starts_with(&prefix), "{oid}");
253 assert_eq!(
254 read_commit_subject_line(&prefix).unwrap(),
255 read_commit_subject_line(&full).unwrap(),
256 "{oid}"
257 );
258 let (short, whole) = (parse_commit(&prefix).unwrap(), parse_commit(&full).unwrap());
259 assert_eq!(
260 (short.tree, &short.parents, &short.author, &short.committer),
261 (whole.tree, &whole.parents, &whole.author, &whole.committer),
262 "{oid}"
263 );
264 assert_eq!(
265 (&short.encoding, &short.extra_headers),
266 (&whole.encoding, &whole.extra_headers),
267 "{oid}"
268 );
269 }
270 }
271
272 #[test]
273 fn read_commit_through_subject_matches_git_loose_and_packed() {
274 let dir = TempDir::new().unwrap();
275 let cases = git_commits_with_prefixes(dir.path());
276 let odb = Odb::new(&dir.path().join("objects"));
277 assert_prefixes(&odb, dir.path(), &cases);
278
279 git(
280 dir.path(),
281 &["update-ref", "refs/heads/a", &cases[0].0.to_hex()],
282 None,
283 );
284 for (i, (oid, _)) in cases.iter().enumerate() {
285 git(
286 dir.path(),
287 &["update-ref", &format!("refs/tags/t{i}"), &oid.to_hex()],
288 None,
289 );
290 }
291 git(dir.path(), &["repack", "-a", "-d", "-q"], None);
292 git(dir.path(), &["prune-packed"], None);
293 let odb = Odb::new(&dir.path().join("objects"));
294 assert_prefixes(&odb, dir.path(), &cases);
295
296 let objects = dir.path().join("objects");
299 let mut cut_short = 0;
300 for (oid, expected) in &cases {
301 let read = odb.with_pack_read_context(|| {
302 crate::pack::pack_cache::try_read_commit_through_subject_via_single_pack_reader(
303 &objects, oid,
304 )
305 });
306 let Some(prefix) = read.unwrap() else {
307 continue;
308 };
309 let full = git(dir.path(), &["cat-file", "commit", &oid.to_hex()], None);
310 if prefix.len() < full.len() {
311 assert_eq!(prefix, *expected, "{oid}");
312 cut_short += 1;
313 } else {
314 assert_eq!(prefix, full, "{oid}");
315 }
316 }
317 assert!(cut_short > 0, "no commit was read only through its subject");
318 }
319
320 #[test]
321 fn read_commit_through_subject_rejects_other_kinds() {
322 let dir = TempDir::new().unwrap();
323 let odb = Odb::new(dir.path());
324 let blob = odb.write(ObjectKind::Blob, b"data").unwrap();
325 assert!(matches!(
326 odb.read_commit_through_subject(&blob),
327 Err(Error::UnexpectedObjectKind {
328 actual: ObjectKind::Blob,
329 ..
330 })
331 ));
332 let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
333 assert!(matches!(
334 odb.read_commit_through_subject(&missing),
335 Err(Error::ObjectNotFound(_))
336 ));
337 }
338
339 #[test]
340 fn non_commits_are_unexpected_kinds() {
341 let dir = TempDir::new().unwrap();
342 let odb = Odb::new(dir.path());
343 let tree = odb.write(ObjectKind::Tree, b"").unwrap();
344 let blob = odb.write(ObjectKind::Blob, b"data").unwrap();
345 let commit = commit_with_parent(&odb, tree, None);
346 let tag = odb
347 .write(
348 ObjectKind::Tag,
349 format!("object {commit}\ntype commit\ntag t\n\nmsg\n").as_bytes(),
350 )
351 .unwrap();
352
353 for (oid, kind) in [
354 (tree, ObjectKind::Tree),
355 (blob, ObjectKind::Blob),
356 (tag, ObjectKind::Tag),
357 ] {
358 for err in [
359 odb.read_commit(&oid).unwrap_err(),
360 odb.commit_tree(&oid).unwrap_err(),
361 ] {
362 assert!(
363 matches!(
364 err,
365 Error::UnexpectedObjectKind {
366 oid: o,
367 expected: ObjectKind::Commit,
368 actual,
369 } if o == oid && actual == kind
370 ),
371 "{err:?}"
372 );
373 assert_eq!(
374 err.to_string(),
375 format!("object {oid} is a {kind}, not a commit")
376 );
377 }
378 }
379 }
380
381 #[test]
382 fn missing_and_malformed_commits_are_errors() {
383 let dir = TempDir::new().unwrap();
384 let odb = Odb::new(dir.path());
385 let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
386 assert!(matches!(
387 odb.read_commit(&missing),
388 Err(Error::ObjectNotFound(_))
389 ));
390 assert!(matches!(
391 odb.commit_tree(&missing),
392 Err(Error::ObjectNotFound(_))
393 ));
394
395 let no_tree = odb
396 .write(
397 ObjectKind::Commit,
398 b"author A <a@example.com> 0 +0000\n\nno tree\n",
399 )
400 .unwrap();
401 assert!(matches!(
402 odb.read_commit(&no_tree),
403 Err(Error::CorruptObject(_))
404 ));
405 assert!(matches!(
406 odb.commit_tree(&no_tree),
407 Err(Error::CorruptObject(_))
408 ));
409 }
410}