1use thiserror::Error;
22
23use crate::diagnostics::Warning;
24use crate::error::{Error, Result};
25use crate::objects::odb::store::ObjectStream;
26use crate::objects::{
27 parse_commit, parse_tag, parse_tree, CommitData, Object, ObjectId, ObjectInfo, ObjectKind,
28 TagData, TreeEntry,
29};
30use crate::refs::store::RawRef;
31use crate::refs::{
32 expand_ref_dwim_full_name, shorten_unambiguous, warn_ambiguous_refs_from_config,
33};
34use crate::repo::Repository;
35use crate::rev_parse::{
36 abbreviate_object_id, ambiguous_object_hint_lines, expand_at_minus_to_branch_name,
37 parse_peel_suffix, resolve_revision, resolve_revision_for_verify,
38 resolve_upstream_symbolic_name, split_double_dot_range, split_triple_dot_range,
39 upstream_suffix_info,
40};
41use crate::rev_parse_error::{AmbiguousChangeIdHint, AmbiguousObjectHint, RevParseError};
42
43#[derive(Debug, Error)]
45#[non_exhaustive]
46pub enum ResolveError {
47 #[error("unknown revision '{spec}'")]
49 UnknownRevision {
50 spec: String,
52 },
53
54 #[error("ambiguous short object id '{spec}'")]
56 AmbiguousShortObject {
57 spec: String,
59 candidates: Vec<AmbiguousObjectHint>,
61 },
62
63 #[error("ambiguous change id '{spec}'")]
65 AmbiguousChangeId {
66 spec: String,
68 candidates: Vec<AmbiguousChangeIdHint>,
70 },
71
72 #[error("path '{path}' does not exist in '{revision}'")]
74 PathNotInTree {
75 path: String,
77 revision: String,
79 },
80
81 #[error("revision range or set '{spec}' is not a single object")]
83 RevisionRange {
84 spec: String,
86 },
87
88 #[error("object '{spec}' is a {found}, not a {expected}")]
90 WrongObjectKind {
91 spec: String,
93 found: ObjectKind,
95 expected: ObjectKind,
97 },
98
99 #[error("invalid reflog date '{date}'")]
101 ReflogDateUnparseable {
102 date: String,
104 },
105
106 #[error("{0}")]
108 DamagedObject(#[source] Error),
109}
110
111impl From<RevParseError> for ResolveError {
112 fn from(value: RevParseError) -> Self {
113 match value {
114 RevParseError::PathNotInRevision { path, revision }
115 | RevParseError::PathOnDiskNotInRevision { path, revision } => {
116 ResolveError::PathNotInTree { path, revision }
117 }
118 RevParseError::InvalidObjectName { name } => {
119 ResolveError::UnknownRevision { spec: name }
120 }
121 RevParseError::AmbiguousChangeId { prefix, candidates } => {
122 ResolveError::AmbiguousChangeId {
123 spec: prefix,
124 candidates,
125 }
126 }
127 RevParseError::ReflogDateUnparseable { date } => {
128 ResolveError::ReflogDateUnparseable { date }
129 }
130 other => ResolveError::UnknownRevision {
131 spec: other.to_string(),
132 },
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ResolvedRefName {
140 pub full: String,
142 pub short: String,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct ResolvedObject {
149 pub oid: ObjectId,
151 pub kind: ObjectKind,
153 pub size: u64,
155 pub ref_name: Option<String>,
157 pub full_ref: Option<String>,
159}
160
161#[derive(Debug, Clone)]
163pub enum ObjectContents {
164 Blob(Vec<u8>),
166 Tree(Vec<TreeEntry>),
168 Commit(CommitData),
170 Tag(TagData),
172}
173
174#[derive(Debug, Clone)]
179pub enum RevisionLabel {
180 Branch {
182 name: String,
184 },
185 Tag {
187 name: String,
189 annotation: Option<TagData>,
191 },
192}
193
194impl Repository {
195 #[inline]
213 pub fn revision_label(&self, spec: &str) -> Result<Option<RevisionLabel>> {
214 if spec == "HEAD" {
215 return Ok(crate::refs::read_symbolic_ref_for_repository(self, "HEAD")
216 .ok()
217 .flatten()
218 .and_then(|target| {
219 target
220 .strip_prefix("refs/heads/")
221 .map(|name| RevisionLabel::Branch {
222 name: name.to_owned(),
223 })
224 }));
225 }
226
227 if self.refs().resolve(&format!("refs/heads/{spec}")).is_ok() {
228 return Ok(Some(RevisionLabel::Branch {
229 name: spec.to_owned(),
230 }));
231 }
232
233 let Ok(oid) = self.refs().resolve(&format!("refs/tags/{spec}")) else {
234 return Ok(None);
235 };
236 let object = self.odb().read(&oid)?;
237 let annotation = if object.kind == ObjectKind::Tag {
238 Some(parse_tag(&object.data)?)
239 } else {
240 None
241 };
242 Ok(Some(RevisionLabel::Tag {
243 name: spec.to_owned(),
244 annotation,
245 }))
246 }
247
248 pub fn resolve(&self, spec: &str) -> std::result::Result<ResolvedObject, ResolveError> {
259 let oid = match resolve_revision(self, spec) {
260 Ok(o) => o,
261 Err(Error::ObjectNotFound(_)) => {
262 return Err(ResolveError::UnknownRevision {
263 spec: spec.to_owned(),
264 });
265 }
266 Err(Error::RevParse(RevParseError::AmbiguousShortObject { spec: prefix })) => {
267 let candidates =
268 ambiguous_object_hint_lines(self, &prefix, None).unwrap_or_default();
269 return Err(ResolveError::AmbiguousShortObject {
270 spec: spec.to_owned(),
271 candidates,
272 });
273 }
274 Err(Error::RevParse(RevParseError::AmbiguousChangeId { prefix, candidates })) => {
275 return Err(ResolveError::AmbiguousChangeId {
276 spec: prefix,
277 candidates,
278 });
279 }
280 Err(Error::RevParse(e)) => return Err(e.into()),
281 Err(_) => {
282 return Err(ResolveError::UnknownRevision {
283 spec: spec.to_owned(),
284 });
285 }
286 };
287
288 let info = self.read_info_replaced(&oid).map_err(|e| {
289 if e.is_damaged_object() {
290 ResolveError::DamagedObject(e)
291 } else {
292 ResolveError::UnknownRevision {
293 spec: spec.to_owned(),
294 }
295 }
296 })?;
297 let ref_names = infer_ref_names(self, spec, oid);
298 Ok(ResolvedObject {
299 oid,
300 kind: info.kind,
301 size: info.size,
302 ref_name: ref_names.as_ref().map(|n| n.short.clone()),
303 full_ref: ref_names.map(|n| n.full),
304 })
305 }
306
307 pub fn verify_object(
320 &self,
321 spec: &str,
322 expected: Option<ObjectKind>,
323 ) -> std::result::Result<ObjectId, ResolveError> {
324 if split_double_dot_range(spec).is_some()
325 || split_triple_dot_range(spec).is_some()
326 || is_revision_set_exclude_spec(spec)
327 {
328 return Err(ResolveError::RevisionRange {
329 spec: spec.to_owned(),
330 });
331 }
332
333 let oid = match resolve_revision_for_verify(self, spec) {
334 Ok(o) => o,
335 Err(Error::ObjectNotFound(_)) => {
336 return Err(ResolveError::UnknownRevision {
337 spec: spec.to_owned(),
338 });
339 }
340 Err(Error::RevParse(RevParseError::AmbiguousShortObject { spec: prefix })) => {
341 let candidates =
342 ambiguous_object_hint_lines(self, &prefix, None).unwrap_or_default();
343 return Err(ResolveError::AmbiguousShortObject {
344 spec: spec.to_owned(),
345 candidates,
346 });
347 }
348 Err(Error::RevParse(RevParseError::AmbiguousChangeId { prefix, candidates })) => {
349 return Err(ResolveError::AmbiguousChangeId {
350 spec: prefix,
351 candidates,
352 });
353 }
354 Err(Error::RevParse(e)) => return Err(e.into()),
355 Err(_) => {
356 return Err(ResolveError::UnknownRevision {
357 spec: spec.to_owned(),
358 });
359 }
360 };
361
362 let info = self.read_info_replaced(&oid).map_err(|e| {
363 if e.is_damaged_object() {
364 ResolveError::DamagedObject(e)
365 } else {
366 ResolveError::UnknownRevision {
367 spec: spec.to_owned(),
368 }
369 }
370 })?;
371
372 if let Some(want) = expected {
373 if info.kind != want {
374 return Err(ResolveError::WrongObjectKind {
375 spec: spec.to_owned(),
376 found: info.kind,
377 expected: want,
378 });
379 }
380 }
381
382 Ok(oid)
383 }
384
385 pub fn resolve_ref_name(
395 &self,
396 spec: &str,
397 ) -> std::result::Result<Option<ResolvedRefName>, RevParseError> {
398 let strict = warn_ambiguous_refs_from_config(self);
399 if upstream_suffix_info(spec).is_some() {
400 let full = resolve_upstream_symbolic_name(self, spec).map_err(map_resolve_ref_error)?;
401 let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
402 return Ok(Some(ResolvedRefName { full, short }));
403 }
404 if let Some(branch) =
405 expand_at_minus_to_branch_name(self, spec).map_err(map_resolve_ref_error)?
406 {
407 let full = format!("refs/heads/{branch}");
408 if self.resolve_ref_oid(&full).is_err() {
409 return Ok(None);
410 }
411 let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
412 return Ok(Some(ResolvedRefName { full, short }));
413 }
414 let (count, full) = expand_ref_dwim_full_name(self, spec).map_err(map_resolve_ref_error)?;
415 if count == 0 {
416 if spec == "HEAD" {
417 match self
418 .refs()
419 .read_raw("HEAD")
420 .map_err(map_resolve_ref_error)?
421 {
422 Some(RawRef::Direct(_)) => {
423 return Ok(Some(ResolvedRefName {
424 full: "HEAD".to_owned(),
425 short: "HEAD".to_owned(),
426 }));
427 }
428 Some(RawRef::Symbolic(target)) if self.resolve_ref_oid(&target).is_err() => {
429 return Ok(None);
430 }
431 Some(RawRef::Symbolic(_)) | None => {}
432 }
433 }
434 return Ok(None);
435 }
436 if count > 1 && strict {
437 self.warn(Warning::AmbiguousRefname {
438 spec: spec.to_owned(),
439 });
440 return Ok(None);
441 }
442 let full = full.ok_or_else(|| RevParseError::AmbiguousRefname {
443 spec: spec.to_owned(),
444 })?;
445 let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
446 Ok(Some(ResolvedRefName { full, short }))
447 }
448
449 pub fn read_object_raw_for_display(&self, oid: &ObjectId) -> Result<Object> {
455 self.read_replaced(oid)
456 }
457
458 pub fn read_object_info_for_display(&self, oid: &ObjectId) -> Result<ObjectInfo> {
467 self.read_info_replaced(oid)
468 }
469
470 pub fn with_object_stream_for_display<R>(
482 &self,
483 oid: &ObjectId,
484 f: impl FnOnce(ObjectStream<'_>) -> Result<R>,
485 ) -> Result<Option<R>> {
486 let effective = self.object_id_after_replace(oid);
487 self.odb().with_stream(&effective, f)
488 }
489
490 pub fn read_object_for_display(&self, oid: &ObjectId) -> Result<ObjectContents> {
497 let obj = self.read_object_raw_for_display(oid)?;
498 match obj.kind {
499 ObjectKind::Blob => Ok(ObjectContents::Blob(obj.data)),
500 ObjectKind::Tree => Ok(ObjectContents::Tree(parse_tree(&obj.data)?)),
501 ObjectKind::Commit => Ok(ObjectContents::Commit(parse_commit(&obj.data)?)),
502 ObjectKind::Tag => Ok(ObjectContents::Tag(parse_tag(&obj.data)?)),
503 }
504 }
505}
506
507#[must_use]
509pub fn tree_entry_mode_cat_file_p(mode: u32) -> String {
510 if mode == 0o040000 {
511 "040000".to_owned()
512 } else {
513 format!("{:o}", mode)
514 }
515}
516
517#[must_use]
522pub fn tree_entry_cat_file_type(mode: u32) -> &'static str {
523 match mode & 0o170_000 {
524 0o040_000 => "tree",
525 0o160_000 => "commit",
526 _ => "blob",
527 }
528}
529
530#[must_use]
532pub fn format_tree_entries_cat_file_p(entries: &[TreeEntry]) -> Vec<u8> {
533 let mut out = Vec::new();
534 for e in entries {
535 out.extend_from_slice(tree_entry_mode_cat_file_p(e.mode).as_bytes());
536 out.push(b' ');
537 out.extend_from_slice(tree_entry_cat_file_type(e.mode).as_bytes());
538 out.push(b' ');
539 out.extend_from_slice(e.oid.to_hex().as_bytes());
540 out.push(b'\t');
541 out.extend_from_slice(&e.name);
542 out.push(b'\n');
543 }
544 out
545}
546
547pub fn format_tree_cat_file_p(data: &[u8]) -> Result<Vec<u8>> {
553 let entries = parse_tree(data)?;
554 Ok(format_tree_entries_cat_file_p(&entries))
555}
556
557#[cfg(test)]
558mod tree_entry_cat_file_type_tests {
559 use super::tree_entry_cat_file_type;
560
561 #[test]
562 fn mode_mask_labels_match_git() {
563 assert_eq!(tree_entry_cat_file_type(0o040_000), "tree");
564 assert_eq!(tree_entry_cat_file_type(0o040_755), "tree");
565 assert_eq!(tree_entry_cat_file_type(0o160_000), "commit");
566 assert_eq!(tree_entry_cat_file_type(0o100_644), "blob");
567 assert_eq!(tree_entry_cat_file_type(0o100_755), "blob");
568 assert_eq!(tree_entry_cat_file_type(0o120_000), "blob");
569 assert_eq!(tree_entry_cat_file_type(0o100_664), "blob");
570 }
571}
572
573pub fn abbreviate_for_display(repo: &Repository, oid: ObjectId, short: bool) -> Result<String> {
575 if short {
576 abbreviate_object_id(repo, oid, 7)
577 } else {
578 Ok(oid.to_hex())
579 }
580}
581
582fn map_resolve_ref_error(err: Error) -> RevParseError {
583 match err {
584 Error::RevParse(e) => e,
585 other => RevParseError::InvalidObjectName {
586 name: other.to_string(),
587 },
588 }
589}
590
591fn is_revision_set_exclude_spec(spec: &str) -> bool {
593 spec.starts_with('^')
594}
595
596fn infer_ref_names(repo: &Repository, spec: &str, oid: ObjectId) -> Option<ResolvedRefName> {
597 let rev_part = spec.split(':').next().unwrap_or(spec);
598 let (base, _) = parse_peel_suffix(rev_part);
599 if base.is_empty() || looks_like_abbrev_hex(base, repo) {
600 return None;
601 }
602 let resolved = match repo.resolve_ref_name(base) {
603 Ok(v) => v?,
604 Err(RevParseError::InvalidObjectName { .. }) => return None,
605 Err(_) => return None,
606 };
607 if repo.resolve_ref_oid(&resolved.full).ok() == Some(oid) {
608 Some(resolved)
609 } else {
610 None
611 }
612}
613
614fn looks_like_abbrev_hex(base: &str, repo: &Repository) -> bool {
615 if base.len() >= repo.odb().hash_algo().hex_len() {
616 return base.chars().all(|c| c.is_ascii_hexdigit());
617 }
618 base.len() >= 4 && base.chars().all(|c| c.is_ascii_hexdigit())
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn resolve_error_from_path_not_in_revision() {
627 let err = ResolveError::from(RevParseError::PathNotInRevision {
628 path: "missing.txt".into(),
629 revision: "HEAD".into(),
630 });
631 assert!(matches!(err, ResolveError::PathNotInTree { .. }));
632 }
633
634 #[test]
635 fn tree_entry_mode_cat_file_p_uses_six_digit_tree_mode() {
636 assert_eq!(tree_entry_mode_cat_file_p(0o040000), "040000");
637 assert_eq!(tree_entry_mode_cat_file_p(0o100644), "100644");
638 }
639 fn label_repo() -> (tempfile::TempDir, Repository, ObjectId) {
640 let dir = tempfile::tempdir().expect("tempdir");
641 let repo = Repository::init(
642 dir.path(),
643 &crate::repo::InitOptions::new().with_initial_branch("main"),
644 )
645 .expect("init");
646 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
647 let commit = repo.odb().write(ObjectKind::Commit, format!(
648 "tree {tree}\nauthor A <a@example.com> 1 +0000\ncommitter A <a@example.com> 1 +0000\n\ninitial\n"
649 ).as_bytes()).expect("commit");
650 crate::refs::write_ref(repo.git_dir(), "refs/heads/main", &commit).expect("branch");
651 (dir, repo, commit)
652 }
653
654 #[test]
655 fn revision_labels_distinguish_names_from_expressions() {
656 let (_dir, repo, oid) = label_repo();
657 for spec in ["HEAD", "main"] {
658 assert!(matches!(repo.revision_label(spec).expect("label"),
659 Some(RevisionLabel::Branch { name }) if name == "main"));
660 }
661 for spec in [
662 oid.to_hex().as_str(),
663 "HEAD~1",
664 "refs/heads/main",
665 "missing",
666 ] {
667 assert!(
668 repo.revision_label(spec).expect("label").is_none(),
669 "{spec}"
670 );
671 }
672 crate::refs::write_ref(repo.git_dir(), "HEAD", &oid).expect("detach");
673 assert!(repo
674 .revision_label("HEAD")
675 .expect("detached label")
676 .is_none());
677 crate::refs::write_symbolic_ref(repo.git_dir(), "HEAD", "refs/tags/main")
678 .expect("non-branch HEAD");
679 assert!(repo
680 .revision_label("HEAD")
681 .expect("non-branch label")
682 .is_none());
683 }
684
685 #[test]
686 fn revision_label_keeps_unborn_branch() {
687 let dir = tempfile::tempdir().expect("tempdir");
688 let repo = Repository::init(
689 dir.path(),
690 &crate::repo::InitOptions::new().with_initial_branch("new"),
691 )
692 .expect("init");
693 assert!(matches!(repo.revision_label("HEAD").expect("label"),
694 Some(RevisionLabel::Branch { name }) if name == "new"));
695 assert!(repo
696 .revision_label("new")
697 .expect("missing branch")
698 .is_none());
699 }
700
701 #[test]
702 fn revision_label_preserves_tag_metadata_and_branch_precedence() {
703 let (_dir, repo, oid) = label_repo();
704 crate::refs::write_ref(repo.git_dir(), "refs/tags/main", &oid).expect("tag");
705 assert!(matches!(
706 repo.revision_label("main").expect("branch wins"),
707 Some(RevisionLabel::Branch { .. })
708 ));
709 crate::refs::write_ref(repo.git_dir(), "refs/tags/light", &oid).expect("tag");
710 assert!(
711 matches!(repo.revision_label("light").expect("lightweight tag"),
712 Some(RevisionLabel::Tag { name, annotation: None }) if name == "light")
713 );
714
715 let blob = repo
716 .odb()
717 .write(ObjectKind::Blob, b"content")
718 .expect("blob");
719 let tag = repo
720 .odb()
721 .write(
722 ObjectKind::Tag,
723 format!("object {blob}\ntype blob\ntag original\n\nmessage\n\n").as_bytes(),
724 )
725 .expect("tag object");
726 crate::refs::write_ref(repo.git_dir(), "refs/tags/alias", &tag).expect("tag ref");
727 let Some(RevisionLabel::Tag {
728 name,
729 annotation: Some(tag),
730 }) = repo.revision_label("alias").expect("annotated tag")
731 else {
732 panic!("expected annotation");
733 };
734 assert_eq!(name, "alias");
735 assert_eq!(tag.tag, "original");
736 assert_eq!(tag.object, blob);
737 assert_eq!(tag.object_type, "blob");
738 assert_eq!(tag.tagger, None);
739 assert_eq!(tag.message, "message\n\n");
740 }
741
742 #[test]
743 fn revision_label_propagates_tag_object_errors() {
744 let (_dir, repo, _oid) = label_repo();
745 let broken = repo
746 .odb()
747 .write(ObjectKind::Tag, b"invalid")
748 .expect("broken object");
749 crate::refs::write_ref(repo.git_dir(), "refs/tags/broken", &broken).expect("tag ref");
750 assert!(matches!(
751 repo.revision_label("broken"),
752 Err(Error::CorruptObject(_))
753 ));
754 let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").expect("oid");
755 let dangling = crate::refs::store::RefTransaction::new()
756 .update(crate::refs::store::RefUpdate {
757 name: "refs/tags/missing".to_owned(),
758 new_value: Some(RawRef::Direct(missing)),
759 expected: crate::refs::store::Expected::Any,
760 reflog: None,
761 flags: crate::refs::store::RefUpdateFlags {
762 skip_oid_verification: true,
763 ..Default::default()
764 },
765 })
766 .expect("txn");
767 repo.refs()
768 .prepare(dangling)
769 .and_then(|prepared| prepared.commit())
770 .expect("dangling tag ref");
771 assert!(matches!(
772 repo.revision_label("missing"),
773 Err(Error::ObjectNotFound(_))
774 ));
775 }
776 #[test]
777 fn revision_labels_use_injected_ref_store() {
778 use crate::refs::store::MemoryRefStore;
779 use std::sync::Arc;
780
781 let (_dir, repo, oid) = label_repo();
782 let refs = Arc::new(MemoryRefStore::new());
783 refs.set_ref("HEAD", RawRef::Symbolic("refs/heads/virtual".into()))
784 .expect("HEAD");
785 refs.set_ref("refs/heads/virtual", RawRef::Direct(oid))
786 .expect("branch");
787 refs.set_ref("refs/tags/virtual-tag", RawRef::Direct(oid))
788 .expect("tag");
789 let repo = repo.with_ref_store(refs);
790 for spec in ["HEAD", "virtual"] {
791 assert!(matches!(repo.revision_label(spec).expect("branch label"),
792 Some(RevisionLabel::Branch { name }) if name == "virtual"));
793 }
794 assert!(
795 matches!(repo.revision_label("virtual-tag").expect("tag label"),
796 Some(RevisionLabel::Tag { name, annotation: None }) if name == "virtual-tag")
797 );
798 assert!(repo
799 .revision_label("main")
800 .expect("disk branch is excluded")
801 .is_none());
802 }
803}