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::ObjectHash;
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: ObjectHash,
54    pub object_hash: ObjectHash,
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: ObjectHash,
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 = ObjectHash::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: ObjectHash) -> 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<ObjectHash> = 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(ObjectHash::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/ SHA-256 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    /// When using SHA-1, `<object_hash>` is 40 hex chars; when using SHA-256, it is 64 hex chars.
199    fn to_data(&self) -> Result<Vec<u8>, GitError> {
200        let mut data = Vec::new();
201
202        data.extend_from_slice(b"object ");
203        data.extend_from_slice(self.object_hash.to_string().as_bytes());
204        data.extend_from_slice(b"\n");
205
206        data.extend_from_slice(b"type ");
207        data.extend_from_slice(self.object_type.to_string().as_bytes());
208        data.extend_from_slice(b"\n");
209
210        data.extend_from_slice(b"tag ");
211        data.extend_from_slice(self.tag_name.as_bytes());
212        data.extend_from_slice(b"\n");
213
214        data.extend_from_slice(&self.tagger.to_data()?);
215        data.extend_from_slice(b"\n\n");
216
217        data.extend_from_slice(self.message.as_bytes());
218
219        Ok(data)
220    }
221}