radicle_git_ext/
author.rs1use std::{
2 fmt,
3 num::ParseIntError,
4 str::{self, FromStr},
5};
6
7use thiserror::Error;
8
9#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct Author {
13 pub name: String,
17 pub email: String,
21 pub time: Time,
23}
24
25#[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 pub fn seconds(&self) -> i64 {
39 self.seconds
40 }
41
42 pub fn offset(&self) -> i32 {
44 self.offset
45 }
46
47 fn from_components<'a>(cs: &mut impl Iterator<Item = &'a str>) -> Result<Self, ParseError> {
48 let offset = match cs.next() {
49 None => Err(ParseError::Missing("offset")),
50 Some(offset) => Self::parse_offset(offset).map_err(ParseError::Offset),
51 }?;
52 let time = match cs.next() {
53 None => return Err(ParseError::Missing("time")),
54 Some(time) => time.parse::<i64>().map_err(ParseError::Time)?,
55 };
56 Ok(Self::new(time, offset))
57 }
58
59 fn parse_offset(offset: &str) -> Result<i32, ParseIntError> {
60 let tz_offset = offset.parse::<i32>()?;
66 let hours = tz_offset / 100;
67 let minutes = tz_offset % 100;
68 Ok(hours * 60 + minutes)
69 }
70}
71
72impl FromStr for Time {
73 type Err = ParseError;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 Self::from_components(&mut s.split(' ').rev())
77 }
78}
79
80impl From<Time> for git2::Time {
81 fn from(t: Time) -> Self {
82 Self::new(t.seconds, t.offset)
83 }
84}
85
86impl From<git2::Time> for Time {
87 fn from(t: git2::Time) -> Self {
88 Self::new(t.seconds(), t.offset_minutes())
89 }
90}
91
92impl fmt::Display for Time {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 let sign = if self.offset.is_negative() { '-' } else { '+' };
95 let hours = self.offset.abs() / 60;
96 let minutes = self.offset.abs() % 60;
97 write!(f, "{} {}{:0>2}{:0>2}", self.seconds, sign, hours, minutes)
98 }
99}
100
101impl fmt::Display for Author {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(f, "{} <{}> {}", self.name, self.email, self.time,)
104 }
105}
106
107impl TryFrom<&Author> for git2::Signature<'_> {
108 type Error = git2::Error;
109
110 fn try_from(person: &Author) -> Result<Self, Self::Error> {
111 let time = git2::Time::new(person.time.seconds, person.time.offset);
112 git2::Signature::new(&person.name, &person.email, &time)
113 }
114}
115
116impl<'a> TryFrom<&git2::Signature<'a>> for Author {
117 type Error = str::Utf8Error;
118
119 fn try_from(value: &git2::Signature<'a>) -> Result<Self, Self::Error> {
120 Ok(Self {
121 name: str::from_utf8(value.name_bytes())?.to_string(),
122 email: str::from_utf8(value.email_bytes())?.to_string(),
123 time: value.when().into(),
124 })
125 }
126}
127
128#[derive(Debug, Error)]
129pub enum ParseError {
130 #[error("missing '{0}' while parsing person signature")]
131 Missing(&'static str),
132 #[error("offset was incorrect format while parsing person signature")]
133 Offset(#[source] ParseIntError),
134 #[error("time was incorrect format while parsing person signature")]
135 Time(#[source] ParseIntError),
136}
137
138impl FromStr for Author {
139 type Err = ParseError;
140
141 fn from_str(s: &str) -> Result<Self, Self::Err> {
142 let mut components = s.rsplitn(4, ' ');
145 let time = Time::from_components(&mut components)?;
146 let email = components
147 .next()
148 .ok_or(ParseError::Missing("email"))?
149 .trim_matches(|c| c == '<' || c == '>')
150 .to_owned();
151 let name = components.next().ok_or(ParseError::Missing("name"))?;
152 Ok(Self {
153 name: name.to_owned(),
154 email: email.to_owned(),
155 time,
156 })
157 }
158}