radicle_surf/
commit.rs

1// This file is part of radicle-surf
2// <https://github.com/radicle-dev/radicle-surf>
3//
4// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
5//
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License version 3 or
8// later as published by the Free Software Foundation.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18use 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    /// When trying to get the summary for a [`git2::Commit`] some action
29    /// failed.
30    #[error("an error occurred trying to get a commit's summary")]
31    MissingSummary,
32    #[error(transparent)]
33    Utf8Error(#[from] str::Utf8Error),
34}
35
36/// Represents the authorship of actions in a git repo.
37#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
38#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
39pub struct Author {
40    /// Name of the author.
41    pub name: String,
42    /// Email of the author.
43    pub email: String,
44    /// Time the action was taken, e.g. time of commit.
45    #[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/// Time used in the authorship of an action in a git repo.
56#[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    /// Returns the seconds since UNIX epoch.
73    pub fn seconds(&self) -> i64 {
74        self.inner.seconds()
75    }
76
77    /// Returns the timezone offset in minutes.
78    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/// `Commit` is the metadata of a [Git commit][git-commit].
129///
130/// [git-commit]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
131#[cfg_attr(feature = "serde", derive(Deserialize))]
132#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
133pub struct Commit {
134    /// Object Id
135    pub id: Oid,
136    /// The author of the commit.
137    pub author: Author,
138    /// The actor who committed this commit.
139    pub committer: Author,
140    /// The long form message of the commit.
141    pub message: String,
142    /// The summary message of the commit.
143    pub summary: String,
144    /// The parents of this commit.
145    pub parents: Vec<Oid>,
146}
147
148impl Commit {
149    /// Returns the commit description text. This is the text after the one-line
150    /// summary.
151    #[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}