Skip to main content

git_async/object/
tag.rs

1use crate::{
2    error::GResult,
3    file_system::FileSystem,
4    object::{
5        Object, ObjectId, ObjectType,
6        header::{ObjectHeaderIter, RangeObjectHeader},
7        parse_author_committer_tagger,
8    },
9    parsing::ParseError,
10    repo::Repo,
11    subslice_range::SubsliceRange,
12};
13use accessory::Accessors;
14use alloc::vec::Vec;
15use chrono::{DateTime, FixedOffset};
16use core::ops::Range;
17use nom::{Parser, combinator::all_consuming};
18
19/// A tag object
20///
21/// Git tags can be ref tags or tag objects; this is the latter.
22#[derive(Accessors, Clone)]
23pub struct Tag {
24    /// The [`ObjectId`] of the tag
25    #[access(get(cp))]
26    id: ObjectId,
27
28    /// The raw data in the object
29    #[access(get(ty(&[u8])))]
30    body: Vec<u8>,
31
32    /// The [`ObjectId`] the object pointed to by the tag
33    #[access(get(cp))]
34    target: ObjectId,
35
36    /// The type of the object pointed to by the tag
37    #[allow(clippy::struct_field_names)]
38    #[access(get(cp))]
39    tag_type: ObjectType,
40
41    name: Range<usize>,
42    tagger_name: Option<Range<usize>>,
43    tagger_email: Option<Range<usize>>,
44    message: Range<usize>,
45
46    /// The tag date, if it exists
47    #[access(get(cp))]
48    date: Option<DateTime<FixedOffset>>,
49
50    additional_headers: Vec<RangeObjectHeader>,
51}
52
53impl PartialEq for Tag {
54    fn eq(&self, other: &Self) -> bool {
55        self.id == other.id
56    }
57}
58impl Eq for Tag {}
59impl PartialOrd for Tag {
60    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
61        Some(self.cmp(other))
62    }
63}
64impl Ord for Tag {
65    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
66        self.id.cmp(&other.id)
67    }
68}
69
70impl Tag {
71    /// The name of the tag
72    pub fn name(&self) -> &[u8] {
73        &self.body[self.name.clone()]
74    }
75
76    /// The name of the tagger, if specified
77    pub fn tagger_name(&self) -> Option<&[u8]> {
78        self.tagger_name
79            .as_ref()
80            .map(|range| &self.body[range.clone()])
81    }
82
83    /// The email address of the tagger, if specified
84    pub fn tagger_email(&self) -> Option<&[u8]> {
85        self.tagger_email
86            .as_ref()
87            .map(|range| &self.body[range.clone()])
88    }
89
90    /// The message of the tag
91    pub fn message(&self) -> &[u8] {
92        &self.body[self.message.clone()]
93    }
94
95    /// Get an iterator over any additional headers in the tag object.
96    ///
97    /// Additional headers are those not parsed by `git-async`, e.g. `mergetag`.
98    pub fn additional_headers(&self) -> ObjectHeaderIter<'_> {
99        ObjectHeaderIter::new(self.body.as_slice(), self.additional_headers.as_slice())
100    }
101
102    /// Wrap the [`Tag`] as a generic [`Object`].
103    pub fn as_object(self) -> Object {
104        Object::Tag(self)
105    }
106
107    /// Look up the target object of the tag using the provided [`Repo`].
108    pub async fn lookup_target<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<Object> {
109        repo.lookup_object(self.target).await
110    }
111
112    pub(crate) fn parse(id: ObjectId, body: Vec<u8>) -> Result<Self, ParseError> {
113        fn f<T>(val: Option<T>) -> Result<T, ParseError> {
114            val.ok_or(ParseError::MissingFields)
115        }
116        let (message, raw_headers) = RangeObjectHeader::parser(&body)?;
117        let mut object: Option<ObjectId> = None;
118        let mut tag_type: Option<ObjectType> = None;
119        let mut tag: Option<&[u8]> = None;
120        let mut tagger_name: Option<&[u8]> = None;
121        let mut tagger_email: Option<&[u8]> = None;
122        let mut tag_date: Option<DateTime<FixedOffset>> = None;
123        let mut additional_headers = Vec::new();
124        for (range_header, header) in raw_headers
125            .iter()
126            .zip(ObjectHeaderIter::new(&body, raw_headers.as_slice()))
127        {
128            match header.name() {
129                b"object" => {
130                    let (_, object_id) = all_consuming(ObjectId::parse).parse(header.value())?;
131                    object = Some(object_id);
132                }
133                b"type" => {
134                    tag_type = match header.value() {
135                        b"commit" => Some(ObjectType::Commit),
136                        b"blob" => Some(ObjectType::Blob),
137                        b"tree" => Some(ObjectType::Tree),
138                        b"tag" => Some(ObjectType::Tag),
139                        _ => None,
140                    };
141                }
142                b"tag" => tag = Some(header.value()),
143                b"tagger" => {
144                    let (_, (name, email, date)) =
145                        all_consuming(parse_author_committer_tagger).parse(header.value())?;
146                    tagger_name = Some(name);
147                    tagger_email = Some(email);
148                    tag_date = Some(date);
149                }
150                _ => {
151                    additional_headers.push(range_header.clone());
152                }
153            }
154        }
155        Ok(Tag {
156            id,
157            target: f(object)?,
158            tag_type: f(tag_type)?,
159            name: body.subslice_range_stable(f(tag)?).unwrap(),
160            tagger_name: tagger_name.map(|t| body.subslice_range_stable(t).unwrap()),
161            tagger_email: tagger_email.map(|t| body.subslice_range_stable(t).unwrap()),
162            date: tag_date,
163            message: body.subslice_range_stable(message).unwrap(),
164            additional_headers,
165            body,
166        })
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use hex_literal::hex;
174
175    const ZERO_OID: ObjectId = ObjectId::from_bytes([0; 20]);
176
177    #[test]
178    fn parse_commit_tag() {
179        let data = b"object eedeffb6da16ddc3fb61b2255a8259cacc045691
180type commit
181tag annotated-tag
182tagger a-user <an-email-address> 1774822895 +0100
183
184a message
185";
186        let tag = Tag::parse(ZERO_OID, data.to_vec()).unwrap();
187        assert_eq!(
188            tag.target,
189            ObjectId::from_bytes(hex!("eedeffb6da16ddc3fb61b2255a8259cacc045691"),)
190        );
191        assert_eq!(tag.tag_type, ObjectType::Commit);
192        assert_eq!(tag.name(), b"annotated-tag");
193        assert_eq!(tag.tagger_name(), Some(b"a-user".as_slice()));
194        assert_eq!(tag.tagger_email(), Some(b"an-email-address".as_slice()));
195        assert_eq!(
196            tag.date,
197            Some(DateTime::parse_from_rfc3339("2026-03-29T23:21:35+01:00").unwrap())
198        );
199        assert_eq!(&tag.message(), b"a message\n");
200    }
201
202    #[test]
203    fn parse_blob_tag() {
204        let data = b"object e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
205type blob
206tag blob-tag
207tagger a-user <an-email-address> 1774826002 +0100
208
209a blob
210";
211        let tag = Tag::parse(ZERO_OID, data.to_vec()).unwrap();
212        assert_eq!(tag.tag_type, ObjectType::Blob);
213    }
214
215    #[test]
216    fn parse_tree_tag() {
217        let data = b"object 3a4df67dd7fd7cb3ca82d9896dbdd28053d39bdb
218type tree
219tag tree-tag
220tagger a-user <an-email-address> 1774826187 +0100
221
222a tree
223";
224        let tag = Tag::parse(ZERO_OID, data.to_vec()).unwrap();
225        assert_eq!(tag.tag_type, ObjectType::Tree);
226    }
227
228    #[test]
229    fn parse_nested_tag() {
230        let data = b"object 1c8bf8368bc9b1fd14227c6c1a0b0f30a1812e70
231type tag
232tag tag-tag
233tagger a-user <an-email-address> 1774826312 +0100
234
235a tag
236";
237        let tag = Tag::parse(ZERO_OID, data.to_vec()).unwrap();
238        assert_eq!(tag.tag_type, ObjectType::Tag);
239    }
240}