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
107impl From<RevParseError> for ResolveError {
108 fn from(value: RevParseError) -> Self {
109 match value {
110 RevParseError::PathNotInRevision { path, revision }
111 | RevParseError::PathOnDiskNotInRevision { path, revision } => {
112 ResolveError::PathNotInTree { path, revision }
113 }
114 RevParseError::InvalidObjectName { name } => {
115 ResolveError::UnknownRevision { spec: name }
116 }
117 RevParseError::AmbiguousChangeId { prefix, candidates } => {
118 ResolveError::AmbiguousChangeId {
119 spec: prefix,
120 candidates,
121 }
122 }
123 RevParseError::ReflogDateUnparseable { date } => {
124 ResolveError::ReflogDateUnparseable { date }
125 }
126 other => ResolveError::UnknownRevision {
127 spec: other.to_string(),
128 },
129 }
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct ResolvedRefName {
136 pub full: String,
138 pub short: String,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct ResolvedObject {
145 pub oid: ObjectId,
147 pub kind: ObjectKind,
149 pub size: u64,
151 pub ref_name: Option<String>,
153 pub full_ref: Option<String>,
155}
156
157#[derive(Debug, Clone)]
159pub enum ObjectContents {
160 Blob(Vec<u8>),
162 Tree(Vec<TreeEntry>),
164 Commit(CommitData),
166 Tag(TagData),
168}
169
170#[derive(Debug, Clone)]
175pub enum RevisionLabel {
176 Branch {
178 name: String,
180 },
181 Tag {
183 name: String,
185 annotation: Option<TagData>,
187 },
188}
189
190impl Repository {
191 #[inline]
209 pub fn revision_label(&self, spec: &str) -> Result<Option<RevisionLabel>> {
210 if spec == "HEAD" {
211 return Ok(crate::refs::read_symbolic_ref_for_repository(self, "HEAD")
212 .ok()
213 .flatten()
214 .and_then(|target| {
215 target
216 .strip_prefix("refs/heads/")
217 .map(|name| RevisionLabel::Branch {
218 name: name.to_owned(),
219 })
220 }));
221 }
222
223 if self.refs().resolve(&format!("refs/heads/{spec}")).is_ok() {
224 return Ok(Some(RevisionLabel::Branch {
225 name: spec.to_owned(),
226 }));
227 }
228
229 let Ok(oid) = self.refs().resolve(&format!("refs/tags/{spec}")) else {
230 return Ok(None);
231 };
232 let object = self.odb().read(&oid)?;
233 let annotation = if object.kind == ObjectKind::Tag {
234 Some(parse_tag(&object.data)?)
235 } else {
236 None
237 };
238 Ok(Some(RevisionLabel::Tag {
239 name: spec.to_owned(),
240 annotation,
241 }))
242 }
243
244 pub fn resolve(&self, spec: &str) -> std::result::Result<ResolvedObject, ResolveError> {
255 let oid = match resolve_revision(self, spec) {
256 Ok(o) => o,
257 Err(Error::ObjectNotFound(_)) => {
258 return Err(ResolveError::UnknownRevision {
259 spec: spec.to_owned(),
260 });
261 }
262 Err(Error::RevParse(RevParseError::AmbiguousShortObject { spec: prefix })) => {
263 let candidates =
264 ambiguous_object_hint_lines(self, &prefix, None).unwrap_or_default();
265 return Err(ResolveError::AmbiguousShortObject {
266 spec: spec.to_owned(),
267 candidates,
268 });
269 }
270 Err(Error::RevParse(RevParseError::AmbiguousChangeId { prefix, candidates })) => {
271 return Err(ResolveError::AmbiguousChangeId {
272 spec: prefix,
273 candidates,
274 });
275 }
276 Err(Error::RevParse(e)) => return Err(e.into()),
277 Err(_) => {
278 return Err(ResolveError::UnknownRevision {
279 spec: spec.to_owned(),
280 });
281 }
282 };
283
284 let info = self
285 .read_info_replaced(&oid)
286 .map_err(|_| ResolveError::UnknownRevision {
287 spec: spec.to_owned(),
288 })?;
289 let ref_names = infer_ref_names(self, spec, oid);
290 Ok(ResolvedObject {
291 oid,
292 kind: info.kind,
293 size: info.size,
294 ref_name: ref_names.as_ref().map(|n| n.short.clone()),
295 full_ref: ref_names.map(|n| n.full),
296 })
297 }
298
299 pub fn verify_object(
312 &self,
313 spec: &str,
314 expected: Option<ObjectKind>,
315 ) -> std::result::Result<ObjectId, ResolveError> {
316 if split_double_dot_range(spec).is_some()
317 || split_triple_dot_range(spec).is_some()
318 || is_revision_set_exclude_spec(spec)
319 {
320 return Err(ResolveError::RevisionRange {
321 spec: spec.to_owned(),
322 });
323 }
324
325 let oid = match resolve_revision_for_verify(self, spec) {
326 Ok(o) => o,
327 Err(Error::ObjectNotFound(_)) => {
328 return Err(ResolveError::UnknownRevision {
329 spec: spec.to_owned(),
330 });
331 }
332 Err(Error::RevParse(RevParseError::AmbiguousShortObject { spec: prefix })) => {
333 let candidates =
334 ambiguous_object_hint_lines(self, &prefix, None).unwrap_or_default();
335 return Err(ResolveError::AmbiguousShortObject {
336 spec: spec.to_owned(),
337 candidates,
338 });
339 }
340 Err(Error::RevParse(RevParseError::AmbiguousChangeId { prefix, candidates })) => {
341 return Err(ResolveError::AmbiguousChangeId {
342 spec: prefix,
343 candidates,
344 });
345 }
346 Err(Error::RevParse(e)) => return Err(e.into()),
347 Err(_) => {
348 return Err(ResolveError::UnknownRevision {
349 spec: spec.to_owned(),
350 });
351 }
352 };
353
354 let info = self
355 .read_info_replaced(&oid)
356 .map_err(|_| ResolveError::UnknownRevision {
357 spec: spec.to_owned(),
358 })?;
359
360 if let Some(want) = expected {
361 if info.kind != want {
362 return Err(ResolveError::WrongObjectKind {
363 spec: spec.to_owned(),
364 found: info.kind,
365 expected: want,
366 });
367 }
368 }
369
370 Ok(oid)
371 }
372
373 pub fn resolve_ref_name(
383 &self,
384 spec: &str,
385 ) -> std::result::Result<Option<ResolvedRefName>, RevParseError> {
386 let strict = warn_ambiguous_refs_from_config(self);
387 if upstream_suffix_info(spec).is_some() {
388 let full = resolve_upstream_symbolic_name(self, spec).map_err(map_resolve_ref_error)?;
389 let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
390 return Ok(Some(ResolvedRefName { full, short }));
391 }
392 if let Some(branch) =
393 expand_at_minus_to_branch_name(self, spec).map_err(map_resolve_ref_error)?
394 {
395 let full = format!("refs/heads/{branch}");
396 if self.resolve_ref_oid(&full).is_err() {
397 return Ok(None);
398 }
399 let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
400 return Ok(Some(ResolvedRefName { full, short }));
401 }
402 let (count, full) = expand_ref_dwim_full_name(self, spec).map_err(map_resolve_ref_error)?;
403 if count == 0 {
404 if spec == "HEAD" {
405 match self
406 .refs()
407 .read_raw("HEAD")
408 .map_err(map_resolve_ref_error)?
409 {
410 Some(RawRef::Direct(_)) => {
411 return Ok(Some(ResolvedRefName {
412 full: "HEAD".to_owned(),
413 short: "HEAD".to_owned(),
414 }));
415 }
416 Some(RawRef::Symbolic(target)) if self.resolve_ref_oid(&target).is_err() => {
417 return Ok(None);
418 }
419 Some(RawRef::Symbolic(_)) | None => {}
420 }
421 }
422 return Ok(None);
423 }
424 if count > 1 && strict {
425 self.warn(Warning::AmbiguousRefname {
426 spec: spec.to_owned(),
427 });
428 return Ok(None);
429 }
430 let full = full.ok_or_else(|| RevParseError::AmbiguousRefname {
431 spec: spec.to_owned(),
432 })?;
433 let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
434 Ok(Some(ResolvedRefName { full, short }))
435 }
436
437 pub fn read_object_raw_for_display(&self, oid: &ObjectId) -> Result<Object> {
443 self.read_replaced(oid)
444 }
445
446 pub fn read_object_info_for_display(&self, oid: &ObjectId) -> Result<ObjectInfo> {
455 self.read_info_replaced(oid)
456 }
457
458 pub fn with_object_stream_for_display<R>(
470 &self,
471 oid: &ObjectId,
472 f: impl FnOnce(ObjectStream<'_>) -> Result<R>,
473 ) -> Result<Option<R>> {
474 let effective = self.object_id_after_replace(oid);
475 self.odb().with_stream(&effective, f)
476 }
477
478 pub fn read_object_for_display(&self, oid: &ObjectId) -> Result<ObjectContents> {
485 let obj = self.read_object_raw_for_display(oid)?;
486 match obj.kind {
487 ObjectKind::Blob => Ok(ObjectContents::Blob(obj.data)),
488 ObjectKind::Tree => Ok(ObjectContents::Tree(parse_tree(&obj.data)?)),
489 ObjectKind::Commit => Ok(ObjectContents::Commit(parse_commit(&obj.data)?)),
490 ObjectKind::Tag => Ok(ObjectContents::Tag(parse_tag(&obj.data)?)),
491 }
492 }
493}
494
495#[must_use]
497pub fn tree_entry_mode_cat_file_p(mode: u32) -> String {
498 if mode == 0o040000 {
499 "040000".to_owned()
500 } else {
501 format!("{:o}", mode)
502 }
503}
504
505#[must_use]
510pub fn tree_entry_cat_file_type(mode: u32) -> &'static str {
511 match mode & 0o170_000 {
512 0o040_000 => "tree",
513 0o160_000 => "commit",
514 _ => "blob",
515 }
516}
517
518#[must_use]
520pub fn format_tree_entries_cat_file_p(entries: &[TreeEntry]) -> Vec<u8> {
521 let mut out = Vec::new();
522 for e in entries {
523 out.extend_from_slice(tree_entry_mode_cat_file_p(e.mode).as_bytes());
524 out.push(b' ');
525 out.extend_from_slice(tree_entry_cat_file_type(e.mode).as_bytes());
526 out.push(b' ');
527 out.extend_from_slice(e.oid.to_hex().as_bytes());
528 out.push(b'\t');
529 out.extend_from_slice(&e.name);
530 out.push(b'\n');
531 }
532 out
533}
534
535pub fn format_tree_cat_file_p(data: &[u8]) -> Result<Vec<u8>> {
541 let entries = parse_tree(data)?;
542 Ok(format_tree_entries_cat_file_p(&entries))
543}
544
545#[cfg(test)]
546mod tree_entry_cat_file_type_tests {
547 use super::tree_entry_cat_file_type;
548
549 #[test]
550 fn mode_mask_labels_match_git() {
551 assert_eq!(tree_entry_cat_file_type(0o040_000), "tree");
552 assert_eq!(tree_entry_cat_file_type(0o040_755), "tree");
553 assert_eq!(tree_entry_cat_file_type(0o160_000), "commit");
554 assert_eq!(tree_entry_cat_file_type(0o100_644), "blob");
555 assert_eq!(tree_entry_cat_file_type(0o100_755), "blob");
556 assert_eq!(tree_entry_cat_file_type(0o120_000), "blob");
557 assert_eq!(tree_entry_cat_file_type(0o100_664), "blob");
558 }
559}
560
561pub fn abbreviate_for_display(repo: &Repository, oid: ObjectId, short: bool) -> Result<String> {
563 if short {
564 abbreviate_object_id(repo, oid, 7)
565 } else {
566 Ok(oid.to_hex())
567 }
568}
569
570fn map_resolve_ref_error(err: Error) -> RevParseError {
571 match err {
572 Error::RevParse(e) => e,
573 other => RevParseError::InvalidObjectName {
574 name: other.to_string(),
575 },
576 }
577}
578
579fn is_revision_set_exclude_spec(spec: &str) -> bool {
581 spec.starts_with('^')
582}
583
584fn infer_ref_names(repo: &Repository, spec: &str, oid: ObjectId) -> Option<ResolvedRefName> {
585 let rev_part = spec.split(':').next().unwrap_or(spec);
586 let (base, _) = parse_peel_suffix(rev_part);
587 if base.is_empty() || looks_like_abbrev_hex(base, repo) {
588 return None;
589 }
590 let resolved = match repo.resolve_ref_name(base) {
591 Ok(v) => v?,
592 Err(RevParseError::InvalidObjectName { .. }) => return None,
593 Err(_) => return None,
594 };
595 if repo.resolve_ref_oid(&resolved.full).ok() == Some(oid) {
596 Some(resolved)
597 } else {
598 None
599 }
600}
601
602fn looks_like_abbrev_hex(base: &str, repo: &Repository) -> bool {
603 if base.len() >= repo.odb().hash_algo().hex_len() {
604 return base.chars().all(|c| c.is_ascii_hexdigit());
605 }
606 base.len() >= 4 && base.chars().all(|c| c.is_ascii_hexdigit())
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn resolve_error_from_path_not_in_revision() {
615 let err = ResolveError::from(RevParseError::PathNotInRevision {
616 path: "missing.txt".into(),
617 revision: "HEAD".into(),
618 });
619 assert!(matches!(err, ResolveError::PathNotInTree { .. }));
620 }
621
622 #[test]
623 fn tree_entry_mode_cat_file_p_uses_six_digit_tree_mode() {
624 assert_eq!(tree_entry_mode_cat_file_p(0o040000), "040000");
625 assert_eq!(tree_entry_mode_cat_file_p(0o100644), "100644");
626 }
627 fn label_repo() -> (tempfile::TempDir, Repository, ObjectId) {
628 let dir = tempfile::tempdir().expect("tempdir");
629 let repo = Repository::init(
630 dir.path(),
631 &crate::repo::InitOptions::new().with_initial_branch("main"),
632 )
633 .expect("init");
634 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
635 let commit = repo.odb().write(ObjectKind::Commit, format!(
636 "tree {tree}\nauthor A <a@example.com> 1 +0000\ncommitter A <a@example.com> 1 +0000\n\ninitial\n"
637 ).as_bytes()).expect("commit");
638 crate::refs::write_ref(repo.git_dir(), "refs/heads/main", &commit).expect("branch");
639 (dir, repo, commit)
640 }
641
642 #[test]
643 fn revision_labels_distinguish_names_from_expressions() {
644 let (_dir, repo, oid) = label_repo();
645 for spec in ["HEAD", "main"] {
646 assert!(matches!(repo.revision_label(spec).expect("label"),
647 Some(RevisionLabel::Branch { name }) if name == "main"));
648 }
649 for spec in [
650 oid.to_hex().as_str(),
651 "HEAD~1",
652 "refs/heads/main",
653 "missing",
654 ] {
655 assert!(
656 repo.revision_label(spec).expect("label").is_none(),
657 "{spec}"
658 );
659 }
660 crate::refs::write_ref(repo.git_dir(), "HEAD", &oid).expect("detach");
661 assert!(repo
662 .revision_label("HEAD")
663 .expect("detached label")
664 .is_none());
665 crate::refs::write_symbolic_ref(repo.git_dir(), "HEAD", "refs/tags/main")
666 .expect("non-branch HEAD");
667 assert!(repo
668 .revision_label("HEAD")
669 .expect("non-branch label")
670 .is_none());
671 }
672
673 #[test]
674 fn revision_label_keeps_unborn_branch() {
675 let dir = tempfile::tempdir().expect("tempdir");
676 let repo = Repository::init(
677 dir.path(),
678 &crate::repo::InitOptions::new().with_initial_branch("new"),
679 )
680 .expect("init");
681 assert!(matches!(repo.revision_label("HEAD").expect("label"),
682 Some(RevisionLabel::Branch { name }) if name == "new"));
683 assert!(repo
684 .revision_label("new")
685 .expect("missing branch")
686 .is_none());
687 }
688
689 #[test]
690 fn revision_label_preserves_tag_metadata_and_branch_precedence() {
691 let (_dir, repo, oid) = label_repo();
692 crate::refs::write_ref(repo.git_dir(), "refs/tags/main", &oid).expect("tag");
693 assert!(matches!(
694 repo.revision_label("main").expect("branch wins"),
695 Some(RevisionLabel::Branch { .. })
696 ));
697 crate::refs::write_ref(repo.git_dir(), "refs/tags/light", &oid).expect("tag");
698 assert!(
699 matches!(repo.revision_label("light").expect("lightweight tag"),
700 Some(RevisionLabel::Tag { name, annotation: None }) if name == "light")
701 );
702
703 let blob = repo
704 .odb()
705 .write(ObjectKind::Blob, b"content")
706 .expect("blob");
707 let tag = repo
708 .odb()
709 .write(
710 ObjectKind::Tag,
711 format!("object {blob}\ntype blob\ntag original\n\nmessage\n\n").as_bytes(),
712 )
713 .expect("tag object");
714 crate::refs::write_ref(repo.git_dir(), "refs/tags/alias", &tag).expect("tag ref");
715 let Some(RevisionLabel::Tag {
716 name,
717 annotation: Some(tag),
718 }) = repo.revision_label("alias").expect("annotated tag")
719 else {
720 panic!("expected annotation");
721 };
722 assert_eq!(name, "alias");
723 assert_eq!(tag.tag, "original");
724 assert_eq!(tag.object, blob);
725 assert_eq!(tag.object_type, "blob");
726 assert_eq!(tag.tagger, None);
727 assert_eq!(tag.message, "message\n\n");
728 }
729
730 #[test]
731 fn revision_label_propagates_tag_object_errors() {
732 let (_dir, repo, _oid) = label_repo();
733 let broken = repo
734 .odb()
735 .write(ObjectKind::Tag, b"invalid")
736 .expect("broken object");
737 crate::refs::write_ref(repo.git_dir(), "refs/tags/broken", &broken).expect("tag ref");
738 assert!(matches!(
739 repo.revision_label("broken"),
740 Err(Error::CorruptObject(_))
741 ));
742 let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").expect("oid");
743 let dangling = crate::refs::store::RefTransaction::new()
744 .update(crate::refs::store::RefUpdate {
745 name: "refs/tags/missing".to_owned(),
746 new_value: Some(RawRef::Direct(missing)),
747 expected: crate::refs::store::Expected::Any,
748 reflog: None,
749 flags: crate::refs::store::RefUpdateFlags {
750 skip_oid_verification: true,
751 ..Default::default()
752 },
753 })
754 .expect("txn");
755 repo.refs()
756 .prepare(dangling)
757 .and_then(|prepared| prepared.commit())
758 .expect("dangling tag ref");
759 assert!(matches!(
760 repo.revision_label("missing"),
761 Err(Error::ObjectNotFound(_))
762 ));
763 }
764 #[test]
765 fn revision_labels_use_injected_ref_store() {
766 use crate::refs::store::MemoryRefStore;
767 use std::sync::Arc;
768
769 let (_dir, repo, oid) = label_repo();
770 let refs = Arc::new(MemoryRefStore::new());
771 refs.set_ref("HEAD", RawRef::Symbolic("refs/heads/virtual".into()))
772 .expect("HEAD");
773 refs.set_ref("refs/heads/virtual", RawRef::Direct(oid))
774 .expect("branch");
775 refs.set_ref("refs/tags/virtual-tag", RawRef::Direct(oid))
776 .expect("tag");
777 let repo = repo.with_ref_store(refs);
778 for spec in ["HEAD", "virtual"] {
779 assert!(matches!(repo.revision_label(spec).expect("branch label"),
780 Some(RevisionLabel::Branch { name }) if name == "virtual"));
781 }
782 assert!(
783 matches!(repo.revision_label("virtual-tag").expect("tag label"),
784 Some(RevisionLabel::Tag { name, annotation: None }) if name == "virtual-tag")
785 );
786 assert!(repo
787 .revision_label("main")
788 .expect("disk branch is excluded")
789 .is_none());
790 }
791}