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
100impl From<RevParseError> for ResolveError {
101 fn from(value: RevParseError) -> Self {
102 match value {
103 RevParseError::PathNotInRevision { path, revision }
104 | RevParseError::PathOnDiskNotInRevision { path, revision } => {
105 ResolveError::PathNotInTree { path, revision }
106 }
107 RevParseError::InvalidObjectName { name } => {
108 ResolveError::UnknownRevision { spec: name }
109 }
110 RevParseError::AmbiguousChangeId { prefix, candidates } => {
111 ResolveError::AmbiguousChangeId {
112 spec: prefix,
113 candidates,
114 }
115 }
116 other => ResolveError::UnknownRevision {
117 spec: other.to_string(),
118 },
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct ResolvedRefName {
126 pub full: String,
128 pub short: String,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct ResolvedObject {
135 pub oid: ObjectId,
137 pub kind: ObjectKind,
139 pub size: u64,
141 pub ref_name: Option<String>,
143 pub full_ref: Option<String>,
145}
146
147#[derive(Debug, Clone)]
149pub enum ObjectContents {
150 Blob(Vec<u8>),
152 Tree(Vec<TreeEntry>),
154 Commit(CommitData),
156 Tag(TagData),
158}
159
160#[derive(Debug, Clone)]
165pub enum RevisionLabel {
166 Branch {
168 name: String,
170 },
171 Tag {
173 name: String,
175 annotation: Option<TagData>,
177 },
178}
179
180impl Repository {
181 #[inline]
199 pub fn revision_label(&self, spec: &str) -> Result<Option<RevisionLabel>> {
200 if spec == "HEAD" {
201 return Ok(crate::refs::read_symbolic_ref_for_repository(self, "HEAD")
202 .ok()
203 .flatten()
204 .and_then(|target| {
205 target
206 .strip_prefix("refs/heads/")
207 .map(|name| RevisionLabel::Branch {
208 name: name.to_owned(),
209 })
210 }));
211 }
212
213 if self.refs().resolve(&format!("refs/heads/{spec}")).is_ok() {
214 return Ok(Some(RevisionLabel::Branch {
215 name: spec.to_owned(),
216 }));
217 }
218
219 let Ok(oid) = self.refs().resolve(&format!("refs/tags/{spec}")) else {
220 return Ok(None);
221 };
222 let object = self.odb().read(&oid)?;
223 let annotation = if object.kind == ObjectKind::Tag {
224 Some(parse_tag(&object.data)?)
225 } else {
226 None
227 };
228 Ok(Some(RevisionLabel::Tag {
229 name: spec.to_owned(),
230 annotation,
231 }))
232 }
233
234 pub fn resolve(&self, spec: &str) -> std::result::Result<ResolvedObject, ResolveError> {
245 let oid = match resolve_revision(self, spec) {
246 Ok(o) => o,
247 Err(Error::ObjectNotFound(_)) => {
248 return Err(ResolveError::UnknownRevision {
249 spec: spec.to_owned(),
250 });
251 }
252 Err(Error::RevParse(RevParseError::AmbiguousShortObject { spec: prefix })) => {
253 let candidates =
254 ambiguous_object_hint_lines(self, &prefix, None).unwrap_or_default();
255 return Err(ResolveError::AmbiguousShortObject {
256 spec: spec.to_owned(),
257 candidates,
258 });
259 }
260 Err(Error::RevParse(RevParseError::AmbiguousChangeId { prefix, candidates })) => {
261 return Err(ResolveError::AmbiguousChangeId {
262 spec: prefix,
263 candidates,
264 });
265 }
266 Err(Error::RevParse(e)) => return Err(e.into()),
267 Err(_) => {
268 return Err(ResolveError::UnknownRevision {
269 spec: spec.to_owned(),
270 });
271 }
272 };
273
274 let info = self
275 .read_info_replaced(&oid)
276 .map_err(|_| ResolveError::UnknownRevision {
277 spec: spec.to_owned(),
278 })?;
279 let ref_names = infer_ref_names(self, spec, oid);
280 Ok(ResolvedObject {
281 oid,
282 kind: info.kind,
283 size: info.size,
284 ref_name: ref_names.as_ref().map(|n| n.short.clone()),
285 full_ref: ref_names.map(|n| n.full),
286 })
287 }
288
289 pub fn verify_object(
302 &self,
303 spec: &str,
304 expected: Option<ObjectKind>,
305 ) -> std::result::Result<ObjectId, ResolveError> {
306 if split_double_dot_range(spec).is_some()
307 || split_triple_dot_range(spec).is_some()
308 || is_revision_set_exclude_spec(spec)
309 {
310 return Err(ResolveError::RevisionRange {
311 spec: spec.to_owned(),
312 });
313 }
314
315 let oid = match resolve_revision_for_verify(self, spec) {
316 Ok(o) => o,
317 Err(Error::ObjectNotFound(_)) => {
318 return Err(ResolveError::UnknownRevision {
319 spec: spec.to_owned(),
320 });
321 }
322 Err(Error::RevParse(RevParseError::AmbiguousShortObject { spec: prefix })) => {
323 let candidates =
324 ambiguous_object_hint_lines(self, &prefix, None).unwrap_or_default();
325 return Err(ResolveError::AmbiguousShortObject {
326 spec: spec.to_owned(),
327 candidates,
328 });
329 }
330 Err(Error::RevParse(RevParseError::AmbiguousChangeId { prefix, candidates })) => {
331 return Err(ResolveError::AmbiguousChangeId {
332 spec: prefix,
333 candidates,
334 });
335 }
336 Err(Error::RevParse(e)) => return Err(e.into()),
337 Err(_) => {
338 return Err(ResolveError::UnknownRevision {
339 spec: spec.to_owned(),
340 });
341 }
342 };
343
344 let info = self
345 .read_info_replaced(&oid)
346 .map_err(|_| ResolveError::UnknownRevision {
347 spec: spec.to_owned(),
348 })?;
349
350 if let Some(want) = expected {
351 if info.kind != want {
352 return Err(ResolveError::WrongObjectKind {
353 spec: spec.to_owned(),
354 found: info.kind,
355 expected: want,
356 });
357 }
358 }
359
360 Ok(oid)
361 }
362
363 pub fn resolve_ref_name(
373 &self,
374 spec: &str,
375 ) -> std::result::Result<Option<ResolvedRefName>, RevParseError> {
376 let strict = warn_ambiguous_refs_from_config(self);
377 if upstream_suffix_info(spec).is_some() {
378 let full = resolve_upstream_symbolic_name(self, spec).map_err(map_resolve_ref_error)?;
379 let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
380 return Ok(Some(ResolvedRefName { full, short }));
381 }
382 if let Some(branch) =
383 expand_at_minus_to_branch_name(self, spec).map_err(map_resolve_ref_error)?
384 {
385 let full = format!("refs/heads/{branch}");
386 if self.resolve_ref_oid(&full).is_err() {
387 return Ok(None);
388 }
389 let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
390 return Ok(Some(ResolvedRefName { full, short }));
391 }
392 let (count, full) = expand_ref_dwim_full_name(self, spec).map_err(map_resolve_ref_error)?;
393 if count == 0 {
394 if spec == "HEAD" {
395 match self
396 .refs()
397 .read_raw("HEAD")
398 .map_err(map_resolve_ref_error)?
399 {
400 Some(RawRef::Direct(_)) => {
401 return Ok(Some(ResolvedRefName {
402 full: "HEAD".to_owned(),
403 short: "HEAD".to_owned(),
404 }));
405 }
406 Some(RawRef::Symbolic(target)) if self.resolve_ref_oid(&target).is_err() => {
407 return Ok(None);
408 }
409 Some(RawRef::Symbolic(_)) | None => {}
410 }
411 }
412 return Ok(None);
413 }
414 if count > 1 && strict {
415 self.warn(Warning::AmbiguousRefname {
416 spec: spec.to_owned(),
417 });
418 return Ok(None);
419 }
420 let full = full.ok_or_else(|| RevParseError::AmbiguousRefname {
421 spec: spec.to_owned(),
422 })?;
423 let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
424 Ok(Some(ResolvedRefName { full, short }))
425 }
426
427 pub fn read_object_raw_for_display(&self, oid: &ObjectId) -> Result<Object> {
433 self.read_replaced(oid)
434 }
435
436 pub fn read_object_info_for_display(&self, oid: &ObjectId) -> Result<ObjectInfo> {
445 self.read_info_replaced(oid)
446 }
447
448 pub fn with_object_stream_for_display<R>(
460 &self,
461 oid: &ObjectId,
462 f: impl FnOnce(ObjectStream<'_>) -> Result<R>,
463 ) -> Result<Option<R>> {
464 let effective = self.object_id_after_replace(oid);
465 self.odb().with_stream(&effective, f)
466 }
467
468 pub fn read_object_for_display(&self, oid: &ObjectId) -> Result<ObjectContents> {
475 let obj = self.read_object_raw_for_display(oid)?;
476 match obj.kind {
477 ObjectKind::Blob => Ok(ObjectContents::Blob(obj.data)),
478 ObjectKind::Tree => Ok(ObjectContents::Tree(parse_tree(&obj.data)?)),
479 ObjectKind::Commit => Ok(ObjectContents::Commit(parse_commit(&obj.data)?)),
480 ObjectKind::Tag => Ok(ObjectContents::Tag(parse_tag(&obj.data)?)),
481 }
482 }
483}
484
485#[must_use]
487pub fn tree_entry_mode_cat_file_p(mode: u32) -> String {
488 if mode == 0o040000 {
489 "040000".to_owned()
490 } else {
491 format!("{:o}", mode)
492 }
493}
494
495#[must_use]
499pub fn tree_entry_cat_file_type(repo: &Repository, mode: u32, oid: &ObjectId) -> &'static str {
500 if mode == 0o040000 {
501 return "tree";
502 }
503 if mode == 0o160000 {
504 return "commit";
505 }
506 if let Ok(info) = repo.odb().read_info(oid) {
507 return object_kind_cat_file_label(info.kind);
508 }
509 "blob"
510}
511
512pub fn format_tree_cat_file_p(repo: &Repository, data: &[u8]) -> Result<Vec<u8>> {
518 let entries = parse_tree(data)?;
519 let mut out = Vec::new();
520 for e in &entries {
521 out.extend_from_slice(tree_entry_mode_cat_file_p(e.mode).as_bytes());
522 out.push(b' ');
523 out.extend_from_slice(tree_entry_cat_file_type(repo, e.mode, &e.oid).as_bytes());
524 out.push(b' ');
525 out.extend_from_slice(e.oid.to_hex().as_bytes());
526 out.push(b'\t');
527 out.extend_from_slice(&e.name);
528 out.push(b'\n');
529 }
530 Ok(out)
531}
532
533fn object_kind_cat_file_label(kind: ObjectKind) -> &'static str {
534 match kind {
535 ObjectKind::Blob => "blob",
536 ObjectKind::Tree => "tree",
537 ObjectKind::Commit => "commit",
538 ObjectKind::Tag => "tag",
539 }
540}
541
542pub fn abbreviate_for_display(repo: &Repository, oid: ObjectId, short: bool) -> Result<String> {
544 if short {
545 abbreviate_object_id(repo, oid, 7)
546 } else {
547 Ok(oid.to_hex())
548 }
549}
550
551fn map_resolve_ref_error(err: Error) -> RevParseError {
552 match err {
553 Error::RevParse(e) => e,
554 other => RevParseError::InvalidObjectName {
555 name: other.to_string(),
556 },
557 }
558}
559
560fn is_revision_set_exclude_spec(spec: &str) -> bool {
562 spec.starts_with('^')
563}
564
565fn infer_ref_names(repo: &Repository, spec: &str, oid: ObjectId) -> Option<ResolvedRefName> {
566 let rev_part = spec.split(':').next().unwrap_or(spec);
567 let (base, _) = parse_peel_suffix(rev_part);
568 if base.is_empty() || looks_like_abbrev_hex(base, repo) {
569 return None;
570 }
571 let resolved = match repo.resolve_ref_name(base) {
572 Ok(v) => v?,
573 Err(RevParseError::InvalidObjectName { .. }) => return None,
574 Err(_) => return None,
575 };
576 if repo.resolve_ref_oid(&resolved.full).ok() == Some(oid) {
577 Some(resolved)
578 } else {
579 None
580 }
581}
582
583fn looks_like_abbrev_hex(base: &str, repo: &Repository) -> bool {
584 if base.len() >= repo.odb().hash_algo().hex_len() {
585 return base.chars().all(|c| c.is_ascii_hexdigit());
586 }
587 base.len() >= 4 && base.chars().all(|c| c.is_ascii_hexdigit())
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593
594 #[test]
595 fn resolve_error_from_path_not_in_revision() {
596 let err = ResolveError::from(RevParseError::PathNotInRevision {
597 path: "missing.txt".into(),
598 revision: "HEAD".into(),
599 });
600 assert!(matches!(err, ResolveError::PathNotInTree { .. }));
601 }
602
603 #[test]
604 fn tree_entry_mode_cat_file_p_uses_six_digit_tree_mode() {
605 assert_eq!(tree_entry_mode_cat_file_p(0o040000), "040000");
606 assert_eq!(tree_entry_mode_cat_file_p(0o100644), "100644");
607 }
608 fn label_repo() -> (tempfile::TempDir, Repository, ObjectId) {
609 let dir = tempfile::tempdir().expect("tempdir");
610 let repo = Repository::init(
611 dir.path(),
612 &crate::repo::InitOptions::new().with_initial_branch("main"),
613 )
614 .expect("init");
615 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
616 let commit = repo.odb().write(ObjectKind::Commit, format!(
617 "tree {tree}\nauthor A <a@example.com> 1 +0000\ncommitter A <a@example.com> 1 +0000\n\ninitial\n"
618 ).as_bytes()).expect("commit");
619 crate::refs::write_ref(repo.git_dir(), "refs/heads/main", &commit).expect("branch");
620 (dir, repo, commit)
621 }
622
623 #[test]
624 fn revision_labels_distinguish_names_from_expressions() {
625 let (_dir, repo, oid) = label_repo();
626 for spec in ["HEAD", "main"] {
627 assert!(matches!(repo.revision_label(spec).expect("label"),
628 Some(RevisionLabel::Branch { name }) if name == "main"));
629 }
630 for spec in [
631 oid.to_hex().as_str(),
632 "HEAD~1",
633 "refs/heads/main",
634 "missing",
635 ] {
636 assert!(
637 repo.revision_label(spec).expect("label").is_none(),
638 "{spec}"
639 );
640 }
641 crate::refs::write_ref(repo.git_dir(), "HEAD", &oid).expect("detach");
642 assert!(repo
643 .revision_label("HEAD")
644 .expect("detached label")
645 .is_none());
646 crate::refs::write_symbolic_ref(repo.git_dir(), "HEAD", "refs/tags/main")
647 .expect("non-branch HEAD");
648 assert!(repo
649 .revision_label("HEAD")
650 .expect("non-branch label")
651 .is_none());
652 }
653
654 #[test]
655 fn revision_label_keeps_unborn_branch() {
656 let dir = tempfile::tempdir().expect("tempdir");
657 let repo = Repository::init(
658 dir.path(),
659 &crate::repo::InitOptions::new().with_initial_branch("new"),
660 )
661 .expect("init");
662 assert!(matches!(repo.revision_label("HEAD").expect("label"),
663 Some(RevisionLabel::Branch { name }) if name == "new"));
664 assert!(repo
665 .revision_label("new")
666 .expect("missing branch")
667 .is_none());
668 }
669
670 #[test]
671 fn revision_label_preserves_tag_metadata_and_branch_precedence() {
672 let (_dir, repo, oid) = label_repo();
673 crate::refs::write_ref(repo.git_dir(), "refs/tags/main", &oid).expect("tag");
674 assert!(matches!(
675 repo.revision_label("main").expect("branch wins"),
676 Some(RevisionLabel::Branch { .. })
677 ));
678 crate::refs::write_ref(repo.git_dir(), "refs/tags/light", &oid).expect("tag");
679 assert!(
680 matches!(repo.revision_label("light").expect("lightweight tag"),
681 Some(RevisionLabel::Tag { name, annotation: None }) if name == "light")
682 );
683
684 let blob = repo
685 .odb()
686 .write(ObjectKind::Blob, b"content")
687 .expect("blob");
688 let tag = repo
689 .odb()
690 .write(
691 ObjectKind::Tag,
692 format!("object {blob}\ntype blob\ntag original\n\nmessage\n\n").as_bytes(),
693 )
694 .expect("tag object");
695 crate::refs::write_ref(repo.git_dir(), "refs/tags/alias", &tag).expect("tag ref");
696 let Some(RevisionLabel::Tag {
697 name,
698 annotation: Some(tag),
699 }) = repo.revision_label("alias").expect("annotated tag")
700 else {
701 panic!("expected annotation");
702 };
703 assert_eq!(name, "alias");
704 assert_eq!(tag.tag, "original");
705 assert_eq!(tag.object, blob);
706 assert_eq!(tag.object_type, "blob");
707 assert_eq!(tag.tagger, None);
708 assert_eq!(tag.message, "message\n\n");
709 }
710
711 #[test]
712 fn revision_label_propagates_tag_object_errors() {
713 let (_dir, repo, _oid) = label_repo();
714 let broken = repo
715 .odb()
716 .write(ObjectKind::Tag, b"invalid")
717 .expect("broken object");
718 crate::refs::write_ref(repo.git_dir(), "refs/tags/broken", &broken).expect("tag ref");
719 assert!(matches!(
720 repo.revision_label("broken"),
721 Err(Error::CorruptObject(_))
722 ));
723 let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").expect("oid");
724 crate::refs::write_ref(repo.git_dir(), "refs/tags/missing", &missing).expect("tag ref");
725 assert!(matches!(
726 repo.revision_label("missing"),
727 Err(Error::ObjectNotFound(_))
728 ));
729 }
730 #[test]
731 fn revision_labels_use_injected_ref_store() {
732 use crate::refs::store::MemoryRefStore;
733 use std::sync::Arc;
734
735 let (_dir, repo, oid) = label_repo();
736 let refs = Arc::new(MemoryRefStore::new());
737 refs.set_ref("HEAD", RawRef::Symbolic("refs/heads/virtual".into()))
738 .expect("HEAD");
739 refs.set_ref("refs/heads/virtual", RawRef::Direct(oid))
740 .expect("branch");
741 refs.set_ref("refs/tags/virtual-tag", RawRef::Direct(oid))
742 .expect("tag");
743 let repo = repo.with_ref_store(refs);
744 for spec in ["HEAD", "virtual"] {
745 assert!(matches!(repo.revision_label(spec).expect("branch label"),
746 Some(RevisionLabel::Branch { name }) if name == "virtual"));
747 }
748 assert!(
749 matches!(repo.revision_label("virtual-tag").expect("tag label"),
750 Some(RevisionLabel::Tag { name, annotation: None }) if name == "virtual-tag")
751 );
752 assert!(repo
753 .revision_label("main")
754 .expect("disk branch is excluded")
755 .is_none());
756 }
757}