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