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 #[inline]
47 pub fn author(&self) -> Signature<'_> {
48 Signature {
49 signature: self.commit.author(),
50 }
51 }
52
53 #[inline]
56 pub fn committer(&self) -> Signature<'_> {
57 Signature {
58 signature: self.commit.committer(),
59 }
60 }
61
62 #[inline]
68 pub fn when(&self) -> (i64, i32) {
69 let time = self.commit.time();
70 (time.seconds(), time.offset_minutes())
71 }
72
73 #[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 #[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 #[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 #[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 #[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 #[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 #[inline]
170 pub fn when(&self) -> (i64, i32) {
171 let time = self.signature.when();
172 (time.seconds(), time.offset_minutes())
173 }
174
175 #[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 #[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 #[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}