git2/
commit.rs

1use std::iter::FusedIterator;
2use std::marker;
3use std::mem;
4use std::ops::Range;
5use std::ptr;
6use std::str;
7
8use crate::util::Binding;
9use crate::{raw, signature, Buf, Error, IntoCString, Mailmap, Object, Oid, Signature, Time, Tree};
10
11/// A structure to represent a git [commit][1]
12///
13/// [1]: http://git-scm.com/book/en/Git-Internals-Git-Objects
14pub struct Commit<'repo> {
15    raw: *mut raw::git_commit,
16    _marker: marker::PhantomData<Object<'repo>>,
17}
18
19/// An iterator over the parent commits of a commit.
20///
21/// Aborts iteration when a commit cannot be found
22pub struct Parents<'commit, 'repo> {
23    range: Range<usize>,
24    commit: &'commit Commit<'repo>,
25}
26
27/// An iterator over the parent commits' ids of a commit.
28///
29/// Aborts iteration when a commit cannot be found
30pub struct ParentIds<'commit> {
31    range: Range<usize>,
32    commit: &'commit Commit<'commit>,
33}
34
35impl<'repo> Commit<'repo> {
36    /// Get the id (SHA1) of a repository commit
37    pub fn id(&self) -> Oid {
38        unsafe { Binding::from_raw(raw::git_commit_id(&*self.raw)) }
39    }
40
41    /// Get the id of the tree pointed to by this commit.
42    ///
43    /// No attempts are made to fetch an object from the ODB.
44    pub fn tree_id(&self) -> Oid {
45        unsafe { Binding::from_raw(raw::git_commit_tree_id(&*self.raw)) }
46    }
47
48    /// Get the tree pointed to by a commit.
49    pub fn tree(&self) -> Result<Tree<'repo>, Error> {
50        let mut ret = ptr::null_mut();
51        unsafe {
52            try_call!(raw::git_commit_tree(&mut ret, &*self.raw));
53            Ok(Binding::from_raw(ret))
54        }
55    }
56
57    /// Get access to the underlying raw pointer.
58    pub fn raw(&self) -> *mut raw::git_commit {
59        self.raw
60    }
61
62    /// Get the full message of a commit.
63    ///
64    /// The returned message will be slightly prettified by removing any
65    /// potential leading newlines.
66    ///
67    /// `None` will be returned if the message is not valid utf-8
68    pub fn message(&self) -> Option<&str> {
69        str::from_utf8(self.message_bytes()).ok()
70    }
71
72    /// Get the full message of a commit as a byte slice.
73    ///
74    /// The returned message will be slightly prettified by removing any
75    /// potential leading newlines.
76    pub fn message_bytes(&self) -> &[u8] {
77        unsafe { crate::opt_bytes(self, raw::git_commit_message(&*self.raw)).unwrap() }
78    }
79
80    /// Get the encoding for the message of a commit, as a string representing a
81    /// standard encoding name.
82    ///
83    /// `None` will be returned if the encoding is not known
84    pub fn message_encoding(&self) -> Option<&str> {
85        let bytes = unsafe { crate::opt_bytes(self, raw::git_commit_message_encoding(&*self.raw)) };
86        bytes.and_then(|b| str::from_utf8(b).ok())
87    }
88
89    /// Get the full raw message of a commit.
90    ///
91    /// `None` will be returned if the message is not valid utf-8
92    pub fn message_raw(&self) -> Option<&str> {
93        str::from_utf8(self.message_raw_bytes()).ok()
94    }
95
96    /// Get the full raw message of a commit.
97    pub fn message_raw_bytes(&self) -> &[u8] {
98        unsafe { crate::opt_bytes(self, raw::git_commit_message_raw(&*self.raw)).unwrap() }
99    }
100
101    /// Get the full raw text of the commit header.
102    ///
103    /// `None` will be returned if the message is not valid utf-8
104    pub fn raw_header(&self) -> Option<&str> {
105        str::from_utf8(self.raw_header_bytes()).ok()
106    }
107
108    /// Get an arbitrary header field.
109    pub fn header_field_bytes<T: IntoCString>(&self, field: T) -> Result<Buf, Error> {
110        let buf = Buf::new();
111        let raw_field = field.into_c_string()?;
112        unsafe {
113            try_call!(raw::git_commit_header_field(
114                buf.raw(),
115                &*self.raw,
116                raw_field
117            ));
118        }
119        Ok(buf)
120    }
121
122    /// Get the full raw text of the commit header.
123    pub fn raw_header_bytes(&self) -> &[u8] {
124        unsafe { crate::opt_bytes(self, raw::git_commit_raw_header(&*self.raw)).unwrap() }
125    }
126
127    /// Get the short "summary" of the git commit message.
128    ///
129    /// The returned message is the summary of the commit, comprising the first
130    /// paragraph of the message with whitespace trimmed and squashed.
131    ///
132    /// `None` may be returned if an error occurs or if the summary is not valid
133    /// utf-8.
134    pub fn summary(&self) -> Option<&str> {
135        self.summary_bytes().and_then(|s| str::from_utf8(s).ok())
136    }
137
138    /// Get the short "summary" of the git commit message.
139    ///
140    /// The returned message is the summary of the commit, comprising the first
141    /// paragraph of the message with whitespace trimmed and squashed.
142    ///
143    /// `None` may be returned if an error occurs
144    pub fn summary_bytes(&self) -> Option<&[u8]> {
145        unsafe { crate::opt_bytes(self, raw::git_commit_summary(self.raw)) }
146    }
147
148    /// Get the long "body" of the git commit message.
149    ///
150    /// The returned message is the body of the commit, comprising everything
151    /// but the first paragraph of the message. Leading and trailing whitespaces
152    /// are trimmed.
153    ///
154    /// `None` may be returned if an error occurs or if the summary is not valid
155    /// utf-8.
156    pub fn body(&self) -> Option<&str> {
157        self.body_bytes().and_then(|s| str::from_utf8(s).ok())
158    }
159
160    /// Get the long "body" of the git commit message.
161    ///
162    /// The returned message is the body of the commit, comprising everything
163    /// but the first paragraph of the message. Leading and trailing whitespaces
164    /// are trimmed.
165    ///
166    /// `None` may be returned if an error occurs.
167    pub fn body_bytes(&self) -> Option<&[u8]> {
168        unsafe { crate::opt_bytes(self, raw::git_commit_body(self.raw)) }
169    }
170
171    /// Get the commit time (i.e. committer time) of a commit.
172    ///
173    /// The first element of the tuple is the time, in seconds, since the epoch.
174    /// The second element is the offset, in minutes, of the time zone of the
175    /// committer's preferred time zone.
176    pub fn time(&self) -> Time {
177        unsafe {
178            Time::new(
179                raw::git_commit_time(&*self.raw) as i64,
180                raw::git_commit_time_offset(&*self.raw) as i32,
181            )
182        }
183    }
184
185    /// Creates a new iterator over the parents of this commit.
186    pub fn parents<'a>(&'a self) -> Parents<'a, 'repo> {
187        Parents {
188            range: 0..self.parent_count(),
189            commit: self,
190        }
191    }
192
193    /// Creates a new iterator over the parents of this commit.
194    pub fn parent_ids(&self) -> ParentIds<'_> {
195        ParentIds {
196            range: 0..self.parent_count(),
197            commit: self,
198        }
199    }
200
201    /// Get the author of this commit.
202    pub fn author(&self) -> Signature<'_> {
203        unsafe {
204            let ptr = raw::git_commit_author(&*self.raw);
205            signature::from_raw_const(self, ptr)
206        }
207    }
208
209    /// Get the author of this commit, using the mailmap to map names and email
210    /// addresses to canonical real names and email addresses.
211    pub fn author_with_mailmap(&self, mailmap: &Mailmap) -> Result<Signature<'static>, Error> {
212        let mut ret = ptr::null_mut();
213        unsafe {
214            try_call!(raw::git_commit_author_with_mailmap(
215                &mut ret,
216                &*self.raw,
217                &*mailmap.raw()
218            ));
219            Ok(Binding::from_raw(ret))
220        }
221    }
222
223    /// Get the committer of this commit.
224    pub fn committer(&self) -> Signature<'_> {
225        unsafe {
226            let ptr = raw::git_commit_committer(&*self.raw);
227            signature::from_raw_const(self, ptr)
228        }
229    }
230
231    /// Get the committer of this commit, using the mailmap to map names and email
232    /// addresses to canonical real names and email addresses.
233    pub fn committer_with_mailmap(&self, mailmap: &Mailmap) -> Result<Signature<'static>, Error> {
234        let mut ret = ptr::null_mut();
235        unsafe {
236            try_call!(raw::git_commit_committer_with_mailmap(
237                &mut ret,
238                &*self.raw,
239                &*mailmap.raw()
240            ));
241            Ok(Binding::from_raw(ret))
242        }
243    }
244
245    /// Amend this existing commit with all non-`None` values
246    ///
247    /// This creates a new commit that is exactly the same as the old commit,
248    /// except that any non-`None` values will be updated. The new commit has
249    /// the same parents as the old commit.
250    ///
251    /// For information about `update_ref`, see [`Repository::commit`].
252    ///
253    /// [`Repository::commit`]: struct.Repository.html#method.commit
254    pub fn amend(
255        &self,
256        update_ref: Option<&str>,
257        author: Option<&Signature<'_>>,
258        committer: Option<&Signature<'_>>,
259        message_encoding: Option<&str>,
260        message: Option<&str>,
261        tree: Option<&Tree<'repo>>,
262    ) -> Result<Oid, Error> {
263        let mut raw = raw::git_oid {
264            id: [0; raw::GIT_OID_RAWSZ],
265        };
266        let update_ref = crate::opt_cstr(update_ref)?;
267        let encoding = crate::opt_cstr(message_encoding)?;
268        let message = crate::opt_cstr(message)?;
269        unsafe {
270            try_call!(raw::git_commit_amend(
271                &mut raw,
272                self.raw(),
273                update_ref,
274                author.map(|s| s.raw()),
275                committer.map(|s| s.raw()),
276                encoding,
277                message,
278                tree.map(|t| t.raw())
279            ));
280            Ok(Binding::from_raw(&raw as *const _))
281        }
282    }
283
284    /// Get the number of parents of this commit.
285    ///
286    /// Use the `parents` iterator to return an iterator over all parents.
287    pub fn parent_count(&self) -> usize {
288        unsafe { raw::git_commit_parentcount(&*self.raw) as usize }
289    }
290
291    /// Get the specified parent of the commit.
292    ///
293    /// Use the `parents` iterator to return an iterator over all parents.
294    pub fn parent(&self, i: usize) -> Result<Commit<'repo>, Error> {
295        unsafe {
296            let mut raw = ptr::null_mut();
297            try_call!(raw::git_commit_parent(
298                &mut raw,
299                &*self.raw,
300                i as libc::c_uint
301            ));
302            Ok(Binding::from_raw(raw))
303        }
304    }
305
306    /// Get the specified parent id of the commit.
307    ///
308    /// This is different from `parent`, which will attempt to load the
309    /// parent commit from the ODB.
310    ///
311    /// Use the `parent_ids` iterator to return an iterator over all parents.
312    pub fn parent_id(&self, i: usize) -> Result<Oid, Error> {
313        unsafe {
314            let id = raw::git_commit_parent_id(self.raw, i as libc::c_uint);
315            if id.is_null() {
316                Err(Error::from_str("parent index out of bounds"))
317            } else {
318                Ok(Binding::from_raw(id))
319            }
320        }
321    }
322
323    /// Casts this Commit to be usable as an `Object`
324    pub fn as_object(&self) -> &Object<'repo> {
325        unsafe { &*(self as *const _ as *const Object<'repo>) }
326    }
327
328    /// Consumes Commit to be returned as an `Object`
329    pub fn into_object(self) -> Object<'repo> {
330        assert_eq!(mem::size_of_val(&self), mem::size_of::<Object<'_>>());
331        unsafe { mem::transmute(self) }
332    }
333}
334
335impl<'repo> Binding for Commit<'repo> {
336    type Raw = *mut raw::git_commit;
337    unsafe fn from_raw(raw: *mut raw::git_commit) -> Commit<'repo> {
338        Commit {
339            raw,
340            _marker: marker::PhantomData,
341        }
342    }
343    fn raw(&self) -> *mut raw::git_commit {
344        self.raw
345    }
346}
347
348impl<'repo> std::fmt::Debug for Commit<'repo> {
349    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
350        let mut ds = f.debug_struct("Commit");
351        ds.field("id", &self.id());
352        if let Some(summary) = self.summary() {
353            ds.field("summary", &summary);
354        }
355        ds.finish()
356    }
357}
358
359/// Aborts iteration when a commit cannot be found
360impl<'repo, 'commit> Iterator for Parents<'commit, 'repo> {
361    type Item = Commit<'repo>;
362    fn next(&mut self) -> Option<Commit<'repo>> {
363        self.range.next().and_then(|i| self.commit.parent(i).ok())
364    }
365    fn size_hint(&self) -> (usize, Option<usize>) {
366        self.range.size_hint()
367    }
368}
369
370/// Aborts iteration when a commit cannot be found
371impl<'repo, 'commit> DoubleEndedIterator for Parents<'commit, 'repo> {
372    fn next_back(&mut self) -> Option<Commit<'repo>> {
373        self.range
374            .next_back()
375            .and_then(|i| self.commit.parent(i).ok())
376    }
377}
378
379impl<'repo, 'commit> FusedIterator for Parents<'commit, 'repo> {}
380
381impl<'repo, 'commit> ExactSizeIterator for Parents<'commit, 'repo> {}
382
383/// Aborts iteration when a commit cannot be found
384impl<'commit> Iterator for ParentIds<'commit> {
385    type Item = Oid;
386    fn next(&mut self) -> Option<Oid> {
387        self.range
388            .next()
389            .and_then(|i| self.commit.parent_id(i).ok())
390    }
391    fn size_hint(&self) -> (usize, Option<usize>) {
392        self.range.size_hint()
393    }
394}
395
396/// Aborts iteration when a commit cannot be found
397impl<'commit> DoubleEndedIterator for ParentIds<'commit> {
398    fn next_back(&mut self) -> Option<Oid> {
399        self.range
400            .next_back()
401            .and_then(|i| self.commit.parent_id(i).ok())
402    }
403}
404
405impl<'commit> FusedIterator for ParentIds<'commit> {}
406
407impl<'commit> ExactSizeIterator for ParentIds<'commit> {}
408
409impl<'repo> Clone for Commit<'repo> {
410    fn clone(&self) -> Self {
411        self.as_object().clone().into_commit().ok().unwrap()
412    }
413}
414
415impl<'repo> Drop for Commit<'repo> {
416    fn drop(&mut self) {
417        unsafe { raw::git_commit_free(self.raw) }
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    #[test]
424    fn smoke() {
425        let (_td, repo) = crate::test::repo_init();
426        let head = repo.head().unwrap();
427        let target = head.target().unwrap();
428        let commit = repo.find_commit(target).unwrap();
429        assert_eq!(commit.message(), Some("initial\n\nbody"));
430        assert_eq!(commit.body(), Some("body"));
431        assert_eq!(commit.id(), target);
432        commit.message_raw().unwrap();
433        commit.raw_header().unwrap();
434        commit.message_encoding();
435        commit.summary().unwrap();
436        commit.body().unwrap();
437        commit.tree_id();
438        commit.tree().unwrap();
439        assert_eq!(commit.parents().count(), 0);
440
441        let tree_header_bytes = commit.header_field_bytes("tree").unwrap();
442        assert_eq!(
443            crate::Oid::from_str(tree_header_bytes.as_str().unwrap()).unwrap(),
444            commit.tree_id()
445        );
446        assert_eq!(commit.author().name(), Some("name"));
447        assert_eq!(commit.author().email(), Some("email"));
448        assert_eq!(commit.committer().name(), Some("name"));
449        assert_eq!(commit.committer().email(), Some("email"));
450
451        let sig = repo.signature().unwrap();
452        let tree = repo.find_tree(commit.tree_id()).unwrap();
453        let id = repo
454            .commit(Some("HEAD"), &sig, &sig, "bar", &tree, &[&commit])
455            .unwrap();
456        let head = repo.find_commit(id).unwrap();
457
458        let new_head = head
459            .amend(Some("HEAD"), None, None, None, Some("new message"), None)
460            .unwrap();
461        let new_head = repo.find_commit(new_head).unwrap();
462        assert_eq!(new_head.message(), Some("new message"));
463        new_head.into_object();
464
465        repo.find_object(target, None).unwrap().as_commit().unwrap();
466        repo.find_object(target, None)
467            .unwrap()
468            .into_commit()
469            .ok()
470            .unwrap();
471    }
472}