git_internal/internal/object/
signature.rs

1//! In a Git commit, the author signature contains the name, email address, timestamp, and timezone
2//! of the person who authored the commit. This information is stored in a specific format, which
3//! consists of the following fields:
4//!
5//! - Name: The name of the author, encoded as a UTF-8 string.
6//! - Email: The email address of the author, encoded as a UTF-8 string.
7//! - Timestamp: The timestamp of when the commit was authored, encoded as a decimal number of seconds
8//!   since the Unix epoch (January 1, 1970, 00:00:00 UTC).
9//! - Timezone: The timezone offset of the author's local time from Coordinated Universal Time (UTC),
10//!   encoded as a string in the format "+HHMM" or "-HHMM".
11//!
12use std::{fmt::Display, str::FromStr};
13
14use bincode::{Decode, Encode};
15use bstr::ByteSlice;
16use chrono::Offset;
17use serde::{Deserialize, Serialize};
18
19use crate::errors::GitError;
20
21/// In addition to the author signature, Git also includes a "committer" signature, which indicates
22/// who committed the changes to the repository. The committer signature is similar in structure to
23/// the author signature, but includes the name, email address, and timestamp of the committer instead.
24/// This can be useful in situations where multiple people are working on a project and changes are
25/// being reviewed and merged by someone other than the original author.
26///
27/// In the following example, it's has the only one who authored and committed.
28/// ```bash
29/// author Eli Ma <eli@patch.sh> 1678102132 +0800
30/// committer Quanyi Ma <eli@patch.sh> 1678102132 +0800
31/// ```
32///
33/// So, we design a `SignatureType` enum to indicate the signature type.
34#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Decode, Encode)]
35pub enum SignatureType {
36    Author,
37    Committer,
38    Tagger,
39}
40
41impl Display for SignatureType {
42    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
43        match self {
44            SignatureType::Author => write!(f, "author"),
45            SignatureType::Committer => write!(f, "committer"),
46            SignatureType::Tagger => write!(f, "tagger"),
47        }
48    }
49}
50impl FromStr for SignatureType {
51    type Err = GitError;
52    /// The `from_str` method is used to convert a string to a `SignatureType` enum.
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        match s {
55            "author" => Ok(SignatureType::Author),
56            "committer" => Ok(SignatureType::Committer),
57            "tagger" => Ok(SignatureType::Tagger),
58            _ => Err(GitError::InvalidSignatureType(s.to_string())),
59        }
60    }
61}
62impl SignatureType {
63    /// The `from_data` method is used to convert a `Vec<u8>` to a `SignatureType` enum.
64    pub fn from_data(data: Vec<u8>) -> Result<Self, GitError> {
65        let s = String::from_utf8(data.to_vec())
66            .map_err(|e| GitError::ConversionError(e.to_string()))?;
67        SignatureType::from_str(s.as_str())
68    }
69
70    /// The `to_bytes` method is used to convert a `SignatureType` enum to a `Vec<u8>`.
71    pub fn to_bytes(&self) -> Vec<u8> {
72        match self {
73            SignatureType::Author => "author".to_string().into_bytes(),
74            SignatureType::Committer => "committer".to_string().into_bytes(),
75            SignatureType::Tagger => "tagger".to_string().into_bytes(),
76        }
77    }
78}
79
80#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Decode, Encode)]
81pub struct Signature {
82    pub signature_type: SignatureType,
83    pub name: String,
84    pub email: String,
85    pub timestamp: usize,
86    pub timezone: String,
87}
88
89impl Display for Signature {
90    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
91        writeln!(f, "{} <{}>", self.name, self.email).unwrap();
92        // format the timestamp to a human-readable date format
93        let date =
94            chrono::DateTime::<chrono::Utc>::from_timestamp(self.timestamp as i64, 0).unwrap();
95        writeln!(f, "Date: {} {}", date, self.timezone)
96    }
97}
98
99impl Signature {
100    pub fn from_data(data: Vec<u8>) -> Result<Signature, GitError> {
101        // Make a mutable copy of the input data vector.
102        let mut sign = data;
103
104        // Find the index of the first space byte in the data vector.
105        let name_start = sign.find_byte(0x20).unwrap();
106
107        // Parse the author name from the bytes up to the first space byte.
108        // If the parsing fails, unwrap will panic.
109        let signature_type = SignatureType::from_data(sign[..name_start].to_vec())?;
110
111        let (name, email) = {
112            let email_start = sign.find_byte(0x3C).unwrap();
113            let email_end = sign.find_byte(0x3E).unwrap();
114
115            unsafe {
116                (
117                    sign[name_start + 1..email_start - 1]
118                        .to_str_unchecked()
119                        .to_string(),
120                    sign[email_start + 1..email_end]
121                        .to_str_unchecked()
122                        .to_string(),
123                )
124            }
125        };
126
127        // Update the data vector to remove the author and email bytes.
128        sign = sign[sign.find_byte(0x3E).unwrap() + 2..].to_vec();
129
130        // Find the index of the second space byte in the updated data vector.
131        let timestamp_split = sign.find_byte(0x20).unwrap();
132
133        // Parse the timestamp integer from the bytes up to the second space byte.
134        // If the parsing fails, unwrap will panic.
135        let timestamp = unsafe {
136            sign[0..timestamp_split]
137                .to_str_unchecked()
138                .parse::<usize>()
139                .unwrap()
140        };
141
142        // Parse the timezone string from the bytes after the second space byte.
143        // If the parsing fails, unwrap will panic.
144        let timezone = unsafe { sign[timestamp_split + 1..].to_str_unchecked().to_string() };
145
146        // Return a Result object indicating success
147        Ok(Signature {
148            signature_type,
149            name,
150            email,
151            timestamp,
152            timezone,
153        })
154    }
155
156    pub fn to_data(&self) -> Result<Vec<u8>, GitError> {
157        // Create a new empty vector to store the encoded data.
158        let mut sign = Vec::new();
159
160        // Append the author name bytes to the data vector, followed by a space byte.
161        sign.extend_from_slice(&self.signature_type.to_bytes());
162        sign.extend_from_slice(&[0x20]);
163
164        // Append the name bytes to the data vector, followed by a space byte.
165        sign.extend_from_slice(self.name.as_bytes());
166        sign.extend_from_slice(&[0x20]);
167
168        // Append the email address bytes to the data vector, enclosed in angle brackets.
169        sign.extend_from_slice(format!("<{}>", self.email).as_bytes());
170        sign.extend_from_slice(&[0x20]);
171
172        // Append the timestamp integer bytes to the data vector, followed by a space byte.
173        sign.extend_from_slice(self.timestamp.to_string().as_bytes());
174        sign.extend_from_slice(&[0x20]);
175
176        // Append the timezone string bytes to the data vector.
177        sign.extend_from_slice(self.timezone.as_bytes());
178
179        // Return the data vector as a Result object indicating success.
180        Ok(sign)
181    }
182
183    /// Represents a signature with author, email, timestamp, and timezone information.
184    pub fn new(sign_type: SignatureType, author: String, email: String) -> Signature {
185        // Get the current local time (with timezone)
186        let local_time = chrono::Local::now();
187
188        // Get the offset from UTC in minutes (local time - UTC time)
189        let offset = local_time.offset().fix().local_minus_utc();
190
191        // Calculate the hours part of the offset (divide by 3600 to convert from seconds to hours)
192        let hours = offset / 60 / 60;
193
194        // Calculate the minutes part of the offset (remaining minutes after dividing by 60)
195        let minutes = offset / 60 % 60;
196
197        // Format the offset as a string (e.g., "+0800", "-0300", etc.)
198        let offset_str = format!("{hours:+03}{minutes:02}");
199
200        // Return the Signature struct with the provided information
201        Signature {
202            signature_type: sign_type, // The type of signature (e.g., commit, merge)
203            name: author,              // The author's name
204            email,                     // The author's email
205            timestamp: chrono::Utc::now().timestamp() as usize, // The timestamp of the signature (seconds since Unix epoch)
206            timezone: offset_str, // The timezone offset (e.g., "+0800")
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use std::str::FromStr;
214
215    use chrono::DateTime;
216
217    use crate::internal::object::signature::{Signature, SignatureType};
218
219    #[test]
220    fn test_signature_type_from_str() {
221        assert_eq!(
222            SignatureType::from_str("author").unwrap(),
223            SignatureType::Author
224        );
225
226        assert_eq!(
227            SignatureType::from_str("committer").unwrap(),
228            SignatureType::Committer
229        );
230    }
231
232    #[test]
233    fn test_signature_type_from_data() {
234        assert_eq!(
235            SignatureType::from_data("author".to_string().into_bytes()).unwrap(),
236            SignatureType::Author
237        );
238
239        assert_eq!(
240            SignatureType::from_data("committer".to_string().into_bytes()).unwrap(),
241            SignatureType::Committer
242        );
243    }
244
245    #[test]
246    fn test_signature_type_to_bytes() {
247        assert_eq!(
248            SignatureType::Author.to_bytes(),
249            "author".to_string().into_bytes()
250        );
251
252        assert_eq!(
253            SignatureType::Committer.to_bytes(),
254            "committer".to_string().into_bytes()
255        );
256    }
257
258    #[test]
259    fn test_signature_new_from_data() {
260        let sign = Signature::from_data(
261            "author Quanyi Ma <eli@patch.sh> 1678101573 +0800"
262                .to_string()
263                .into_bytes(),
264        )
265        .unwrap();
266
267        assert_eq!(sign.signature_type, SignatureType::Author);
268        assert_eq!(sign.name, "Quanyi Ma");
269        assert_eq!(sign.email, "eli@patch.sh");
270        assert_eq!(sign.timestamp, 1678101573);
271        assert_eq!(sign.timezone, "+0800");
272    }
273
274    #[test]
275    fn test_signature_to_data() {
276        let sign = Signature::from_data(
277            "committer Quanyi Ma <eli@patch.sh> 1678101573 +0800"
278                .to_string()
279                .into_bytes(),
280        )
281        .unwrap();
282
283        let dest = sign.to_data().unwrap();
284
285        assert_eq!(
286            dest,
287            "committer Quanyi Ma <eli@patch.sh> 1678101573 +0800"
288                .to_string()
289                .into_bytes()
290        );
291    }
292
293    /// When the test case run in the GitHub Action, the timezone is +0000, so we ignore it.
294    #[test]
295    fn test_signature_with_time() {
296        let sign = Signature::new(
297            SignatureType::Author,
298            "MEGA".to_owned(),
299            "admin@mega.com".to_owned(),
300        );
301        assert_eq!(sign.signature_type, SignatureType::Author);
302        assert_eq!(sign.name, "MEGA");
303        assert_eq!(sign.email, "admin@mega.com");
304        // assert_eq!(sign.timezone, "+0800");//it depends on the local timezone
305
306        let naive_datetime = DateTime::from_timestamp(sign.timestamp as i64, 0).unwrap();
307        println!("Formatted DateTime: {}", naive_datetime.naive_local());
308    }
309}