1use std::path::Path;
2
3use bstr::{BString, ByteSlice};
4use cursive::theme::BaseColor;
5use cursive::utils::markup::StyledString;
6use git2::message_trailers_bytes;
7use tracing::instrument;
8
9use crate::core::formatting::{Glyphs, StyledStringBuilder};
10use crate::core::node_descriptors::{
11 render_node_descriptors, CommitMessageDescriptor, CommitOidDescriptor, NodeObject, Redactor,
12};
13use crate::git::oid::make_non_zero_oid;
14use crate::git::repo::{Error, Result, Signature};
15use crate::git::{NonZeroOid, Time, Tree};
16
17use super::MaybeZeroOid;
18
19#[derive(Clone, Debug)]
21pub struct Commit<'repo> {
22 pub(super) inner: git2::Commit<'repo>,
23}
24
25impl<'repo> Commit<'repo> {
26 #[instrument]
28 pub fn get_oid(&self) -> NonZeroOid {
29 NonZeroOid {
30 inner: self.inner.id(),
31 }
32 }
33
34 #[instrument]
36 pub fn get_short_oid(&self) -> Result<String> {
37 Ok(String::from_utf8_lossy(
38 &self
39 .inner
40 .clone()
41 .into_object()
42 .short_id()
43 .map_err(Error::Git)?,
44 )
45 .to_string())
46 }
47
48 #[instrument]
50 pub fn get_parent_oids(&self) -> Vec<NonZeroOid> {
51 self.inner.parent_ids().map(make_non_zero_oid).collect()
52 }
53
54 #[instrument]
57 pub fn get_only_parent_oid(&self) -> Option<NonZeroOid> {
58 match self.get_parent_oids().as_slice() {
59 [] | [_, _, ..] => None,
60 [only_parent_oid] => Some(*only_parent_oid),
61 }
62 }
63
64 #[instrument]
66 pub fn get_parent_count(&self) -> usize {
67 self.inner.parent_count()
68 }
69
70 #[instrument]
72 pub fn get_parents(&self) -> Vec<Commit<'repo>> {
73 self.inner
74 .parents()
75 .map(|commit| Commit { inner: commit })
76 .collect()
77 }
78
79 #[instrument]
82 pub fn get_only_parent(&self) -> Option<Commit<'repo>> {
83 match self.get_parents().as_slice() {
84 [] | [_, _, ..] => None,
85 [only_parent] => Some(only_parent.clone()),
86 }
87 }
88
89 #[instrument]
91 pub fn get_time(&self) -> Time {
92 Time {
93 inner: self.inner.time(),
94 }
95 }
96
97 #[instrument]
99 pub fn get_summary(&self) -> Result<BString> {
100 match self.inner.summary_bytes() {
101 Some(summary) => Ok(BString::from(summary)),
102 None => Err(Error::DecodeUtf8 { item: "summary" }),
103 }
104 }
105
106 #[instrument]
108 pub fn get_message_pretty(&self) -> BString {
109 BString::from(self.inner.message_bytes())
110 }
111
112 #[instrument]
114 pub fn get_message_raw(&self) -> BString {
115 BString::from(self.inner.message_raw_bytes())
116 }
117
118 #[instrument]
120 pub fn get_author(&self) -> Signature {
121 Signature {
122 inner: self.inner.author(),
123 }
124 }
125
126 #[instrument]
128 pub fn get_committer(&self) -> Signature {
129 Signature {
130 inner: self.inner.committer(),
131 }
132 }
133
134 #[instrument]
136 pub fn get_tree_oid(&self) -> MaybeZeroOid {
137 self.inner.tree_id().into()
138 }
139
140 #[instrument]
142 pub fn get_tree(&self) -> Result<Tree> {
143 let tree = self.inner.tree().map_err(|err| Error::FindTree {
144 source: err,
145 oid: self.inner.tree_id().into(),
146 })?;
147 Ok(Tree { inner: tree })
148 }
149
150 #[instrument]
153 pub fn get_trailers(&self) -> Result<Vec<(String, String)>> {
154 let message = self.get_message_raw();
155 let message = message.to_str().map_err(|_| Error::DecodeUtf8 {
156 item: "raw message",
157 })?;
158 let mut result = Vec::new();
159 for (k, v) in message_trailers_bytes(message)
160 .map_err(Error::ReadMessageTrailer)?
161 .iter()
162 {
163 if let (Ok(k), Ok(v)) = (std::str::from_utf8(k), std::str::from_utf8(v)) {
164 result.push((k.to_owned(), v.to_owned()));
165 }
166 }
167 Ok(result)
168 }
169
170 #[instrument]
173 pub fn friendly_describe(&self, glyphs: &Glyphs) -> Result<StyledString> {
174 let description = render_node_descriptors(
175 glyphs,
176 &NodeObject::Commit {
177 commit: self.clone(),
178 },
179 &mut [
180 &mut CommitOidDescriptor::new(true).map_err(|err| Error::DescribeCommit {
181 source: err,
182 commit: self.get_oid(),
183 })?,
184 &mut CommitMessageDescriptor::new(&Redactor::Disabled).map_err(|err| {
185 Error::DescribeCommit {
186 source: err,
187 commit: self.get_oid(),
188 }
189 })?,
190 ],
191 )
192 .map_err(|err| Error::DescribeCommit {
193 source: err,
194 commit: self.get_oid(),
195 })?;
196 Ok(description)
197 }
198
199 #[instrument]
201 pub fn friendly_describe_oid(&self, glyphs: &Glyphs) -> Result<StyledString> {
202 let description = render_node_descriptors(
203 glyphs,
204 &NodeObject::Commit {
205 commit: self.clone(),
206 },
207 &mut [
208 &mut CommitOidDescriptor::new(true).map_err(|err| Error::DescribeCommit {
209 source: err,
210 commit: self.get_oid(),
211 })?,
212 ],
213 )
214 .map_err(|err| Error::DescribeCommit {
215 source: err,
216 commit: self.get_oid(),
217 })?;
218 Ok(description)
219 }
220
221 #[instrument]
224 pub fn friendly_preview(&self) -> Result<StyledString> {
225 let commit_time = self.get_time().to_naive_date_time();
226 let preview = StyledStringBuilder::from_lines(vec![
227 StyledStringBuilder::new()
228 .append_styled(
229 format!("Commit:\t{}", self.get_oid()),
230 BaseColor::Yellow.light(),
231 )
232 .build(),
233 StyledString::styled(
234 format!(
235 "Author:\t{}",
236 self.get_author()
237 .friendly_describe()
238 .unwrap_or_else(|| "".into())
239 ),
240 BaseColor::Magenta.light(),
241 ),
242 StyledString::styled(
243 format!(
244 "Date:\t{}",
245 commit_time
246 .map(|commit_time| commit_time.to_string())
247 .unwrap_or_else(|| "?".to_string())
248 ),
249 BaseColor::Green.light(),
250 ),
251 StyledString::plain(textwrap::indent(
252 &self.get_message_pretty().to_str_lossy(),
253 " ",
254 )),
255 ]);
256 Ok(preview)
257 }
258
259 pub fn is_empty(&self) -> bool {
262 match self.get_parents().as_slice() {
263 [] => false,
264 [parent_commit] => self.inner.tree_id() == parent_commit.inner.tree_id(),
265 _ => false,
266 }
267 }
268
269 #[instrument]
272 pub fn contains_touched_path(&self, path: &Path) -> Result<Option<bool>> {
273 let parent = match self.get_only_parent() {
274 None => return Ok(None),
275 Some(parent) => parent,
276 };
277 let parent_tree = parent.get_tree()?;
278 let current_tree = self.get_tree()?;
279 let parent_oid = parent_tree
280 .get_oid_for_path(path)
281 .map_err(Error::ReadTreeEntry)?;
282 let current_oid = current_tree
283 .get_oid_for_path(path)
284 .map_err(Error::ReadTreeEntry)?;
285 match (parent_oid, current_oid) {
286 (None, None) => Ok(Some(false)),
287 (None, Some(_)) | (Some(_), None) => Ok(Some(true)),
288 (Some(parent_oid), Some(current_oid)) => Ok(Some(parent_oid != current_oid)),
289 }
290 }
291
292 #[instrument]
295 pub fn amend_commit(
296 &self,
297 update_ref: Option<&str>,
298 author: Option<&Signature>,
299 committer: Option<&Signature>,
300 message: Option<&str>,
301 tree: Option<&Tree>,
302 ) -> Result<NonZeroOid> {
303 let oid = self
304 .inner
305 .amend(
306 update_ref,
307 author.map(|author| &author.inner),
308 committer.map(|committer| &committer.inner),
309 None,
310 message,
311 tree.map(|tree| &tree.inner),
312 )
313 .map_err(Error::Amend)?;
314 Ok(make_non_zero_oid(oid))
315 }
316}
317
318pub struct Blob<'repo> {
319 pub(super) inner: git2::Blob<'repo>,
320}
321
322impl<'repo> Blob<'repo> {
323 pub fn size(&self) -> u64 {
325 self.inner.size().try_into().unwrap()
326 }
327
328 pub fn get_content(&self) -> &[u8] {
330 self.inner.content()
331 }
332
333 pub fn is_binary(&self) -> bool {
337 self.inner.is_binary()
338 }
339}