1use std::{convert::TryFrom, str};
2
3use radicle_git_ext::Oid;
4use thiserror::Error;
5
6#[cfg(feature = "serde")]
7use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
8
9#[derive(Debug, Error)]
10pub enum Error {
11 #[error("an error occurred trying to get a commit's summary")]
14 MissingSummary,
15 #[error(transparent)]
16 Utf8Error(#[from] str::Utf8Error),
17}
18
19#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
21#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
22pub struct Author {
23 pub name: String,
25 pub email: String,
27 #[cfg_attr(
29 feature = "serde",
30 serde(
31 serialize_with = "serialize_time",
32 deserialize_with = "deserialize_time"
33 )
34 )]
35 pub time: Time,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
40pub struct Time {
41 inner: git2::Time,
42}
43
44impl From<git2::Time> for Time {
45 fn from(inner: git2::Time) -> Self {
46 Self { inner }
47 }
48}
49
50impl Time {
51 pub fn new(epoch_seconds: i64, offset_minutes: i32) -> Self {
52 git2::Time::new(epoch_seconds, offset_minutes).into()
53 }
54
55 pub fn seconds(&self) -> i64 {
57 self.inner.seconds()
58 }
59
60 pub fn offset_minutes(&self) -> i32 {
62 self.inner.offset_minutes()
63 }
64}
65
66#[cfg(feature = "serde")]
67fn deserialize_time<'de, D>(deserializer: D) -> Result<Time, D::Error>
68where
69 D: Deserializer<'de>,
70{
71 let seconds: i64 = Deserialize::deserialize(deserializer)?;
72 Ok(Time::new(seconds, 0))
73}
74
75#[cfg(feature = "serde")]
76fn serialize_time<S>(t: &Time, serializer: S) -> Result<S::Ok, S::Error>
77where
78 S: Serializer,
79{
80 serializer.serialize_i64(t.seconds())
81}
82
83impl std::fmt::Debug for Author {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 use std::cmp::Ordering;
86 let time = match self.time.offset_minutes().cmp(&0) {
87 Ordering::Equal => format!("{}", self.time.seconds()),
88 Ordering::Greater => format!("{}+{}", self.time.seconds(), self.time.offset_minutes()),
89 Ordering::Less => format!("{}{}", self.time.seconds(), self.time.offset_minutes()),
90 };
91 f.debug_struct("Author")
92 .field("name", &self.name)
93 .field("email", &self.email)
94 .field("time", &time)
95 .finish()
96 }
97}
98
99impl TryFrom<git2::Signature<'_>> for Author {
100 type Error = str::Utf8Error;
101
102 fn try_from(signature: git2::Signature) -> Result<Self, Self::Error> {
103 let name = str::from_utf8(signature.name_bytes())?.into();
104 let email = str::from_utf8(signature.email_bytes())?.into();
105 let time = signature.when().into();
106
107 Ok(Author { name, email, time })
108 }
109}
110
111#[cfg_attr(feature = "serde", derive(Deserialize))]
115#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
116pub struct Commit {
117 pub id: Oid,
119 pub author: Author,
121 pub committer: Author,
123 pub message: String,
125 pub summary: String,
127 pub parents: Vec<Oid>,
129}
130
131impl Commit {
132 #[must_use]
135 pub fn description(&self) -> &str {
136 self.message
137 .strip_prefix(&self.summary)
138 .unwrap_or(&self.message)
139 .trim()
140 }
141}
142
143#[cfg(feature = "serde")]
144impl Serialize for Commit {
145 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
146 where
147 S: Serializer,
148 {
149 let mut state = serializer.serialize_struct("Commit", 7)?;
150 state.serialize_field("id", &self.id.to_string())?;
151 state.serialize_field("author", &self.author)?;
152 state.serialize_field("committer", &self.committer)?;
153 state.serialize_field("summary", &self.summary)?;
154 state.serialize_field("message", &self.message)?;
155 state.serialize_field("description", &self.description())?;
156 state.serialize_field(
157 "parents",
158 &self
159 .parents
160 .iter()
161 .map(|oid| oid.to_string())
162 .collect::<Vec<String>>(),
163 )?;
164 state.end()
165 }
166}
167
168impl TryFrom<git2::Commit<'_>> for Commit {
169 type Error = Error;
170
171 fn try_from(commit: git2::Commit) -> Result<Self, Self::Error> {
172 let id = commit.id().into();
173 let author = Author::try_from(commit.author())?;
174 let committer = Author::try_from(commit.committer())?;
175 let message_raw = commit.message_bytes();
176 let message = str::from_utf8(message_raw)?.into();
177 let summary_raw = commit.summary_bytes().ok_or(Error::MissingSummary)?;
178 let summary = str::from_utf8(summary_raw)?.into();
179 let parents = commit.parent_ids().map(|oid| oid.into()).collect();
180
181 Ok(Commit {
182 id,
183 author,
184 committer,
185 message,
186 summary,
187 parents,
188 })
189 }
190}