git_internal/internal/object/
tag.rs1use std::{fmt::Display, str::FromStr};
40
41use bstr::ByteSlice;
42
43use crate::{
44 errors::GitError,
45 hash::ObjectHash,
46 internal::object::{ObjectTrait, ObjectType, signature::Signature},
47};
48
49#[derive(Eq, Debug, Clone)]
51pub struct Tag {
52 pub id: ObjectHash,
53 pub object_hash: ObjectHash,
54 pub object_type: ObjectType,
55 pub tag_name: String,
56 pub tagger: Signature,
57 pub message: String,
58}
59
60impl PartialEq for Tag {
61 fn eq(&self, other: &Self) -> bool {
62 self.id == other.id
63 }
64}
65
66impl Display for Tag {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 write!(
69 f,
70 "object {}\ntype {}\ntag {}\ntagger {}\n\n{}",
71 self.object_hash, self.object_type, self.tag_name, self.tagger, self.message
72 )
73 }
74}
75
76impl Tag {
77 pub fn new(
87 object_hash: ObjectHash,
88 object_type: ObjectType,
89 tag_name: String,
90 tagger: Signature,
91 message: String,
92 ) -> Self {
93 let data = format!(
95 "object {object_hash}\ntype {object_type}\ntag {tag_name}\ntagger {tagger}\n\n{message}"
96 );
97 let id = ObjectHash::from_type_and_data(ObjectType::Tag, data.as_bytes());
98
99 Self {
100 id,
101 object_hash,
102 object_type,
103 tag_name,
104 tagger,
105 message,
106 }
107 }
108}
109
110impl ObjectTrait for Tag {
111 fn from_bytes(row_data: &[u8], hash: ObjectHash) -> Result<Self, GitError>
121 where
122 Self: Sized,
123 {
124 let mut headers = row_data;
125 let mut message_start = 0;
126
127 if let Some(pos) = headers.find(b"\n\n") {
128 message_start = pos + 2;
129 headers = &headers[..pos];
130 }
131
132 let mut object_hash: Option<ObjectHash> = None;
133 let mut object_type: Option<ObjectType> = None;
134 let mut tag_name: Option<String> = None;
135 let mut tagger: Option<Signature> = None;
136
137 for line in headers.lines() {
138 if let Some(s) = line.strip_prefix(b"object ") {
139 let hash_str = s.to_str().map_err(|_| {
140 GitError::InvalidTagObject("Invalid UTF-8 in object hash".to_string())
141 })?;
142 object_hash = Some(ObjectHash::from_str(hash_str).map_err(|_| {
143 GitError::InvalidTagObject("Invalid object hash format".to_string())
144 })?);
145 } else if let Some(s) = line.strip_prefix(b"type ") {
146 let type_str = s.to_str().map_err(|_| {
147 GitError::InvalidTagObject("Invalid UTF-8 in object type".to_string())
148 })?;
149 object_type = Some(ObjectType::from_string(type_str)?);
150 } else if let Some(s) = line.strip_prefix(b"tag ") {
151 let tag_str = s.to_str().map_err(|_| {
152 GitError::InvalidTagObject("Invalid UTF-8 in tag name".to_string())
153 })?;
154 tag_name = Some(tag_str.to_string());
155 } else if line.starts_with(b"tagger ") {
156 tagger = Some(Signature::from_data(line.to_vec())?);
157 }
158 }
159
160 let message = if message_start > 0 {
161 String::from_utf8_lossy(&row_data[message_start..]).to_string()
162 } else {
163 String::new()
164 };
165
166 Ok(Tag {
167 id: hash,
168 object_hash: object_hash
169 .ok_or_else(|| GitError::InvalidTagObject("Missing object hash".to_string()))?,
170 object_type: object_type
171 .ok_or_else(|| GitError::InvalidTagObject("Missing object type".to_string()))?,
172 tag_name: tag_name
173 .ok_or_else(|| GitError::InvalidTagObject("Missing tag name".to_string()))?,
174 tagger: tagger
175 .ok_or_else(|| GitError::InvalidTagObject("Missing tagger".to_string()))?,
176 message,
177 })
178 }
179
180 fn get_type(&self) -> ObjectType {
181 ObjectType::Tag
182 }
183
184 fn get_size(&self) -> usize {
185 self.to_data().map(|data| data.len()).unwrap_or(0)
186 }
187
188 fn to_data(&self) -> Result<Vec<u8>, GitError> {
198 let mut data = Vec::new();
199
200 data.extend_from_slice(b"object ");
201 data.extend_from_slice(self.object_hash.to_string().as_bytes());
202 data.extend_from_slice(b"\n");
203
204 data.extend_from_slice(b"type ");
205 data.extend_from_slice(self.object_type.to_string().as_bytes());
206 data.extend_from_slice(b"\n");
207
208 data.extend_from_slice(b"tag ");
209 data.extend_from_slice(self.tag_name.as_bytes());
210 data.extend_from_slice(b"\n");
211
212 data.extend_from_slice(&self.tagger.to_data()?);
213 data.extend_from_slice(b"\n\n");
214
215 data.extend_from_slice(self.message.as_bytes());
216
217 Ok(data)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::{
225 hash::{HashKind, ObjectHash, set_hash_kind_for_test},
226 internal::object::signature::{Signature, SignatureType},
227 };
228
229 fn make_sig() -> Signature {
231 Signature::new(
232 SignatureType::Tagger,
233 "tagger".to_string(),
234 "tagger@example.com".to_string(),
235 )
236 }
237
238 fn round_trip(kind: HashKind) {
240 let _guard = set_hash_kind_for_test(kind);
241 let target = match kind {
242 HashKind::Sha1 => {
243 ObjectHash::from_str("1234567890abcdef1234567890abcdef12345678").unwrap()
244 }
245 HashKind::Sha256 => ObjectHash::from_str(
246 "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
247 )
248 .unwrap(),
249 };
250 let sig = make_sig();
251 let tag = Tag::new(
252 target,
253 ObjectType::Commit,
254 "v1.0.0".to_string(),
255 sig.clone(),
256 "release".to_string(),
257 );
258
259 let data = tag.to_data().unwrap();
260 let parsed = Tag::from_bytes(&data, tag.id).unwrap();
261
262 assert_eq!(parsed.id, tag.id);
263 assert_eq!(parsed.object_hash, target);
264 assert_eq!(parsed.object_type, ObjectType::Commit);
265 assert_eq!(parsed.tag_name, "v1.0.0");
266 assert_eq!(parsed.message, "release");
267 assert_eq!(parsed.tagger.to_string(), sig.to_string());
268 }
269
270 #[tokio::test]
272 async fn tag_round_trip() {
273 round_trip(HashKind::Sha1);
274 round_trip(HashKind::Sha256);
275 }
276
277 #[test]
279 fn tag_invalid_missing_fields_errors() {
280 let _guard = set_hash_kind_for_test(HashKind::Sha1);
281 let bad = b"type commit\ntag v1.0.0\n\nno object line".to_vec();
282 let hash = ObjectHash::from_str("ffffffffffffffffffffffffffffffffffffffff").unwrap();
283 assert!(Tag::from_bytes(&bad, hash).is_err());
284 }
285}