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