use std::collections::HashSet;
use crate::error::{Error, Result};
use crate::objects::{tag_object_line_oid, ObjectId, ObjectKind};
use super::Odb;
impl Odb {
pub fn peel_tags(&self, oid: ObjectId) -> Result<(ObjectId, ObjectKind)> {
let mut current = oid;
let mut visited: Option<HashSet<ObjectId>> = None;
loop {
let kind = self.read_info(¤t)?.kind;
if kind != ObjectKind::Tag {
return Ok((current, kind));
}
if !visited.get_or_insert_with(HashSet::new).insert(current) {
return Err(Error::CorruptObject(format!(
"tag chain from {oid} loops back to {current}"
)));
}
let tag = self.read(¤t)?;
current = tag_object_line_oid(&tag.data).ok_or_else(|| {
Error::CorruptObject(format!("tag {current} has no valid object header"))
})?;
}
}
pub fn peel_to_commit(&self, oid: ObjectId) -> Result<Option<ObjectId>> {
let (peeled, kind) = self.peel_tags(oid)?;
Ok((kind == ObjectKind::Commit).then_some(peeled))
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
use crate::objects::HashAlgo;
use crate::odb::{MemoryStore, OdbBuilder};
use std::sync::Arc;
use tempfile::TempDir;
fn tag(odb: &Odb, target: ObjectId, kind: &str, name: &str) -> ObjectId {
let body = format!(
"object {target}\ntype {kind}\ntag {name}\ntagger T <t@example.com> 0 +0000\n\nmsg\n"
);
odb.write(ObjectKind::Tag, body.as_bytes()).unwrap()
}
fn commit(odb: &Odb) -> ObjectId {
let tree = odb.write(ObjectKind::Tree, b"").unwrap();
let body = format!(
"tree {tree}\nauthor A <a@example.com> 0 +0000\ncommitter A <a@example.com> 0 +0000\n\nc\n"
);
odb.write(ObjectKind::Commit, body.as_bytes()).unwrap()
}
#[test]
fn non_tag_is_returned_unchanged() {
let dir = TempDir::new().unwrap();
let odb = Odb::new(dir.path());
let c = commit(&odb);
let blob = odb.write(ObjectKind::Blob, b"data").unwrap();
assert_eq!(odb.peel_tags(c).unwrap(), (c, ObjectKind::Commit));
assert_eq!(odb.peel_tags(blob).unwrap(), (blob, ObjectKind::Blob));
assert_eq!(odb.peel_to_commit(c).unwrap(), Some(c));
assert_eq!(odb.peel_to_commit(blob).unwrap(), None);
}
#[test]
fn nested_tags_peel_to_the_innermost_object() {
let dir = TempDir::new().unwrap();
let odb = Odb::new(dir.path());
let c = commit(&odb);
let inner = tag(&odb, c, "commit", "inner");
let outer = tag(&odb, inner, "tag", "outer");
assert_eq!(odb.peel_tags(outer).unwrap(), (c, ObjectKind::Commit));
assert_eq!(odb.peel_to_commit(outer).unwrap(), Some(c));
let blob = odb.write(ObjectKind::Blob, b"data").unwrap();
let tag_of_blob = tag(&odb, blob, "blob", "b");
assert_eq!(
odb.peel_tags(tag_of_blob).unwrap(),
(blob, ObjectKind::Blob)
);
assert_eq!(odb.peel_to_commit(tag_of_blob).unwrap(), None);
}
#[test]
fn missing_objects_are_errors() {
let dir = TempDir::new().unwrap();
let odb = Odb::new(dir.path());
let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
assert!(matches!(
odb.peel_tags(missing),
Err(Error::ObjectNotFound(_))
));
let dangling = tag(&odb, missing, "commit", "dangling");
assert!(matches!(
odb.peel_to_commit(dangling),
Err(Error::ObjectNotFound(_))
));
}
#[test]
fn only_the_object_header_is_required() {
let dir = TempDir::new().unwrap();
let odb = Odb::new(dir.path());
let c = commit(&odb);
let mut body = format!("object {c}\ntype commit\n\n").into_bytes();
body.extend_from_slice(b"latin-1 message \xe9\n");
let bare = odb.write(ObjectKind::Tag, &body).unwrap();
assert_eq!(odb.peel_to_commit(bare).unwrap(), Some(c));
}
#[test]
fn unparsable_tag_is_corrupt() {
let dir = TempDir::new().unwrap();
let odb = Odb::new(dir.path());
let bad = odb.write(ObjectKind::Tag, b"not a tag\n").unwrap();
assert!(matches!(odb.peel_tags(bad), Err(Error::CorruptObject(_))));
}
#[test]
fn long_chains_peel_without_a_depth_limit() {
let dir = TempDir::new().unwrap();
let odb = Odb::new(dir.path());
let c = commit(&odb);
let mut tip = tag(&odb, c, "commit", "t0");
for i in 1..200 {
tip = tag(&odb, tip, "tag", &format!("t{i}"));
}
assert_eq!(odb.peel_to_commit(tip).unwrap(), Some(c));
}
#[test]
fn a_store_serving_a_tag_cycle_is_corrupt() {
let store = Arc::new(MemoryStore::new(HashAlgo::Sha1));
let a = ObjectId::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
let b = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
for (from, to) in [(a, b), (b, a)] {
let body = format!("object {to}\ntype tag\ntag cycle\n\n");
store.insert_object(from, ObjectKind::Tag, Arc::from(body.into_bytes()));
}
let dir = TempDir::new().unwrap();
let odb = OdbBuilder::files(dir.path())
.primary(store)
.alternates(false)
.build();
assert!(matches!(odb.peel_tags(a), Err(Error::CorruptObject(_))));
}
}