use thiserror::Error;
use crate::diagnostics::Warning;
use crate::error::{Error, Result};
use crate::objects::odb::store::ObjectStream;
use crate::objects::{
parse_commit, parse_tag, parse_tree, CommitData, Object, ObjectId, ObjectInfo, ObjectKind,
TagData, TreeEntry,
};
use crate::refs::store::RawRef;
use crate::refs::{
expand_ref_dwim_full_name, shorten_unambiguous, warn_ambiguous_refs_from_config,
};
use crate::repo::Repository;
use crate::rev_parse::{
abbreviate_object_id, ambiguous_object_hint_lines, expand_at_minus_to_branch_name,
parse_peel_suffix, resolve_revision, resolve_revision_for_verify,
resolve_upstream_symbolic_name, split_double_dot_range, split_triple_dot_range,
upstream_suffix_info,
};
use crate::rev_parse_error::{AmbiguousChangeIdHint, AmbiguousObjectHint, RevParseError};
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ResolveError {
#[error("unknown revision '{spec}'")]
UnknownRevision {
spec: String,
},
#[error("ambiguous short object id '{spec}'")]
AmbiguousShortObject {
spec: String,
candidates: Vec<AmbiguousObjectHint>,
},
#[error("ambiguous change id '{spec}'")]
AmbiguousChangeId {
spec: String,
candidates: Vec<AmbiguousChangeIdHint>,
},
#[error("path '{path}' does not exist in '{revision}'")]
PathNotInTree {
path: String,
revision: String,
},
#[error("revision range or set '{spec}' is not a single object")]
RevisionRange {
spec: String,
},
#[error("object '{spec}' is a {found}, not a {expected}")]
WrongObjectKind {
spec: String,
found: ObjectKind,
expected: ObjectKind,
},
#[error("invalid reflog date '{date}'")]
ReflogDateUnparseable {
date: String,
},
#[error("{0}")]
DamagedObject(#[source] Error),
}
impl From<RevParseError> for ResolveError {
fn from(value: RevParseError) -> Self {
match value {
RevParseError::PathNotInRevision { path, revision }
| RevParseError::PathOnDiskNotInRevision { path, revision } => {
ResolveError::PathNotInTree { path, revision }
}
RevParseError::InvalidObjectName { name } => {
ResolveError::UnknownRevision { spec: name }
}
RevParseError::AmbiguousChangeId { prefix, candidates } => {
ResolveError::AmbiguousChangeId {
spec: prefix,
candidates,
}
}
RevParseError::ReflogDateUnparseable { date } => {
ResolveError::ReflogDateUnparseable { date }
}
other => ResolveError::UnknownRevision {
spec: other.to_string(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRefName {
pub full: String,
pub short: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedObject {
pub oid: ObjectId,
pub kind: ObjectKind,
pub size: u64,
pub ref_name: Option<String>,
pub full_ref: Option<String>,
}
#[derive(Debug, Clone)]
pub enum ObjectContents {
Blob(Vec<u8>),
Tree(Vec<TreeEntry>),
Commit(CommitData),
Tag(TagData),
}
#[derive(Debug, Clone)]
pub enum RevisionLabel {
Branch {
name: String,
},
Tag {
name: String,
annotation: Option<TagData>,
},
}
impl Repository {
#[inline]
pub fn revision_label(&self, spec: &str) -> Result<Option<RevisionLabel>> {
if spec == "HEAD" {
return Ok(crate::refs::read_symbolic_ref_for_repository(self, "HEAD")
.ok()
.flatten()
.and_then(|target| {
target
.strip_prefix("refs/heads/")
.map(|name| RevisionLabel::Branch {
name: name.to_owned(),
})
}));
}
if self.refs().resolve(&format!("refs/heads/{spec}")).is_ok() {
return Ok(Some(RevisionLabel::Branch {
name: spec.to_owned(),
}));
}
let Ok(oid) = self.refs().resolve(&format!("refs/tags/{spec}")) else {
return Ok(None);
};
let object = self.odb().read(&oid)?;
let annotation = if object.kind == ObjectKind::Tag {
Some(parse_tag(&object.data)?)
} else {
None
};
Ok(Some(RevisionLabel::Tag {
name: spec.to_owned(),
annotation,
}))
}
pub fn resolve(&self, spec: &str) -> std::result::Result<ResolvedObject, ResolveError> {
let oid = match resolve_revision(self, spec) {
Ok(o) => o,
Err(Error::ObjectNotFound(_)) => {
return Err(ResolveError::UnknownRevision {
spec: spec.to_owned(),
});
}
Err(Error::RevParse(RevParseError::AmbiguousShortObject { spec: prefix })) => {
let candidates =
ambiguous_object_hint_lines(self, &prefix, None).unwrap_or_default();
return Err(ResolveError::AmbiguousShortObject {
spec: spec.to_owned(),
candidates,
});
}
Err(Error::RevParse(RevParseError::AmbiguousChangeId { prefix, candidates })) => {
return Err(ResolveError::AmbiguousChangeId {
spec: prefix,
candidates,
});
}
Err(Error::RevParse(e)) => return Err(e.into()),
Err(_) => {
return Err(ResolveError::UnknownRevision {
spec: spec.to_owned(),
});
}
};
let info = self.read_info_replaced(&oid).map_err(|e| {
if e.is_damaged_object() {
ResolveError::DamagedObject(e)
} else {
ResolveError::UnknownRevision {
spec: spec.to_owned(),
}
}
})?;
let ref_names = infer_ref_names(self, spec, oid);
Ok(ResolvedObject {
oid,
kind: info.kind,
size: info.size,
ref_name: ref_names.as_ref().map(|n| n.short.clone()),
full_ref: ref_names.map(|n| n.full),
})
}
pub fn verify_object(
&self,
spec: &str,
expected: Option<ObjectKind>,
) -> std::result::Result<ObjectId, ResolveError> {
if split_double_dot_range(spec).is_some()
|| split_triple_dot_range(spec).is_some()
|| is_revision_set_exclude_spec(spec)
{
return Err(ResolveError::RevisionRange {
spec: spec.to_owned(),
});
}
let oid = match resolve_revision_for_verify(self, spec) {
Ok(o) => o,
Err(Error::ObjectNotFound(_)) => {
return Err(ResolveError::UnknownRevision {
spec: spec.to_owned(),
});
}
Err(Error::RevParse(RevParseError::AmbiguousShortObject { spec: prefix })) => {
let candidates =
ambiguous_object_hint_lines(self, &prefix, None).unwrap_or_default();
return Err(ResolveError::AmbiguousShortObject {
spec: spec.to_owned(),
candidates,
});
}
Err(Error::RevParse(RevParseError::AmbiguousChangeId { prefix, candidates })) => {
return Err(ResolveError::AmbiguousChangeId {
spec: prefix,
candidates,
});
}
Err(Error::RevParse(e)) => return Err(e.into()),
Err(_) => {
return Err(ResolveError::UnknownRevision {
spec: spec.to_owned(),
});
}
};
let info = self.read_info_replaced(&oid).map_err(|e| {
if e.is_damaged_object() {
ResolveError::DamagedObject(e)
} else {
ResolveError::UnknownRevision {
spec: spec.to_owned(),
}
}
})?;
if let Some(want) = expected {
if info.kind != want {
return Err(ResolveError::WrongObjectKind {
spec: spec.to_owned(),
found: info.kind,
expected: want,
});
}
}
Ok(oid)
}
pub fn resolve_ref_name(
&self,
spec: &str,
) -> std::result::Result<Option<ResolvedRefName>, RevParseError> {
let strict = warn_ambiguous_refs_from_config(self);
if upstream_suffix_info(spec).is_some() {
let full = resolve_upstream_symbolic_name(self, spec).map_err(map_resolve_ref_error)?;
let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
return Ok(Some(ResolvedRefName { full, short }));
}
if let Some(branch) =
expand_at_minus_to_branch_name(self, spec).map_err(map_resolve_ref_error)?
{
let full = format!("refs/heads/{branch}");
if self.resolve_ref_oid(&full).is_err() {
return Ok(None);
}
let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
return Ok(Some(ResolvedRefName { full, short }));
}
let (count, full) = expand_ref_dwim_full_name(self, spec).map_err(map_resolve_ref_error)?;
if count == 0 {
if spec == "HEAD" {
match self
.refs()
.read_raw("HEAD")
.map_err(map_resolve_ref_error)?
{
Some(RawRef::Direct(_)) => {
return Ok(Some(ResolvedRefName {
full: "HEAD".to_owned(),
short: "HEAD".to_owned(),
}));
}
Some(RawRef::Symbolic(target)) if self.resolve_ref_oid(&target).is_err() => {
return Ok(None);
}
Some(RawRef::Symbolic(_)) | None => {}
}
}
return Ok(None);
}
if count > 1 && strict {
self.warn(Warning::AmbiguousRefname {
spec: spec.to_owned(),
});
return Ok(None);
}
let full = full.ok_or_else(|| RevParseError::AmbiguousRefname {
spec: spec.to_owned(),
})?;
let short = shorten_unambiguous(self, &full, strict).map_err(map_resolve_ref_error)?;
Ok(Some(ResolvedRefName { full, short }))
}
pub fn read_object_raw_for_display(&self, oid: &ObjectId) -> Result<Object> {
self.read_replaced(oid)
}
pub fn read_object_info_for_display(&self, oid: &ObjectId) -> Result<ObjectInfo> {
self.read_info_replaced(oid)
}
pub fn with_object_stream_for_display<R>(
&self,
oid: &ObjectId,
f: impl FnOnce(ObjectStream<'_>) -> Result<R>,
) -> Result<Option<R>> {
let effective = self.object_id_after_replace(oid);
self.odb().with_stream(&effective, f)
}
pub fn read_object_for_display(&self, oid: &ObjectId) -> Result<ObjectContents> {
let obj = self.read_object_raw_for_display(oid)?;
match obj.kind {
ObjectKind::Blob => Ok(ObjectContents::Blob(obj.data)),
ObjectKind::Tree => Ok(ObjectContents::Tree(parse_tree(&obj.data)?)),
ObjectKind::Commit => Ok(ObjectContents::Commit(parse_commit(&obj.data)?)),
ObjectKind::Tag => Ok(ObjectContents::Tag(parse_tag(&obj.data)?)),
}
}
}
#[must_use]
pub fn tree_entry_mode_cat_file_p(mode: u32) -> String {
if mode == 0o040000 {
"040000".to_owned()
} else {
format!("{:o}", mode)
}
}
#[must_use]
pub fn tree_entry_cat_file_type(mode: u32) -> &'static str {
match mode & 0o170_000 {
0o040_000 => "tree",
0o160_000 => "commit",
_ => "blob",
}
}
#[must_use]
pub fn format_tree_entries_cat_file_p(entries: &[TreeEntry]) -> Vec<u8> {
let mut out = Vec::new();
for e in entries {
out.extend_from_slice(tree_entry_mode_cat_file_p(e.mode).as_bytes());
out.push(b' ');
out.extend_from_slice(tree_entry_cat_file_type(e.mode).as_bytes());
out.push(b' ');
out.extend_from_slice(e.oid.to_hex().as_bytes());
out.push(b'\t');
out.extend_from_slice(&e.name);
out.push(b'\n');
}
out
}
pub fn format_tree_cat_file_p(data: &[u8]) -> Result<Vec<u8>> {
let entries = parse_tree(data)?;
Ok(format_tree_entries_cat_file_p(&entries))
}
#[cfg(test)]
mod tree_entry_cat_file_type_tests {
use super::tree_entry_cat_file_type;
#[test]
fn mode_mask_labels_match_git() {
assert_eq!(tree_entry_cat_file_type(0o040_000), "tree");
assert_eq!(tree_entry_cat_file_type(0o040_755), "tree");
assert_eq!(tree_entry_cat_file_type(0o160_000), "commit");
assert_eq!(tree_entry_cat_file_type(0o100_644), "blob");
assert_eq!(tree_entry_cat_file_type(0o100_755), "blob");
assert_eq!(tree_entry_cat_file_type(0o120_000), "blob");
assert_eq!(tree_entry_cat_file_type(0o100_664), "blob");
}
}
pub fn abbreviate_for_display(repo: &Repository, oid: ObjectId, short: bool) -> Result<String> {
if short {
let config = repo.config()?.clone();
let min_len = crate::commit_pretty::resolve_core_abbrev_len(&config, repo.odb())?;
abbreviate_object_id(repo, oid, min_len)
} else {
Ok(oid.to_hex())
}
}
#[cfg(test)]
mod abbreviate_for_display_tests {
use std::collections::HashMap;
use crate::hash::hash_object;
use crate::objects::{HashAlgo, ObjectId, ObjectKind};
use crate::repo::{InitOptions, Repository};
use crate::rev_parse::resolve_revision;
use super::abbreviate_for_display;
fn two_blobs_sharing_seven_hex_prefix() -> (ObjectId, ObjectId, Vec<u8>, Vec<u8>) {
let mut first_by_prefix: HashMap<String, (ObjectId, Vec<u8>)> = HashMap::new();
for n in 0_u64..2_000_000 {
let data = n.to_string().into_bytes();
let oid = hash_object(HashAlgo::Sha1, ObjectKind::Blob, &data).expect("hash");
let prefix = oid.to_hex()[..7].to_string();
if let Some((other, _)) = first_by_prefix.get(&prefix) {
if *other != oid {
let (first_oid, first_data) = first_by_prefix.remove(&prefix).expect("prefix");
return (first_oid, oid, first_data, data);
}
} else {
first_by_prefix.insert(prefix, (oid, data));
}
}
panic!("failed to find two blobs sharing a 7-hex prefix");
}
#[test]
fn display_abbrev_extends_on_collision_and_rev_parse_round_trips() -> crate::error::Result<()> {
let dir = tempfile::TempDir::new().expect("tempdir");
let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
let (a, b, data_a, data_b) = two_blobs_sharing_seven_hex_prefix();
assert_eq!(&a.to_hex()[..7], &b.to_hex()[..7]);
repo.odb().write(ObjectKind::Blob, &data_a)?;
repo.odb().write(ObjectKind::Blob, &data_b)?;
for oid in [a, b] {
let abbrev = abbreviate_for_display(&repo, oid, true)?;
assert!(abbrev.len() > 7);
assert_eq!(resolve_revision(&repo, &abbrev)?, oid);
}
Ok(())
}
}
fn map_resolve_ref_error(err: Error) -> RevParseError {
match err {
Error::RevParse(e) => e,
other => RevParseError::InvalidObjectName {
name: other.to_string(),
},
}
}
fn is_revision_set_exclude_spec(spec: &str) -> bool {
spec.starts_with('^')
}
fn infer_ref_names(repo: &Repository, spec: &str, oid: ObjectId) -> Option<ResolvedRefName> {
let rev_part = spec.split(':').next().unwrap_or(spec);
let (base, _) = parse_peel_suffix(rev_part);
if base.is_empty() || looks_like_abbrev_hex(base, repo) {
return None;
}
let resolved = match repo.resolve_ref_name(base) {
Ok(v) => v?,
Err(RevParseError::InvalidObjectName { .. }) => return None,
Err(_) => return None,
};
if repo.resolve_ref_oid(&resolved.full).ok() == Some(oid) {
Some(resolved)
} else {
None
}
}
fn looks_like_abbrev_hex(base: &str, repo: &Repository) -> bool {
if base.len() >= repo.odb().hash_algo().hex_len() {
return base.chars().all(|c| c.is_ascii_hexdigit());
}
base.len() >= 4 && base.chars().all(|c| c.is_ascii_hexdigit())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_error_from_path_not_in_revision() {
let err = ResolveError::from(RevParseError::PathNotInRevision {
path: "missing.txt".into(),
revision: "HEAD".into(),
});
assert!(matches!(err, ResolveError::PathNotInTree { .. }));
}
#[test]
fn tree_entry_mode_cat_file_p_uses_six_digit_tree_mode() {
assert_eq!(tree_entry_mode_cat_file_p(0o040000), "040000");
assert_eq!(tree_entry_mode_cat_file_p(0o100644), "100644");
}
fn label_repo() -> (tempfile::TempDir, Repository, ObjectId) {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Repository::init(
dir.path(),
&crate::repo::InitOptions::new().with_initial_branch("main"),
)
.expect("init");
let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
let commit = repo.odb().write(ObjectKind::Commit, format!(
"tree {tree}\nauthor A <a@example.com> 1 +0000\ncommitter A <a@example.com> 1 +0000\n\ninitial\n"
).as_bytes()).expect("commit");
crate::refs::write_ref(repo.git_dir(), "refs/heads/main", &commit).expect("branch");
(dir, repo, commit)
}
#[test]
fn revision_labels_distinguish_names_from_expressions() {
let (_dir, repo, oid) = label_repo();
for spec in ["HEAD", "main"] {
assert!(matches!(repo.revision_label(spec).expect("label"),
Some(RevisionLabel::Branch { name }) if name == "main"));
}
for spec in [
oid.to_hex().as_str(),
"HEAD~1",
"refs/heads/main",
"missing",
] {
assert!(
repo.revision_label(spec).expect("label").is_none(),
"{spec}"
);
}
crate::refs::write_ref(repo.git_dir(), "HEAD", &oid).expect("detach");
assert!(repo
.revision_label("HEAD")
.expect("detached label")
.is_none());
crate::refs::write_symbolic_ref(repo.git_dir(), "HEAD", "refs/tags/main")
.expect("non-branch HEAD");
assert!(repo
.revision_label("HEAD")
.expect("non-branch label")
.is_none());
}
#[test]
fn revision_label_keeps_unborn_branch() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Repository::init(
dir.path(),
&crate::repo::InitOptions::new().with_initial_branch("new"),
)
.expect("init");
assert!(matches!(repo.revision_label("HEAD").expect("label"),
Some(RevisionLabel::Branch { name }) if name == "new"));
assert!(repo
.revision_label("new")
.expect("missing branch")
.is_none());
}
#[test]
fn revision_label_preserves_tag_metadata_and_branch_precedence() {
let (_dir, repo, oid) = label_repo();
crate::refs::write_ref(repo.git_dir(), "refs/tags/main", &oid).expect("tag");
assert!(matches!(
repo.revision_label("main").expect("branch wins"),
Some(RevisionLabel::Branch { .. })
));
crate::refs::write_ref(repo.git_dir(), "refs/tags/light", &oid).expect("tag");
assert!(
matches!(repo.revision_label("light").expect("lightweight tag"),
Some(RevisionLabel::Tag { name, annotation: None }) if name == "light")
);
let blob = repo
.odb()
.write(ObjectKind::Blob, b"content")
.expect("blob");
let tag = repo
.odb()
.write(
ObjectKind::Tag,
format!("object {blob}\ntype blob\ntag original\n\nmessage\n\n").as_bytes(),
)
.expect("tag object");
crate::refs::write_ref(repo.git_dir(), "refs/tags/alias", &tag).expect("tag ref");
let Some(RevisionLabel::Tag {
name,
annotation: Some(tag),
}) = repo.revision_label("alias").expect("annotated tag")
else {
panic!("expected annotation");
};
assert_eq!(name, "alias");
assert_eq!(tag.tag, "original");
assert_eq!(tag.object, blob);
assert_eq!(tag.object_type, "blob");
assert_eq!(tag.tagger, None);
assert_eq!(tag.message, "message\n\n");
}
#[test]
fn revision_label_propagates_tag_object_errors() {
let (_dir, repo, _oid) = label_repo();
let broken = repo
.odb()
.write(ObjectKind::Tag, b"invalid")
.expect("broken object");
crate::refs::write_ref(repo.git_dir(), "refs/tags/broken", &broken).expect("tag ref");
assert!(matches!(
repo.revision_label("broken"),
Err(Error::CorruptObject(_))
));
let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").expect("oid");
let dangling = crate::refs::store::RefTransaction::new()
.update(crate::refs::store::RefUpdate {
name: "refs/tags/missing".to_owned(),
new_value: Some(RawRef::Direct(missing)),
expected: crate::refs::store::Expected::Any,
reflog: None,
flags: crate::refs::store::RefUpdateFlags {
skip_oid_verification: true,
..Default::default()
},
})
.expect("txn");
repo.refs()
.prepare(dangling)
.and_then(|prepared| prepared.commit())
.expect("dangling tag ref");
assert!(matches!(
repo.revision_label("missing"),
Err(Error::ObjectNotFound(_))
));
}
#[test]
fn revision_labels_use_injected_ref_store() {
use crate::refs::store::MemoryRefStore;
use std::sync::Arc;
let (_dir, repo, oid) = label_repo();
let refs = Arc::new(MemoryRefStore::new());
refs.set_ref("HEAD", RawRef::Symbolic("refs/heads/virtual".into()))
.expect("HEAD");
refs.set_ref("refs/heads/virtual", RawRef::Direct(oid))
.expect("branch");
refs.set_ref("refs/tags/virtual-tag", RawRef::Direct(oid))
.expect("tag");
let repo = repo.with_ref_store(refs);
for spec in ["HEAD", "virtual"] {
assert!(matches!(repo.revision_label(spec).expect("branch label"),
Some(RevisionLabel::Branch { name }) if name == "virtual"));
}
assert!(
matches!(repo.revision_label("virtual-tag").expect("tag label"),
Some(RevisionLabel::Tag { name, annotation: None }) if name == "virtual-tag")
);
assert!(repo
.revision_label("main")
.expect("disk branch is excluded")
.is_none());
}
}