Skip to main content

radicle_surf/
commit.rs

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    /// When trying to get the summary for a [`git2::Commit`] some action
12    /// failed.
13    #[error("an error occurred trying to get a commit's summary")]
14    MissingSummary,
15    #[error(transparent)]
16    Utf8Error(#[from] str::Utf8Error),
17}
18
19/// Represents the authorship of actions in a git repo.
20#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
21#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
22pub struct Author {
23    /// Name of the author.
24    pub name: String,
25    /// Email of the author.
26    pub email: String,
27    /// Time the action was taken, e.g. time of commit.
28    #[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/// Time used in the authorship of an action in a git repo.
39#[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    /// Returns the seconds since UNIX epoch.
56    pub fn seconds(&self) -> i64 {
57        self.inner.seconds()
58    }
59
60    /// Returns the timezone offset in minutes.
61    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/// `Commit` is the metadata of a [Git commit][git-commit].
112///
113/// [git-commit]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
114#[cfg_attr(feature = "serde", derive(Deserialize))]
115#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
116pub struct Commit {
117    /// Object Id
118    pub id: Oid,
119    /// The author of the commit.
120    pub author: Author,
121    /// The actor who committed this commit.
122    pub committer: Author,
123    /// The long form message of the commit.
124    pub message: String,
125    /// The summary message of the commit.
126    pub summary: String,
127    /// The parents of this commit.
128    pub parents: Vec<Oid>,
129}
130
131impl Commit {
132    /// Returns the commit description text. This is the text after the one-line
133    /// summary.
134    #[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}