branchless/git/
object.rs

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/// Represents a commit object in the Git object database.
20#[derive(Clone, Debug)]
21pub struct Commit<'repo> {
22    pub(super) inner: git2::Commit<'repo>,
23}
24
25impl<'repo> Commit<'repo> {
26    /// Get the object ID of the commit.
27    #[instrument]
28    pub fn get_oid(&self) -> NonZeroOid {
29        NonZeroOid {
30            inner: self.inner.id(),
31        }
32    }
33
34    /// Get the short object ID of the commit.
35    #[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    /// Get the object IDs of the parents of this commit.
49    #[instrument]
50    pub fn get_parent_oids(&self) -> Vec<NonZeroOid> {
51        self.inner.parent_ids().map(make_non_zero_oid).collect()
52    }
53
54    /// Get the parent OID of this commit if there is exactly one parent, or
55    /// `None` otherwise.
56    #[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    /// Get the number of parents of this commit.
65    #[instrument]
66    pub fn get_parent_count(&self) -> usize {
67        self.inner.parent_count()
68    }
69
70    /// Get the parent commits of this commit.
71    #[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    /// Get the parent of this commit if there is exactly one parent, or `None`
80    /// otherwise.
81    #[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    /// Get the commit time of this commit.
90    #[instrument]
91    pub fn get_time(&self) -> Time {
92        Time {
93            inner: self.inner.time(),
94        }
95    }
96
97    /// Get the summary (first line) of the commit message.
98    #[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    /// Get the commit message with some whitespace trimmed.
107    #[instrument]
108    pub fn get_message_pretty(&self) -> BString {
109        BString::from(self.inner.message_bytes())
110    }
111
112    /// Get the commit message, without any whitespace trimmed.
113    #[instrument]
114    pub fn get_message_raw(&self) -> BString {
115        BString::from(self.inner.message_raw_bytes())
116    }
117
118    /// Get the author of this commit.
119    #[instrument]
120    pub fn get_author(&self) -> Signature {
121        Signature {
122            inner: self.inner.author(),
123        }
124    }
125
126    /// Get the committer of this commit.
127    #[instrument]
128    pub fn get_committer(&self) -> Signature {
129        Signature {
130            inner: self.inner.committer(),
131        }
132    }
133
134    /// Get the OID of the `Tree` object associated with this commit.
135    #[instrument]
136    pub fn get_tree_oid(&self) -> MaybeZeroOid {
137        self.inner.tree_id().into()
138    }
139
140    /// Get the `Tree` object associated with this commit.
141    #[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    /// Get the "trailer" metadata from this commit's message. These are strings
151    /// like `Signed-off-by: foo` which appear at the end of the commit message.
152    #[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    /// Print a one-line description of this commit containing its OID and
171    /// summary.
172    #[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    /// Print a shortened colorized version of the OID of this commit.
200    #[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    /// Get a multi-line description of this commit containing information about
222    /// its OID, author, commit time, and message.
223    #[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    /// Determine if the current commit is empty (has no changes compared to its
260    /// parent).
261    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    /// Determine if this commit added, removed, or changed the entry at the
270    /// provided file path.
271    #[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    /// Amend this existing commit.
293    /// Returns the OID of the resulting new commit.
294    #[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    /// Get the size of the blob in bytes.
324    pub fn size(&self) -> u64 {
325        self.inner.size().try_into().unwrap()
326    }
327
328    /// Get the content of the blob as a byte slice.
329    pub fn get_content(&self) -> &[u8] {
330        self.inner.content()
331    }
332
333    /// Determine if the blob is binary. Note that this looks only at the
334    /// content of the blob to determine if it's binary; attributes set in
335    /// `.gitattributes`, etc. are not checked.
336    pub fn is_binary(&self) -> bool {
337        self.inner.is_binary()
338    }
339}