git_commit/
author.rs

1use std::{
2    fmt,
3    num::ParseIntError,
4    str::{self, FromStr},
5};
6
7use thiserror::Error;
8
9/// The data for indicating authorship of an action within a
10/// [`super::Commit`].
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct Author {
13    /// Name corresponding to `user.name` in the git config.
14    ///
15    /// Note: this must not contain `<` or `>`.
16    pub name: String,
17    /// Email corresponding to `user.email` in the git config.
18    ///
19    /// Note: this must not contain `<` or `>`.
20    pub email: String,
21    /// The time of this author's action.
22    pub time: Time,
23}
24
25/// The time of a [`Author`]'s action.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27pub struct Time {
28    seconds: i64,
29    offset: i32,
30}
31
32impl Time {
33    pub fn new(seconds: i64, offset: i32) -> Self {
34        Self { seconds, offset }
35    }
36
37    /// Return the time, in seconds, since the epoch.
38    pub fn seconds(&self) -> i64 {
39        self.seconds
40    }
41
42    /// Return the timezone offset, in minutes.
43    pub fn offset(&self) -> i32 {
44        self.offset
45    }
46}
47
48impl From<Time> for git2::Time {
49    fn from(t: Time) -> Self {
50        Self::new(t.seconds, t.offset)
51    }
52}
53
54impl From<git2::Time> for Time {
55    fn from(t: git2::Time) -> Self {
56        Self::new(t.seconds(), t.offset_minutes())
57    }
58}
59
60impl fmt::Display for Time {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        let sign = if self.offset.is_negative() { '-' } else { '+' };
63        write!(f, "{} {}{:0>4}", self.seconds, sign, self.offset.abs())
64    }
65}
66
67impl fmt::Display for Author {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "{} <{}> {}", self.name, self.email, self.time,)
70    }
71}
72
73impl TryFrom<&Author> for git2::Signature<'_> {
74    type Error = git2::Error;
75
76    fn try_from(person: &Author) -> Result<Self, Self::Error> {
77        let time = git2::Time::new(person.time.seconds, person.time.offset);
78        git2::Signature::new(&person.name, &person.email, &time)
79    }
80}
81
82impl<'a> TryFrom<&git2::Signature<'a>> for Author {
83    type Error = str::Utf8Error;
84
85    fn try_from(value: &git2::Signature<'a>) -> Result<Self, Self::Error> {
86        Ok(Self {
87            name: str::from_utf8(value.name_bytes())?.to_string(),
88            email: str::from_utf8(value.email_bytes())?.to_string(),
89            time: value.when().into(),
90        })
91    }
92}
93
94#[derive(Debug, Error)]
95pub enum ParseError {
96    #[error("missing '{0}' while parsing person signature")]
97    Missing(&'static str),
98    #[error("offset was incorrect format while parsing person signature")]
99    Offset(#[source] ParseIntError),
100    #[error("time was incorrect format while parsing person signature")]
101    Time(#[source] ParseIntError),
102    #[error("time offset is expected to be '+'/'-' for a person siganture")]
103    UnknownOffset,
104}
105
106impl FromStr for Author {
107    type Err = ParseError;
108
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        let mut components = s.split(' ');
111        let offset = match components.next_back() {
112            None => return Err(ParseError::Missing("offset")),
113            Some(offset) => offset.parse::<i32>().map_err(ParseError::Offset)?,
114        };
115        let time = match components.next_back() {
116            None => return Err(ParseError::Missing("time")),
117            Some(time) => time.parse::<i64>().map_err(ParseError::Time)?,
118        };
119        let time = Time::new(time, offset);
120
121        let email = components
122            .next_back()
123            .ok_or(ParseError::Missing("email"))?
124            .trim_matches(|c| c == '<' || c == '>')
125            .to_owned();
126        let name = components.collect::<Vec<_>>().join(" ");
127        Ok(Self { name, email, time })
128    }
129}