git_commits/
commit.rs

1use std::borrow::Cow;
2use std::fmt;
3
4use git2::Repository;
5
6#[cfg(feature = "chrono")]
7use chrono::{DateTime, FixedOffset, Local, TimeZone, Utc};
8
9use super::Changes;
10use super::GitError;
11
12pub struct Commit<'repo> {
13    pub(crate) repo: &'repo Repository,
14    pub(crate) commit: git2::Commit<'repo>,
15}
16
17impl<'repo> Commit<'repo> {
18    #[inline]
19    pub(crate) const fn new(repo: &'repo Repository, commit: git2::Commit<'repo>) -> Self {
20        Self { repo, commit }
21    }
22
23    #[doc(alias = "hash")]
24    #[inline]
25    pub fn sha(&self) -> String {
26        self.commit.id().to_string()
27    }
28
29    #[inline]
30    pub fn message_bytes(&self) -> &[u8] {
31        self.commit.message_bytes()
32    }
33
34    #[inline]
35    pub fn message(&self) -> Option<&str> {
36        self.commit.message()
37    }
38
39    #[inline]
40    pub fn message_lossy(&self) -> String {
41        let msg = self.message_bytes();
42        String::from_utf8_lossy(msg).into_owned()
43    }
44
45    /// The author is the person who wrote the code.
46    #[inline]
47    pub fn author(&self) -> Signature<'_> {
48        Signature {
49            signature: self.commit.author(),
50        }
51    }
52
53    /// The committer is the person who committed the code,
54    /// on behalf of the author.
55    #[inline]
56    pub fn committer(&self) -> Signature<'_> {
57        Signature {
58            signature: self.commit.committer(),
59        }
60    }
61
62    /// Returns the commit time (i.e. committer time) of a commit.
63    ///
64    /// Returns `(seconds, offset_minutes)`.
65    ///
66    /// _See also [`.time()`](Self::time) for a `chrono` `DateTime`._
67    #[inline]
68    pub fn when(&self) -> (i64, i32) {
69        let time = self.commit.time();
70        (time.seconds(), time.offset_minutes())
71    }
72
73    /// Returns the commit time (i.e. committer time) of a commit.
74    ///
75    /// Returns `None` for an invalid timestamp.
76    #[cfg(feature = "chrono")]
77    pub fn time(&self) -> Option<DateTime<FixedOffset>> {
78        let time = self.commit.time();
79
80        let offset = time.offset_minutes().checked_mul(60)?;
81        let offset = FixedOffset::east_opt(offset)?;
82        offset.timestamp_opt(time.seconds(), 0).single()
83    }
84
85    /// Returns the commit time (i.e. committer time) of a commit.
86    ///
87    /// Returns `None` for an invalid timestamp.
88    #[cfg(feature = "chrono")]
89    #[inline]
90    pub fn time_utc(&self) -> Option<DateTime<Utc>> {
91        let time = self.time()?.with_timezone(&Utc);
92        Some(time)
93    }
94
95    /// Returns the commit time (i.e. committer time) of a commit.
96    ///
97    /// Returns `None` for an invalid timestamp.
98    #[cfg(feature = "chrono")]
99    #[inline]
100    pub fn time_local(&self) -> Option<DateTime<Local>> {
101        let time = self.time()?.with_timezone(&Local);
102        Some(time)
103    }
104
105    /// Returns an iterator that produces all changes
106    /// this commit performed.
107    #[inline]
108    pub fn changes(&self) -> Result<Changes<'repo, '_>, GitError> {
109        Changes::from_commit(self)
110    }
111}
112
113impl fmt::Display for Commit<'_> {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        match self.time() {
116            Some(time) => write!(f, "[{time}]")?,
117            None => write!(f, "[invalid time]")?,
118        }
119
120        let msg = self.message_lossy();
121        let first_line = msg.trim().lines().next().unwrap_or_default();
122
123        write!(f, " {} {first_line}", self.author().name_lossy())?;
124
125        Ok(())
126    }
127}
128
129pub struct Signature<'a> {
130    signature: git2::Signature<'a>,
131}
132
133impl Signature<'_> {
134    /// Returns `None` if the name is not valid UTF-8.
135    #[inline]
136    pub fn name(&self) -> Option<&str> {
137        self.signature.name()
138    }
139
140    #[inline]
141    pub fn name_bytes(&self) -> &[u8] {
142        self.signature.name_bytes()
143    }
144
145    #[inline]
146    pub fn name_lossy(&self) -> Cow<'_, str> {
147        String::from_utf8_lossy(self.name_bytes())
148    }
149
150    /// Returns `None` if the email is not valid UTF-8.
151    #[inline]
152    pub fn email(&self) -> Option<&str> {
153        self.signature.email()
154    }
155
156    #[inline]
157    pub fn email_bytes(&self) -> &[u8] {
158        self.signature.email_bytes()
159    }
160
161    #[inline]
162    pub fn email_lossy(&self) -> Cow<'_, str> {
163        String::from_utf8_lossy(self.email_bytes())
164    }
165
166    /// Returns `(seconds, offset_minutes)`.
167    ///
168    /// _See also [`.time()`](Self::time) for a `chrono` `DateTime`._
169    #[inline]
170    pub fn when(&self) -> (i64, i32) {
171        let time = self.signature.when();
172        (time.seconds(), time.offset_minutes())
173    }
174
175    /// Returns `None` for an invalid timestamp.
176    #[cfg(feature = "chrono")]
177    pub fn time(&self) -> Option<DateTime<FixedOffset>> {
178        let time = self.signature.when();
179
180        let offset = time.offset_minutes().checked_mul(60)?;
181        let offset = FixedOffset::east_opt(offset)?;
182        offset.timestamp_opt(time.seconds(), 0).single()
183    }
184
185    /// Returns `None` for an invalid timestamp.
186    #[cfg(feature = "chrono")]
187    #[inline]
188    pub fn time_utc(&self) -> Option<DateTime<Utc>> {
189        let time = self.time()?.with_timezone(&Utc);
190        Some(time)
191    }
192
193    /// Returns `None` for an invalid timestamp.
194    #[cfg(feature = "chrono")]
195    #[inline]
196    pub fn time_local(&self) -> Option<DateTime<Local>> {
197        let time = self.time()?.with_timezone(&Local);
198        Some(time)
199    }
200}
201
202impl fmt::Display for Signature<'_> {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        match self.time() {
205            Some(time) => write!(f, "[{time}]")?,
206            None => write!(f, "[invalid time]")?,
207        }
208
209        write!(f, " {} <{}>", self.name_lossy(), self.email_lossy())?;
210
211        Ok(())
212    }
213}