1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
use std::{convert::TryFrom, str};

use git_ext::{
    ref_format::{component, lit, Qualified, RefStr, RefString},
    Oid,
};

use crate::{refs::refstr_join, Author};

/// The metadata of a [`Git tag`][git-tag].
///
/// [git-tag]: https://git-scm.com/book/en/v2/Git-Basics-Tagging
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Tag {
    /// A light-weight git tag.
    Light {
        /// The Object ID for the `Tag`, i.e the SHA1 digest.
        id: Oid,
        /// The reference name for this `Tag`.
        name: RefString,
    },
    /// An annotated git tag.
    Annotated {
        /// The Object ID for the `Tag`, i.e the SHA1 digest.
        id: Oid,
        /// The Object ID for the object that is tagged.
        target: Oid,
        /// The reference name for this `Tag`.
        name: RefString,
        /// The named author of this `Tag`, if the `Tag` was annotated.
        tagger: Option<Author>,
        /// The message with this `Tag`, if the `Tag` was annotated.
        message: Option<String>,
    },
}

impl Tag {
    /// Get the `Oid` of the tag, regardless of its type.
    pub fn id(&self) -> Oid {
        match self {
            Self::Light { id, .. } => *id,
            Self::Annotated { id, .. } => *id,
        }
    }

    /// Return the short `Tag` refname,
    /// e.g. `release/v1`.
    pub fn short_name(&self) -> &RefString {
        match &self {
            Tag::Light { name, .. } => name,
            Tag::Annotated { name, .. } => name,
        }
    }

    /// Return the fully qualified `Tag` refname,
    /// e.g. `refs/tags/release/v1`.
    pub fn refname(&self) -> Qualified {
        lit::refs_tags(self.short_name()).into()
    }
}

pub mod error {
    use std::str;

    use radicle_git_ext::ref_format::{self, RefString};
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum FromTag {
        #[error(transparent)]
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] str::Utf8Error),
    }

    #[derive(Debug, Error)]
    pub enum FromReference {
        #[error(transparent)]
        FromTag(#[from] FromTag),
        #[error(transparent)]
        Git(#[from] git2::Error),
        #[error("the refname '{0}' did not begin with 'refs/tags'")]
        NotQualified(String),
        #[error("the refname '{0}' did not begin with 'refs/tags'")]
        NotTag(RefString),
        #[error(transparent)]
        RefFormat(#[from] ref_format::Error),
        #[error(transparent)]
        Utf8(#[from] str::Utf8Error),
    }
}

impl TryFrom<&git2::Tag<'_>> for Tag {
    type Error = error::FromTag;

    fn try_from(tag: &git2::Tag) -> Result<Self, Self::Error> {
        let id = tag.id().into();
        let target = tag.target_id().into();
        let name = {
            let name = str::from_utf8(tag.name_bytes())?;
            RefStr::try_from_str(name)?.to_ref_string()
        };
        let tagger = tag.tagger().map(Author::try_from).transpose()?;
        let message = tag
            .message_bytes()
            .map(str::from_utf8)
            .transpose()?
            .map(|message| message.into());

        Ok(Tag::Annotated {
            id,
            target,
            name,
            tagger,
            message,
        })
    }
}

impl TryFrom<&git2::Reference<'_>> for Tag {
    type Error = error::FromReference;

    fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
        let name = reference_name(reference)?;
        match reference.peel_to_tag() {
            Ok(tag) => Tag::try_from(&tag).map_err(error::FromReference::from),
            // If we get an error peeling to a tag _BUT_ we also have confirmed the
            // reference is a tag, that means we have a lightweight tag,
            // i.e. a commit SHA and name.
            Err(err)
                if err.class() == git2::ErrorClass::Object
                    && err.code() == git2::ErrorCode::InvalidSpec =>
            {
                let commit = reference.peel_to_commit()?;
                Ok(Tag::Light {
                    id: commit.id().into(),
                    name,
                })
            }
            Err(err) => Err(err.into()),
        }
    }
}

pub(crate) fn reference_name(
    reference: &git2::Reference,
) -> Result<RefString, error::FromReference> {
    let name = str::from_utf8(reference.name_bytes())?;
    let name = RefStr::try_from_str(name)?
        .qualified()
        .ok_or_else(|| error::FromReference::NotQualified(name.to_string()))?;

    let (_refs, tags, c, cs) = name.non_empty_components();

    if tags == component::TAGS {
        Ok(refstr_join(c, cs))
    } else {
        Err(error::FromReference::NotTag(name.into()))
    }
}