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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
12pub enum Tag {
13 Light {
15 id: Oid,
17 name: RefString,
19 },
20 Annotated {
22 id: Oid,
24 target: Oid,
26 name: RefString,
28 tagger: Option<Author>,
30 message: Option<String>,
32 },
33}
34
35impl Tag {
36 pub fn id(&self) -> Oid {
38 match self {
39 Self::Light { id, .. } => *id,
40 Self::Annotated { id, .. } => *id,
41 }
42 }
43
44 pub fn short_name(&self) -> &RefString {
47 match &self {
48 Tag::Light { name, .. } => name,
49 Tag::Annotated { name, .. } => name,
50 }
51 }
52
53 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 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}