radicle_surf/
tag.rs

1use std::{convert::TryFrom, str};
2
3use git_ext::{
4    ref_format::{component, lit, Qualified, RefStr, RefString},
5    Oid,
6};
7
8use crate::{refs::refstr_join, Author};
9
10/// The metadata of a [`Git tag`][git-tag].
11///
12/// [git-tag]: https://git-scm.com/book/en/v2/Git-Basics-Tagging
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
14pub enum Tag {
15    /// A light-weight git tag.
16    Light {
17        /// The Object ID for the `Tag`, i.e the SHA1 digest.
18        id: Oid,
19        /// The reference name for this `Tag`.
20        name: RefString,
21    },
22    /// An annotated git tag.
23    Annotated {
24        /// The Object ID for the `Tag`, i.e the SHA1 digest.
25        id: Oid,
26        /// The Object ID for the object that is tagged.
27        target: Oid,
28        /// The reference name for this `Tag`.
29        name: RefString,
30        /// The named author of this `Tag`, if the `Tag` was annotated.
31        tagger: Option<Author>,
32        /// The message with this `Tag`, if the `Tag` was annotated.
33        message: Option<String>,
34    },
35}
36
37impl Tag {
38    /// Get the `Oid` of the tag, regardless of its type.
39    pub fn id(&self) -> Oid {
40        match self {
41            Self::Light { id, .. } => *id,
42            Self::Annotated { id, .. } => *id,
43        }
44    }
45
46    /// Return the short `Tag` refname,
47    /// e.g. `release/v1`.
48    pub fn short_name(&self) -> &RefString {
49        match &self {
50            Tag::Light { name, .. } => name,
51            Tag::Annotated { name, .. } => name,
52        }
53    }
54
55    /// Return the fully qualified `Tag` refname,
56    /// e.g. `refs/tags/release/v1`.
57    pub fn refname(&self) -> Qualified {
58        lit::refs_tags(self.short_name()).into()
59    }
60}
61
62pub mod error {
63    use std::str;
64
65    use radicle_git_ext::ref_format::{self, RefString};
66    use thiserror::Error;
67
68    #[derive(Debug, Error)]
69    pub enum FromTag {
70        #[error(transparent)]
71        RefFormat(#[from] ref_format::Error),
72        #[error(transparent)]
73        Utf8(#[from] str::Utf8Error),
74    }
75
76    #[derive(Debug, Error)]
77    pub enum FromReference {
78        #[error(transparent)]
79        FromTag(#[from] FromTag),
80        #[error(transparent)]
81        Git(#[from] git2::Error),
82        #[error("the refname '{0}' did not begin with 'refs/tags'")]
83        NotQualified(String),
84        #[error("the refname '{0}' did not begin with 'refs/tags'")]
85        NotTag(RefString),
86        #[error(transparent)]
87        RefFormat(#[from] ref_format::Error),
88        #[error(transparent)]
89        Utf8(#[from] str::Utf8Error),
90    }
91}
92
93impl TryFrom<&git2::Tag<'_>> for Tag {
94    type Error = error::FromTag;
95
96    fn try_from(tag: &git2::Tag) -> Result<Self, Self::Error> {
97        let id = tag.id().into();
98        let target = tag.target_id().into();
99        let name = {
100            let name = str::from_utf8(tag.name_bytes())?;
101            RefStr::try_from_str(name)?.to_ref_string()
102        };
103        let tagger = tag.tagger().map(Author::try_from).transpose()?;
104        let message = tag
105            .message_bytes()
106            .map(str::from_utf8)
107            .transpose()?
108            .map(|message| message.into());
109
110        Ok(Tag::Annotated {
111            id,
112            target,
113            name,
114            tagger,
115            message,
116        })
117    }
118}
119
120impl TryFrom<&git2::Reference<'_>> for Tag {
121    type Error = error::FromReference;
122
123    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
124        let name = reference_name(reference)?;
125        match reference.peel_to_tag() {
126            Ok(tag) => Tag::try_from(&tag).map_err(error::FromReference::from),
127            // If we get an error peeling to a tag _BUT_ we also have confirmed the
128            // reference is a tag, that means we have a lightweight tag,
129            // i.e. a commit SHA and name.
130            Err(err)
131                if err.class() == git2::ErrorClass::Object
132                    && err.code() == git2::ErrorCode::InvalidSpec =>
133            {
134                let commit = reference.peel_to_commit()?;
135                Ok(Tag::Light {
136                    id: commit.id().into(),
137                    name,
138                })
139            }
140            Err(err) => Err(err.into()),
141        }
142    }
143}
144
145pub(crate) fn reference_name(
146    reference: &git2::Reference,
147) -> Result<RefString, error::FromReference> {
148    let name = str::from_utf8(reference.name_bytes())?;
149    let name = RefStr::try_from_str(name)?
150        .qualified()
151        .ok_or_else(|| error::FromReference::NotQualified(name.to_string()))?;
152
153    let (_refs, tags, c, cs) = name.non_empty_components();
154
155    if tags == component::TAGS {
156        Ok(refstr_join(c, cs))
157    } else {
158        Err(error::FromReference::NotTag(name.into()))
159    }
160}