1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
use std::{
    fmt,
    num::ParseIntError,
    str::{self, FromStr},
};

use thiserror::Error;

/// The data for indicating authorship of an action within a
/// [`crate::commit::Commit`].
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Author {
    /// Name corresponding to `user.name` in the git config.
    ///
    /// Note: this must not contain `<` or `>`.
    pub name: String,
    /// Email corresponding to `user.email` in the git config.
    ///
    /// Note: this must not contain `<` or `>`.
    pub email: String,
    /// The time of this author's action.
    pub time: Time,
}

/// The time of a [`Author`]'s action.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Time {
    seconds: i64,
    offset: i32,
}

impl Time {
    pub fn new(seconds: i64, offset: i32) -> Self {
        Self { seconds, offset }
    }

    /// Return the time, in seconds, since the epoch.
    pub fn seconds(&self) -> i64 {
        self.seconds
    }

    /// Return the timezone offset, in minutes.
    pub fn offset(&self) -> i32 {
        self.offset
    }
}

impl From<Time> for git2::Time {
    fn from(t: Time) -> Self {
        Self::new(t.seconds, t.offset)
    }
}

impl From<git2::Time> for Time {
    fn from(t: git2::Time) -> Self {
        Self::new(t.seconds(), t.offset_minutes())
    }
}

impl fmt::Display for Time {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let sign = if self.offset.is_negative() { '-' } else { '+' };
        let hours = self.offset.abs() / 60;
        let minutes = self.offset.abs() % 60;
        write!(f, "{} {}{:0>2}{:0>2}", self.seconds, sign, hours, minutes)
    }
}

impl fmt::Display for Author {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} <{}> {}", self.name, self.email, self.time,)
    }
}

impl TryFrom<&Author> for git2::Signature<'_> {
    type Error = git2::Error;

    fn try_from(person: &Author) -> Result<Self, Self::Error> {
        let time = git2::Time::new(person.time.seconds, person.time.offset);
        git2::Signature::new(&person.name, &person.email, &time)
    }
}

impl<'a> TryFrom<&git2::Signature<'a>> for Author {
    type Error = str::Utf8Error;

    fn try_from(value: &git2::Signature<'a>) -> Result<Self, Self::Error> {
        Ok(Self {
            name: str::from_utf8(value.name_bytes())?.to_string(),
            email: str::from_utf8(value.email_bytes())?.to_string(),
            time: value.when().into(),
        })
    }
}

#[derive(Debug, Error)]
pub enum ParseError {
    #[error("missing '{0}' while parsing person signature")]
    Missing(&'static str),
    #[error("offset was incorrect format while parsing person signature")]
    Offset(#[source] ParseIntError),
    #[error("time was incorrect format while parsing person signature")]
    Time(#[source] ParseIntError),
    #[error("time offset is expected to be '+'/'-' for a person siganture")]
    UnknownOffset,
}

impl FromStr for Author {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut components = s.split(' ');
        let offset = match components.next_back() {
            None => return Err(ParseError::Missing("offset")),
            Some(offset) => {
                // The offset is in the form of timezone offset,
                // e.g. +0200, -0100.  This needs to be converted into
                // minutes. The first two digits in the offset are the
                // number of hours in the offset, while the latter two
                // digits are the number of minutes in the offset.
                let tz_offset = offset.parse::<i32>().map_err(ParseError::Offset)?;
                let hours = tz_offset / 100;
                let minutes = tz_offset % 100;
                hours * 60 + minutes
            },
        };
        let time = match components.next_back() {
            None => return Err(ParseError::Missing("time")),
            Some(time) => time.parse::<i64>().map_err(ParseError::Time)?,
        };
        let time = Time::new(time, offset);

        let email = components
            .next_back()
            .ok_or(ParseError::Missing("email"))?
            .trim_matches(|c| c == '<' || c == '>')
            .to_owned();
        let name = components.collect::<Vec<_>>().join(" ");
        Ok(Self { name, email, time })
    }
}