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/// Represents a Git signature, including the author's name, email, timestamp, and timezone.
81#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Decode, Encode)]
82pub struct Signature {
83    pub signature_type: SignatureType,
84    pub name: String,
85    pub email: String,
86    pub timestamp: usize,
87    pub timezone: String,
88}
89
90impl Display for Signature {
91    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
92        writeln!(f, "{} <{}>", self.name, self.email).unwrap();
93        // format the timestamp to a human-readable date format
94        let date =
95            chrono::DateTime::<chrono::Utc>::from_timestamp(self.timestamp as i64, 0).unwrap();
96        writeln!(f, "Date: {} {}", date, self.timezone)
97    }
98}
99
100impl Signature {
101    /// The `from_data` method is used to convert a `Vec<u8>` to a `Signature` struct.
102    pub fn from_data(data: Vec<u8>) -> Result<Signature, GitError> {
103        // Make a mutable copy of the input data vector.
104        let mut sign = data;
105
106        // Find the index of the first space byte in the data vector.
107        let name_start = sign.find_byte(0x20).unwrap();
108
109        // Parse the author name from the bytes up to the first space byte.
110        // If the parsing fails, unwrap will panic.
111        let signature_type = SignatureType::from_data(sign[..name_start].to_vec())?;
112
113        let (name, email) = {
114            let email_start = sign.find_byte(0x3C).unwrap();
115            let email_end = sign.find_byte(0x3E).unwrap();
116
117            unsafe {
118                (
119                    sign[name_start + 1..email_start - 1]
120                        .to_str_unchecked()
121                        .to_string(),
122                    sign[email_start + 1..email_end]
123                        .to_str_unchecked()
124                        .to_string(),
125                )
126            }
127        };
128
129        // Update the data vector to remove the author and email bytes.
130        sign = sign[sign.find_byte(0x3E).unwrap() + 2..].to_vec();
131
132        // Find the index of the second space byte in the updated data vector.
133        let timestamp_split = sign.find_byte(0x20).unwrap();
134
135        // Parse the timestamp integer from the bytes up to the second space byte.
136        // If the parsing fails, unwrap will panic.
137        let timestamp = unsafe {
138            sign[0..timestamp_split]
139                .to_str_unchecked()
140                .parse::<usize>()
141                .unwrap()
142        };
143
144        // Parse the timezone string from the bytes after the second space byte.
145        // If the parsing fails, unwrap will panic.
146        let timezone = unsafe { sign[timestamp_split + 1..].to_str_unchecked().to_string() };
147
148        // Return a Result object indicating success
149        Ok(Signature {
150            signature_type,
151            name,
152            email,
153            timestamp,
154            timezone,
155        })
156    }
157
158    /// The `to_data` method is used to convert a `Signature` struct to a `Vec<u8>`.
159    pub fn to_data(&self) -> Result<Vec<u8>, GitError> {
160        // Create a new empty vector to store the encoded data.
161        let mut sign = Vec::new();
162
163        // Append the author name bytes to the data vector, followed by a space byte.
164        sign.extend_from_slice(&self.signature_type.to_bytes());
165        sign.extend_from_slice(&[0x20]);
166
167        // Append the name bytes to the data vector, followed by a space byte.
168        sign.extend_from_slice(self.name.as_bytes());
169        sign.extend_from_slice(&[0x20]);
170
171        // Append the email address bytes to the data vector, enclosed in angle brackets.
172        sign.extend_from_slice(format!("<{}>", self.email).as_bytes());
173        sign.extend_from_slice(&[0x20]);
174
175        // Append the timestamp integer bytes to the data vector, followed by a space byte.
176        sign.extend_from_slice(self.timestamp.to_string().as_bytes());
177        sign.extend_from_slice(&[0x20]);
178
179        // Append the timezone string bytes to the data vector.
180        sign.extend_from_slice(self.timezone.as_bytes());
181
182        // Return the data vector as a Result object indicating success.
183        Ok(sign)
184    }
185
186    /// Represents a signature with author, email, timestamp, and timezone information.
187    pub fn new(sign_type: SignatureType, author: String, email: String) -> Signature {
188        // Get the current local time (with timezone)
189        let local_time = chrono::Local::now();
190
191        // Get the offset from UTC in minutes (local time - UTC time)
192        let offset = local_time.offset().fix().local_minus_utc();
193
194        // Calculate the hours part of the offset (divide by 3600 to convert from seconds to hours)
195        let hours = offset / 60 / 60;
196
197        // Calculate the minutes part of the offset (remaining minutes after dividing by 60)
198        let minutes = offset / 60 % 60;
199
200        // Format the offset as a string (e.g., "+0800", "-0300", etc.)
201        let offset_str = format!("{hours:+03}{minutes:02}");
202
203        // Return the Signature struct with the provided information
204        Signature {
205            signature_type: sign_type, // The type of signature (e.g., commit, merge)
206            name: author,              // The author's name
207            email,                     // The author's email
208            timestamp: chrono::Utc::now().timestamp() as usize, // The timestamp of the signature (seconds since Unix epoch)
209            timezone: offset_str, // The timezone offset (e.g., "+0800")
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use std::str::FromStr;
217
218    use chrono::DateTime;
219
220    use crate::internal::object::signature::{Signature, SignatureType};
221
222    /// Test conversion from string to SignatureType enum.
223    #[test]
224    fn test_signature_type_from_str() {
225        assert_eq!(
226            SignatureType::from_str("author").unwrap(),
227            SignatureType::Author
228        );
229
230        assert_eq!(
231            SignatureType::from_str("committer").unwrap(),
232            SignatureType::Committer
233        );
234    }
235
236    /// Test conversion from SignatureType enum to string.
237    #[test]
238    fn test_signature_type_from_data() {
239        assert_eq!(
240            SignatureType::from_data("author".to_string().into_bytes()).unwrap(),
241            SignatureType::Author
242        );
243
244        assert_eq!(
245            SignatureType::from_data("committer".to_string().into_bytes()).unwrap(),
246            SignatureType::Committer
247        );
248    }
249
250    /// Test conversion from SignatureType enum to bytes.
251    #[test]
252    fn test_signature_type_to_bytes() {
253        assert_eq!(
254            SignatureType::Author.to_bytes(),
255            "author".to_string().into_bytes()
256        );
257
258        assert_eq!(
259            SignatureType::Committer.to_bytes(),
260            "committer".to_string().into_bytes()
261        );
262    }
263
264    /// Test conversion from data bytes to Signature struct.
265    #[test]
266    fn test_signature_new_from_data() {
267        let sign = Signature::from_data(
268            "author Quanyi Ma <eli@patch.sh> 1678101573 +0800"
269                .to_string()
270                .into_bytes(),
271        )
272        .unwrap();
273
274        assert_eq!(sign.signature_type, SignatureType::Author);
275        assert_eq!(sign.name, "Quanyi Ma");
276        assert_eq!(sign.email, "eli@patch.sh");
277        assert_eq!(sign.timestamp, 1678101573);
278        assert_eq!(sign.timezone, "+0800");
279    }
280
281    /// Test conversion from Signature struct to data bytes.
282    #[test]
283    fn test_signature_to_data() {
284        let sign = Signature::from_data(
285            "committer Quanyi Ma <eli@patch.sh> 1678101573 +0800"
286                .to_string()
287                .into_bytes(),
288        )
289        .unwrap();
290
291        let dest = sign.to_data().unwrap();
292
293        assert_eq!(
294            dest,
295            "committer Quanyi Ma <eli@patch.sh> 1678101573 +0800"
296                .to_string()
297                .into_bytes()
298        );
299    }
300
301    /// When the test case run in the GitHub Action, the timezone is +0000, so we ignore it.
302    #[test]
303    fn test_signature_with_time() {
304        let sign = Signature::new(
305            SignatureType::Author,
306            "MEGA".to_owned(),
307            "admin@mega.com".to_owned(),
308        );
309        assert_eq!(sign.signature_type, SignatureType::Author);
310        assert_eq!(sign.name, "MEGA");
311        assert_eq!(sign.email, "admin@mega.com");
312        // assert_eq!(sign.timezone, "+0800");//it depends on the local timezone
313
314        let naive_datetime = DateTime::from_timestamp(sign.timestamp as i64, 0).unwrap();
315        println!("Formatted DateTime: {}", naive_datetime.naive_local());
316    }
317}