Skip to main content

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