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