git_internal/internal/object/tag.rs
1//! In Git objects there are two types of tags: Lightweight tags and annotated tags.
2//!
3//! A lightweight tag is simply a pointer to a specific commit in Git's version history,
4//! without any additional metadata or information associated with it. It is created by
5//! running the `git tag` command with a name for the tag and the commit hash that it points to.
6//!
7//! An annotated tag, on the other hand, is a Git object in its own right, and includes
8//! metadata such as the tagger's name and email address, the date and time the tag was created,
9//! and a message describing the tag. It is created by running the `git tag -a` command with
10//! a name for the tag, the commit hash that it points to, and the additional metadata that
11//! should be associated with the tag.
12//!
13//! When you create a tag in Git, whether it's a lightweight or annotated tag, Git creates a
14//! new object in its object database to represent the tag. This object includes the name of the
15//! tag, the hash of the commit it points to, and any additional metadata associated with the
16//! tag (in the case of an annotated tag).
17//!
18//! There is no difference in binary format between lightweight tags and annotated tags in Git,
19//! as both are represented using the same lightweight object format in Git's object database.
20//!
21//! The lightweight tag is a reference to a specific commit in Git's version history, not be stored
22//! as a separate object in Git's object database. This means that if you create a lightweight tag
23//! and then move the tag to a different commit, the tag will still point to the original commit.
24//!
25//! The lightweight just a text file with the commit hash in it, and the file name is the tag name.
26//! If one of -a, -s, or -u \<key-id\> is passed, the command creates a tag object, and requires a tag
27//! message. Unless -m \<msg\> or -F \<file\> is given, an editor is started for the user to type in the
28//! tag message.
29//!
30//! ```bash
31//! 4b00093bee9b3ef5afc5f8e3645dc39cfa2f49aa
32//! ```
33//!
34//! The annotated tag is a Git object in its own right, and includes metadata such as the tagger's
35//! name and email address, the date and time the tag was created, and a message describing the tag.
36//!
37//! So, we can use the `git cat-file -p <tag>` command to get the tag object, and the command not
38//! for the lightweight tag.
39use std::fmt::Display;
40use std::str::FromStr;
41
42use bstr::ByteSlice;
43
44use crate::errors::GitError;
45use crate::hash::SHA1;
46use crate::internal::object::ObjectTrait;
47use crate::internal::object::ObjectType;
48use crate::internal::object::signature::Signature;
49
50/// The tag object is used to Annotated tag
51#[derive(Eq, Debug, Clone)]
52pub struct Tag {
53 pub id: SHA1,
54 pub object_hash: SHA1,
55 pub object_type: ObjectType,
56 pub tag_name: String,
57 pub tagger: Signature,
58 pub message: String,
59}
60
61impl PartialEq for Tag {
62 fn eq(&self, other: &Self) -> bool {
63 self.id == other.id
64 }
65}
66
67impl Display for Tag {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(
70 f,
71 "object {}\ntype {}\ntag {}\ntagger {}\n\n{}",
72 self.object_hash, self.object_type, self.tag_name, self.tagger, self.message
73 )
74 }
75}
76
77impl Tag {
78 // pub fn new_from_meta(meta: Meta) -> Result<Tag, GitError> {
79 // Ok(Tag::new_from_data(meta.data))
80 // }
81
82 // pub fn new_from_file(path: &str) -> Result<Tag, GitError> {
83 // let meta = Meta::new_from_file(path)?;
84 // Tag::new_from_meta(meta)
85 // }
86
87 pub fn new(
88 object_hash: SHA1,
89 object_type: ObjectType,
90 tag_name: String,
91 tagger: Signature,
92 message: String,
93 ) -> Self {
94 // Serialize the tag data to calculate its hash
95 let data = format!(
96 "object {}\ntype {}\ntag {}\ntagger {}\n\n{}",
97 object_hash, object_type, tag_name, tagger, message
98 );
99 let id = SHA1::from_type_and_data(ObjectType::Tag, data.as_bytes());
100
101 Self {
102 id,
103 object_hash,
104 object_type,
105 tag_name,
106 tagger,
107 message,
108 }
109 }
110}
111
112impl ObjectTrait for Tag {
113 /// The tag object is used to Annotated tag, it's binary format is:
114 ///
115 /// ```bash
116 /// object <object_hash> 0x0a # The SHA-1 hash of the object that the annotated tag is attached to (usually a commit)
117 /// type <object_type> 0x0a #The type of Git object that the annotated tag is attached to (usually 'commit')
118 /// tag <tag_name> 0x0a # The name of the annotated tag(in UTF-8 encoding)
119 /// tagger <tagger> 0x0a # The name, email address, and date of the person who created the annotated tag
120 /// <message>
121 /// ```
122 fn from_bytes(row_data: &[u8], hash: SHA1) -> Result<Self, GitError>
123 where
124 Self: Sized,
125 {
126 let mut headers = row_data;
127 let mut message_start = 0;
128
129 if let Some(pos) = headers.find(b"\n\n") {
130 message_start = pos + 2;
131 headers = &headers[..pos];
132 }
133
134 let mut object_hash: Option<SHA1> = None;
135 let mut object_type: Option<ObjectType> = None;
136 let mut tag_name: Option<String> = None;
137 let mut tagger: Option<Signature> = None;
138
139 for line in headers.lines() {
140 if let Some(s) = line.strip_prefix(b"object ") {
141 let hash_str = s.to_str().map_err(|_| {
142 GitError::InvalidTagObject("Invalid UTF-8 in object hash".to_string())
143 })?;
144 object_hash = Some(SHA1::from_str(hash_str).map_err(|_| {
145 GitError::InvalidTagObject("Invalid object hash format".to_string())
146 })?);
147 } else if let Some(s) = line.strip_prefix(b"type ") {
148 let type_str = s.to_str().map_err(|_| {
149 GitError::InvalidTagObject("Invalid UTF-8 in object type".to_string())
150 })?;
151 object_type = Some(ObjectType::from_string(type_str)?);
152 } else if let Some(s) = line.strip_prefix(b"tag ") {
153 let tag_str = s.to_str().map_err(|_| {
154 GitError::InvalidTagObject("Invalid UTF-8 in tag name".to_string())
155 })?;
156 tag_name = Some(tag_str.to_string());
157 } else if line.starts_with(b"tagger ") {
158 tagger = Some(Signature::from_data(line.to_vec())?);
159 }
160 }
161
162 let message = if message_start > 0 {
163 String::from_utf8_lossy(&row_data[message_start..]).to_string()
164 } else {
165 String::new()
166 };
167
168 Ok(Tag {
169 id: hash,
170 object_hash: object_hash
171 .ok_or_else(|| GitError::InvalidTagObject("Missing object hash".to_string()))?,
172 object_type: object_type
173 .ok_or_else(|| GitError::InvalidTagObject("Missing object type".to_string()))?,
174 tag_name: tag_name
175 .ok_or_else(|| GitError::InvalidTagObject("Missing tag name".to_string()))?,
176 tagger: tagger
177 .ok_or_else(|| GitError::InvalidTagObject("Missing tagger".to_string()))?,
178 message,
179 })
180 }
181
182 fn get_type(&self) -> ObjectType {
183 ObjectType::Tag
184 }
185
186 fn get_size(&self) -> usize {
187 self.to_data().map(|data| data.len()).unwrap_or(0)
188 }
189
190 ///
191 /// ```bash
192 /// object <object_hash> 0x0a # The SHA-1 hash of the object that the annotated tag is attached to (usually a commit)
193 /// type <object_type> 0x0a #The type of Git object that the annotated tag is attached to (usually 'commit')
194 /// tag <tag_name> 0x0a # The name of the annotated tag(in UTF-8 encoding)
195 /// tagger <tagger> 0x0a # The name, email address, and date of the person who created the annotated tag
196 /// <message>
197 /// ```
198 fn to_data(&self) -> Result<Vec<u8>, GitError> {
199 let mut data = Vec::new();
200
201 data.extend_from_slice(b"object ");
202 data.extend_from_slice(self.object_hash.to_string().as_bytes());
203 data.extend_from_slice(b"\n");
204
205 data.extend_from_slice(b"type ");
206 data.extend_from_slice(self.object_type.to_string().as_bytes());
207 data.extend_from_slice(b"\n");
208
209 data.extend_from_slice(b"tag ");
210 data.extend_from_slice(self.tag_name.as_bytes());
211 data.extend_from_slice(b"\n");
212
213 data.extend_from_slice(&self.tagger.to_data()?);
214 data.extend_from_slice(b"\n\n");
215
216 data.extend_from_slice(self.message.as_bytes());
217
218 Ok(data)
219 }
220}