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#[derive(Accessors, Clone)]
23pub struct Tag {
24 #[access(get(cp))]
26 id: ObjectId,
27
28 #[access(get(ty(&[u8])))]
30 body: Vec<u8>,
31
32 #[access(get(cp))]
34 target: ObjectId,
35
36 #[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 #[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 pub fn name(&self) -> &[u8] {
73 &self.body[self.name.clone()]
74 }
75
76 pub fn tagger_name(&self) -> Option<&[u8]> {
78 self.tagger_name
79 .as_ref()
80 .map(|range| &self.body[range.clone()])
81 }
82
83 pub fn tagger_email(&self) -> Option<&[u8]> {
85 self.tagger_email
86 .as_ref()
87 .map(|range| &self.body[range.clone()])
88 }
89
90 pub fn message(&self) -> &[u8] {
92 &self.body[self.message.clone()]
93 }
94
95 pub fn additional_headers(&self) -> ObjectHeaderIter<'_> {
99 ObjectHeaderIter::new(self.body.as_slice(), self.additional_headers.as_slice())
100 }
101
102 pub fn as_object(self) -> Object {
104 Object::Tag(self)
105 }
106
107 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}