Skip to main content

radicle_surf/
tag.rs

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