1 use libc::{c_char, c_int, c_void, size_t};
2 use std::ffi::CString;
3 use std::marker;
4 use std::mem;
5 use std::ops::Range;
6 use std::path::Path;
7 use std::ptr;
8 use std::slice;
9
10 use crate::util::{self, Binding};
11 use crate::{panic, raw, Buf, Delta, DiffFormat, Error, FileMode, Oid, Repository};
12 use crate::{DiffFlags, DiffStatsFormat, IntoCString};
13
14 /// The diff object that contains all individual file deltas.
15 ///
16 /// This is an opaque structure which will be allocated by one of the diff
17 /// generator functions on the `Repository` structure (e.g. `diff_tree_to_tree`
18 /// or other `diff_*` functions).
19 pub struct Diff<'repo> {
20 raw: *mut raw::git_diff,
21 _marker: marker::PhantomData<&'repo Repository>,
22 }
23
24 unsafe impl<'repo> Send for Diff<'repo> {}
25
26 /// Description of changes to one entry.
27 pub struct DiffDelta<'a> {
28 raw: *mut raw::git_diff_delta,
29 _marker: marker::PhantomData<&'a raw::git_diff_delta>,
30 }
31
32 /// Description of one side of a delta.
33 ///
34 /// Although this is called a "file" it could represent a file, a symbolic
35 /// link, a submodule commit id, or even a tree (although that only happens if
36 /// you are tracking type changes or ignored/untracked directories).
37 pub struct DiffFile<'a> {
38 raw: *const raw::git_diff_file,
39 _marker: marker::PhantomData<&'a raw::git_diff_file>,
40 }
41
42 /// Structure describing options about how the diff should be executed.
43 pub struct DiffOptions {
44 pathspec: Vec<CString>,
45 pathspec_ptrs: Vec<*const c_char>,
46 old_prefix: Option<CString>,
47 new_prefix: Option<CString>,
48 raw: raw::git_diff_options,
49 }
50
51 /// Control behavior of rename and copy detection
52 pub struct DiffFindOptions {
53 raw: raw::git_diff_find_options,
54 }
55
56 /// Control behavior of formatting emails
57 pub struct DiffFormatEmailOptions {
58 raw: raw::git_diff_format_email_options,
59 }
60
61 /// An iterator over the diffs in a delta
62 pub struct Deltas<'diff> {
63 range: Range<usize>,
64 diff: &'diff Diff<'diff>,
65 }
66
67 /// Structure describing a line (or data span) of a diff.
68 pub struct DiffLine<'a> {
69 raw: *const raw::git_diff_line,
70 _marker: marker::PhantomData<&'a raw::git_diff_line>,
71 }
72
73 /// Structure describing a hunk of a diff.
74 pub struct DiffHunk<'a> {
75 raw: *const raw::git_diff_hunk,
76 _marker: marker::PhantomData<&'a raw::git_diff_hunk>,
77 }
78
79 /// Structure describing a hunk of a diff.
80 pub struct DiffStats {
81 raw: *mut raw::git_diff_stats,
82 }
83
84 /// Structure describing the binary contents of a diff.
85 pub struct DiffBinary<'a> {
86 raw: *const raw::git_diff_binary,
87 _marker: marker::PhantomData<&'a raw::git_diff_binary>,
88 }
89
90 /// The contents of one of the files in a binary diff.
91 pub struct DiffBinaryFile<'a> {
92 raw: *const raw::git_diff_binary_file,
93 _marker: marker::PhantomData<&'a raw::git_diff_binary_file>,
94 }
95
96 /// When producing a binary diff, the binary data returned will be
97 /// either the deflated full ("literal") contents of the file, or
98 /// the deflated binary delta between the two sides (whichever is
99 /// smaller).
100 #[derive(Copy, Clone, Debug)]
101 pub enum DiffBinaryKind {
102 /// There is no binary delta
103 None,
104 /// The binary data is the literal contents of the file
105 Literal,
106 /// The binary data is the delta from one side to the other
107 Delta,
108 }
109
110 type PrintCb<'a> = dyn FnMut(DiffDelta<'_>, Option<DiffHunk<'_>>, DiffLine<'_>) -> bool + 'a;
111
112 pub type FileCb<'a> = dyn FnMut(DiffDelta<'_>, f32) -> bool + 'a;
113 pub type BinaryCb<'a> = dyn FnMut(DiffDelta<'_>, DiffBinary<'_>) -> bool + 'a;
114 pub type HunkCb<'a> = dyn FnMut(DiffDelta<'_>, DiffHunk<'_>) -> bool + 'a;
115 pub type LineCb<'a> = dyn FnMut(DiffDelta<'_>, Option<DiffHunk<'_>>, DiffLine<'_>) -> bool + 'a;
116
117 pub struct DiffCallbacks<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> {
118 pub file: Option<&'a mut FileCb<'b>>,
119 pub binary: Option<&'c mut BinaryCb<'d>>,
120 pub hunk: Option<&'e mut HunkCb<'f>>,
121 pub line: Option<&'g mut LineCb<'h>>,
122 }
123
124 impl<'repo> Diff<'repo> {
125 /// Merge one diff into another.
126 ///
127 /// This merges items from the "from" list into the "self" list. The
128 /// resulting diff will have all items that appear in either list.
129 /// If an item appears in both lists, then it will be "merged" to appear
130 /// as if the old version was from the "onto" list and the new version
131 /// is from the "from" list (with the exception that if the item has a
132 /// pending DELETE in the middle, then it will show as deleted).
merge(&mut self, from: &Diff<'repo>) -> Result<(), Error>133 pub fn merge(&mut self, from: &Diff<'repo>) -> Result<(), Error> {
134 unsafe {
135 try_call!(raw::git_diff_merge(self.raw, &*from.raw));
136 }
137 Ok(())
138 }
139
140 /// Returns an iterator over the deltas in this diff.
deltas(&self) -> Deltas<'_>141 pub fn deltas(&self) -> Deltas<'_> {
142 let num_deltas = unsafe { raw::git_diff_num_deltas(&*self.raw) };
143 Deltas {
144 range: 0..(num_deltas as usize),
145 diff: self,
146 }
147 }
148
149 /// Return the diff delta for an entry in the diff list.
get_delta(&self, i: usize) -> Option<DiffDelta<'_>>150 pub fn get_delta(&self, i: usize) -> Option<DiffDelta<'_>> {
151 unsafe {
152 let ptr = raw::git_diff_get_delta(&*self.raw, i as size_t);
153 Binding::from_raw_opt(ptr as *mut _)
154 }
155 }
156
157 /// Check if deltas are sorted case sensitively or insensitively.
is_sorted_icase(&self) -> bool158 pub fn is_sorted_icase(&self) -> bool {
159 unsafe { raw::git_diff_is_sorted_icase(&*self.raw) == 1 }
160 }
161
162 /// Iterate over a diff generating formatted text output.
163 ///
164 /// Returning `false` from the callback will terminate the iteration and
165 /// return an error from this function.
print<F>(&self, format: DiffFormat, mut cb: F) -> Result<(), Error> where F: FnMut(DiffDelta<'_>, Option<DiffHunk<'_>>, DiffLine<'_>) -> bool,166 pub fn print<F>(&self, format: DiffFormat, mut cb: F) -> Result<(), Error>
167 where
168 F: FnMut(DiffDelta<'_>, Option<DiffHunk<'_>>, DiffLine<'_>) -> bool,
169 {
170 let mut cb: &mut PrintCb<'_> = &mut cb;
171 let ptr = &mut cb as *mut _;
172 let print: raw::git_diff_line_cb = Some(print_cb);
173 unsafe {
174 try_call!(raw::git_diff_print(self.raw, format, print, ptr as *mut _));
175 Ok(())
176 }
177 }
178
179 /// Loop over all deltas in a diff issuing callbacks.
180 ///
181 /// Returning `false` from any callback will terminate the iteration and
182 /// return an error from this function.
foreach( &self, file_cb: &mut FileCb<'_>, binary_cb: Option<&mut BinaryCb<'_>>, hunk_cb: Option<&mut HunkCb<'_>>, line_cb: Option<&mut LineCb<'_>>, ) -> Result<(), Error>183 pub fn foreach(
184 &self,
185 file_cb: &mut FileCb<'_>,
186 binary_cb: Option<&mut BinaryCb<'_>>,
187 hunk_cb: Option<&mut HunkCb<'_>>,
188 line_cb: Option<&mut LineCb<'_>>,
189 ) -> Result<(), Error> {
190 let mut cbs = DiffCallbacks {
191 file: Some(file_cb),
192 binary: binary_cb,
193 hunk: hunk_cb,
194 line: line_cb,
195 };
196 let ptr = &mut cbs as *mut _;
197 unsafe {
198 let binary_cb_c: raw::git_diff_binary_cb = if cbs.binary.is_some() {
199 Some(binary_cb_c)
200 } else {
201 None
202 };
203 let hunk_cb_c: raw::git_diff_hunk_cb = if cbs.hunk.is_some() {
204 Some(hunk_cb_c)
205 } else {
206 None
207 };
208 let line_cb_c: raw::git_diff_line_cb = if cbs.line.is_some() {
209 Some(line_cb_c)
210 } else {
211 None
212 };
213 let file_cb: raw::git_diff_file_cb = Some(file_cb_c);
214 try_call!(raw::git_diff_foreach(
215 self.raw,
216 file_cb,
217 binary_cb_c,
218 hunk_cb_c,
219 line_cb_c,
220 ptr as *mut _
221 ));
222 Ok(())
223 }
224 }
225
226 /// Accumulate diff statistics for all patches.
stats(&self) -> Result<DiffStats, Error>227 pub fn stats(&self) -> Result<DiffStats, Error> {
228 let mut ret = ptr::null_mut();
229 unsafe {
230 try_call!(raw::git_diff_get_stats(&mut ret, self.raw));
231 Ok(Binding::from_raw(ret))
232 }
233 }
234
235 /// Transform a diff marking file renames, copies, etc.
236 ///
237 /// This modifies a diff in place, replacing old entries that look like
238 /// renames or copies with new entries reflecting those changes. This also
239 /// will, if requested, break modified files into add/remove pairs if the
240 /// amount of change is above a threshold.
find_similar(&mut self, opts: Option<&mut DiffFindOptions>) -> Result<(), Error>241 pub fn find_similar(&mut self, opts: Option<&mut DiffFindOptions>) -> Result<(), Error> {
242 let opts = opts.map(|opts| &opts.raw);
243 unsafe {
244 try_call!(raw::git_diff_find_similar(self.raw, opts));
245 }
246 Ok(())
247 }
248
249 /// Create an e-mail ready patch from a diff.
250 ///
251 /// Matches the format created by `git format-patch`
format_email( &mut self, patch_no: usize, total_patches: usize, commit: &crate::Commit<'repo>, opts: Option<&mut DiffFormatEmailOptions>, ) -> Result<Buf, Error>252 pub fn format_email(
253 &mut self,
254 patch_no: usize,
255 total_patches: usize,
256 commit: &crate::Commit<'repo>,
257 opts: Option<&mut DiffFormatEmailOptions>,
258 ) -> Result<Buf, Error> {
259 assert!(patch_no > 0);
260 assert!(patch_no <= total_patches);
261 let mut default = DiffFormatEmailOptions::default();
262 let mut raw_opts = opts.map_or(&mut default.raw, |opts| &mut opts.raw);
263 let summary = commit.summary_bytes().unwrap();
264 let mut message = commit.message_bytes();
265 assert!(message.starts_with(summary));
266 message = &message[summary.len()..];
267 raw_opts.patch_no = patch_no;
268 raw_opts.total_patches = total_patches;
269 let id = commit.id();
270 raw_opts.id = id.raw();
271 raw_opts.summary = summary.as_ptr() as *const _;
272 raw_opts.body = message.as_ptr() as *const _;
273 raw_opts.author = commit.author().raw();
274 let buf = Buf::new();
275 unsafe {
276 try_call!(raw::git_diff_format_email(buf.raw(), self.raw, &*raw_opts));
277 }
278 Ok(buf)
279 }
280
281 // TODO: num_deltas_of_type, find_similar
282 }
283 impl Diff<'static> {
284 /// Read the contents of a git patch file into a `git_diff` object.
285 ///
286 /// The diff object produced is similar to the one that would be
287 /// produced if you actually produced it computationally by comparing
288 /// two trees, however there may be subtle differences. For example,
289 /// a patch file likely contains abbreviated object IDs, so the
290 /// object IDs parsed by this function will also be abreviated.
from_buffer(buffer: &[u8]) -> Result<Diff<'static>, Error>291 pub fn from_buffer(buffer: &[u8]) -> Result<Diff<'static>, Error> {
292 let mut diff: *mut raw::git_diff = std::ptr::null_mut();
293 unsafe {
294 // NOTE: Doesn't depend on repo, so lifetime can be 'static
295 try_call!(raw::git_diff_from_buffer(
296 &mut diff,
297 buffer.as_ptr() as *const c_char,
298 buffer.len()
299 ));
300 Ok(Diff::from_raw(diff))
301 }
302 }
303 }
304
print_cb( delta: *const raw::git_diff_delta, hunk: *const raw::git_diff_hunk, line: *const raw::git_diff_line, data: *mut c_void, ) -> c_int305 pub extern "C" fn print_cb(
306 delta: *const raw::git_diff_delta,
307 hunk: *const raw::git_diff_hunk,
308 line: *const raw::git_diff_line,
309 data: *mut c_void,
310 ) -> c_int {
311 unsafe {
312 let delta = Binding::from_raw(delta as *mut _);
313 let hunk = Binding::from_raw_opt(hunk);
314 let line = Binding::from_raw(line);
315
316 let r = panic::wrap(|| {
317 let data = data as *mut &mut PrintCb<'_>;
318 (*data)(delta, hunk, line)
319 });
320 if r == Some(true) {
321 0
322 } else {
323 -1
324 }
325 }
326 }
327
file_cb_c( delta: *const raw::git_diff_delta, progress: f32, data: *mut c_void, ) -> c_int328 pub extern "C" fn file_cb_c(
329 delta: *const raw::git_diff_delta,
330 progress: f32,
331 data: *mut c_void,
332 ) -> c_int {
333 unsafe {
334 let delta = Binding::from_raw(delta as *mut _);
335
336 let r = panic::wrap(|| {
337 let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>;
338 match (*cbs).file {
339 Some(ref mut cb) => cb(delta, progress),
340 None => false,
341 }
342 });
343 if r == Some(true) {
344 0
345 } else {
346 -1
347 }
348 }
349 }
350
binary_cb_c( delta: *const raw::git_diff_delta, binary: *const raw::git_diff_binary, data: *mut c_void, ) -> c_int351 pub extern "C" fn binary_cb_c(
352 delta: *const raw::git_diff_delta,
353 binary: *const raw::git_diff_binary,
354 data: *mut c_void,
355 ) -> c_int {
356 unsafe {
357 let delta = Binding::from_raw(delta as *mut _);
358 let binary = Binding::from_raw(binary);
359
360 let r = panic::wrap(|| {
361 let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>;
362 match (*cbs).binary {
363 Some(ref mut cb) => cb(delta, binary),
364 None => false,
365 }
366 });
367 if r == Some(true) {
368 0
369 } else {
370 -1
371 }
372 }
373 }
374
hunk_cb_c( delta: *const raw::git_diff_delta, hunk: *const raw::git_diff_hunk, data: *mut c_void, ) -> c_int375 pub extern "C" fn hunk_cb_c(
376 delta: *const raw::git_diff_delta,
377 hunk: *const raw::git_diff_hunk,
378 data: *mut c_void,
379 ) -> c_int {
380 unsafe {
381 let delta = Binding::from_raw(delta as *mut _);
382 let hunk = Binding::from_raw(hunk);
383
384 let r = panic::wrap(|| {
385 let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>;
386 match (*cbs).hunk {
387 Some(ref mut cb) => cb(delta, hunk),
388 None => false,
389 }
390 });
391 if r == Some(true) {
392 0
393 } else {
394 -1
395 }
396 }
397 }
398
line_cb_c( delta: *const raw::git_diff_delta, hunk: *const raw::git_diff_hunk, line: *const raw::git_diff_line, data: *mut c_void, ) -> c_int399 pub extern "C" fn line_cb_c(
400 delta: *const raw::git_diff_delta,
401 hunk: *const raw::git_diff_hunk,
402 line: *const raw::git_diff_line,
403 data: *mut c_void,
404 ) -> c_int {
405 unsafe {
406 let delta = Binding::from_raw(delta as *mut _);
407 let hunk = Binding::from_raw_opt(hunk);
408 let line = Binding::from_raw(line);
409
410 let r = panic::wrap(|| {
411 let cbs = data as *mut DiffCallbacks<'_, '_, '_, '_, '_, '_, '_, '_>;
412 match (*cbs).line {
413 Some(ref mut cb) => cb(delta, hunk, line),
414 None => false,
415 }
416 });
417 if r == Some(true) {
418 0
419 } else {
420 -1
421 }
422 }
423 }
424
425 impl<'repo> Binding for Diff<'repo> {
426 type Raw = *mut raw::git_diff;
from_raw(raw: *mut raw::git_diff) -> Diff<'repo>427 unsafe fn from_raw(raw: *mut raw::git_diff) -> Diff<'repo> {
428 Diff {
429 raw: raw,
430 _marker: marker::PhantomData,
431 }
432 }
raw(&self) -> *mut raw::git_diff433 fn raw(&self) -> *mut raw::git_diff {
434 self.raw
435 }
436 }
437
438 impl<'repo> Drop for Diff<'repo> {
drop(&mut self)439 fn drop(&mut self) {
440 unsafe { raw::git_diff_free(self.raw) }
441 }
442 }
443
444 impl<'a> DiffDelta<'a> {
445 /// Returns the flags on the delta.
446 ///
447 /// For more information, see `DiffFlags`'s documentation.
flags(&self) -> DiffFlags448 pub fn flags(&self) -> DiffFlags {
449 let flags = unsafe { (*self.raw).flags };
450 let mut result = DiffFlags::empty();
451
452 #[cfg(target_env = "msvc")]
453 fn as_u32(flag: i32) -> u32 {
454 flag as u32
455 }
456 #[cfg(not(target_env = "msvc"))]
457 fn as_u32(flag: u32) -> u32 {
458 flag
459 }
460
461 if (flags & as_u32(raw::GIT_DIFF_FLAG_BINARY)) != 0 {
462 result |= DiffFlags::BINARY;
463 }
464 if (flags & as_u32(raw::GIT_DIFF_FLAG_NOT_BINARY)) != 0 {
465 result |= DiffFlags::NOT_BINARY;
466 }
467 if (flags & as_u32(raw::GIT_DIFF_FLAG_VALID_ID)) != 0 {
468 result |= DiffFlags::VALID_ID;
469 }
470 if (flags & as_u32(raw::GIT_DIFF_FLAG_EXISTS)) != 0 {
471 result |= DiffFlags::EXISTS;
472 }
473 result
474 }
475
476 // TODO: expose when diffs are more exposed
477 // pub fn similarity(&self) -> u16 {
478 // unsafe { (*self.raw).similarity }
479 // }
480
481 /// Returns the number of files in this delta.
nfiles(&self) -> u16482 pub fn nfiles(&self) -> u16 {
483 unsafe { (*self.raw).nfiles }
484 }
485
486 /// Returns the status of this entry
487 ///
488 /// For more information, see `Delta`'s documentation
status(&self) -> Delta489 pub fn status(&self) -> Delta {
490 match unsafe { (*self.raw).status } {
491 raw::GIT_DELTA_UNMODIFIED => Delta::Unmodified,
492 raw::GIT_DELTA_ADDED => Delta::Added,
493 raw::GIT_DELTA_DELETED => Delta::Deleted,
494 raw::GIT_DELTA_MODIFIED => Delta::Modified,
495 raw::GIT_DELTA_RENAMED => Delta::Renamed,
496 raw::GIT_DELTA_COPIED => Delta::Copied,
497 raw::GIT_DELTA_IGNORED => Delta::Ignored,
498 raw::GIT_DELTA_UNTRACKED => Delta::Untracked,
499 raw::GIT_DELTA_TYPECHANGE => Delta::Typechange,
500 raw::GIT_DELTA_UNREADABLE => Delta::Unreadable,
501 raw::GIT_DELTA_CONFLICTED => Delta::Conflicted,
502 n => panic!("unknown diff status: {}", n),
503 }
504 }
505
506 /// Return the file which represents the "from" side of the diff.
507 ///
508 /// What side this means depends on the function that was used to generate
509 /// the diff and will be documented on the function itself.
old_file(&self) -> DiffFile<'a>510 pub fn old_file(&self) -> DiffFile<'a> {
511 unsafe { Binding::from_raw(&(*self.raw).old_file as *const _) }
512 }
513
514 /// Return the file which represents the "to" side of the diff.
515 ///
516 /// What side this means depends on the function that was used to generate
517 /// the diff and will be documented on the function itself.
new_file(&self) -> DiffFile<'a>518 pub fn new_file(&self) -> DiffFile<'a> {
519 unsafe { Binding::from_raw(&(*self.raw).new_file as *const _) }
520 }
521 }
522
523 impl<'a> Binding for DiffDelta<'a> {
524 type Raw = *mut raw::git_diff_delta;
from_raw(raw: *mut raw::git_diff_delta) -> DiffDelta<'a>525 unsafe fn from_raw(raw: *mut raw::git_diff_delta) -> DiffDelta<'a> {
526 DiffDelta {
527 raw: raw,
528 _marker: marker::PhantomData,
529 }
530 }
raw(&self) -> *mut raw::git_diff_delta531 fn raw(&self) -> *mut raw::git_diff_delta {
532 self.raw
533 }
534 }
535
536 impl<'a> std::fmt::Debug for DiffDelta<'a> {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error>537 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
538 f.debug_struct("DiffDelta")
539 .field("nfiles", &self.nfiles())
540 .field("status", &self.status())
541 .field("old_file", &self.old_file())
542 .field("new_file", &self.new_file())
543 .finish()
544 }
545 }
546
547 impl<'a> DiffFile<'a> {
548 /// Returns the Oid of this item.
549 ///
550 /// If this entry represents an absent side of a diff (e.g. the `old_file`
551 /// of a `Added` delta), then the oid returned will be zeroes.
id(&self) -> Oid552 pub fn id(&self) -> Oid {
553 unsafe { Binding::from_raw(&(*self.raw).id as *const _) }
554 }
555
556 /// Returns the path, in bytes, of the entry relative to the working
557 /// directory of the repository.
path_bytes(&self) -> Option<&'a [u8]>558 pub fn path_bytes(&self) -> Option<&'a [u8]> {
559 static FOO: () = ();
560 unsafe { crate::opt_bytes(&FOO, (*self.raw).path) }
561 }
562
563 /// Returns the path of the entry relative to the working directory of the
564 /// repository.
path(&self) -> Option<&'a Path>565 pub fn path(&self) -> Option<&'a Path> {
566 self.path_bytes().map(util::bytes2path)
567 }
568
569 /// Returns the size of this entry, in bytes
size(&self) -> u64570 pub fn size(&self) -> u64 {
571 unsafe { (*self.raw).size as u64 }
572 }
573
574 /// Returns `true` if file(s) are treated as binary data.
is_binary(&self) -> bool575 pub fn is_binary(&self) -> bool {
576 unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_BINARY as u32 != 0 }
577 }
578
579 /// Returns `true` if file(s) are treated as text data.
is_not_binary(&self) -> bool580 pub fn is_not_binary(&self) -> bool {
581 unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_NOT_BINARY as u32 != 0 }
582 }
583
584 /// Returns `true` if `id` value is known correct.
is_valid_id(&self) -> bool585 pub fn is_valid_id(&self) -> bool {
586 unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_VALID_ID as u32 != 0 }
587 }
588
589 /// Returns `true` if file exists at this side of the delta.
exists(&self) -> bool590 pub fn exists(&self) -> bool {
591 unsafe { (*self.raw).flags & raw::GIT_DIFF_FLAG_EXISTS as u32 != 0 }
592 }
593
594 /// Returns file mode.
mode(&self) -> FileMode595 pub fn mode(&self) -> FileMode {
596 match unsafe { (*self.raw).mode.into() } {
597 raw::GIT_FILEMODE_UNREADABLE => FileMode::Unreadable,
598 raw::GIT_FILEMODE_TREE => FileMode::Tree,
599 raw::GIT_FILEMODE_BLOB => FileMode::Blob,
600 raw::GIT_FILEMODE_BLOB_EXECUTABLE => FileMode::BlobExecutable,
601 raw::GIT_FILEMODE_LINK => FileMode::Link,
602 raw::GIT_FILEMODE_COMMIT => FileMode::Commit,
603 mode => panic!("unknown mode: {}", mode),
604 }
605 }
606 }
607
608 impl<'a> Binding for DiffFile<'a> {
609 type Raw = *const raw::git_diff_file;
from_raw(raw: *const raw::git_diff_file) -> DiffFile<'a>610 unsafe fn from_raw(raw: *const raw::git_diff_file) -> DiffFile<'a> {
611 DiffFile {
612 raw: raw,
613 _marker: marker::PhantomData,
614 }
615 }
raw(&self) -> *const raw::git_diff_file616 fn raw(&self) -> *const raw::git_diff_file {
617 self.raw
618 }
619 }
620
621 impl<'a> std::fmt::Debug for DiffFile<'a> {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error>622 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
623 let mut ds = f.debug_struct("DiffFile");
624 ds.field("id", &self.id());
625 if let Some(path_bytes) = &self.path_bytes() {
626 ds.field("path_bytes", path_bytes);
627 }
628 if let Some(path) = &self.path() {
629 ds.field("path", path);
630 }
631 ds.field("size", &self.size()).finish()
632 }
633 }
634
635 impl Default for DiffOptions {
default() -> Self636 fn default() -> Self {
637 Self::new()
638 }
639 }
640
641 impl DiffOptions {
642 /// Creates a new set of empty diff options.
643 ///
644 /// All flags and other options are defaulted to false or their otherwise
645 /// zero equivalents.
new() -> DiffOptions646 pub fn new() -> DiffOptions {
647 let mut opts = DiffOptions {
648 pathspec: Vec::new(),
649 pathspec_ptrs: Vec::new(),
650 raw: unsafe { mem::zeroed() },
651 old_prefix: None,
652 new_prefix: None,
653 };
654 assert_eq!(unsafe { raw::git_diff_init_options(&mut opts.raw, 1) }, 0);
655 opts
656 }
657
flag(&mut self, opt: i32, val: bool) -> &mut DiffOptions658 fn flag(&mut self, opt: i32, val: bool) -> &mut DiffOptions {
659 let opt = opt as u32;
660 if val {
661 self.raw.flags |= opt;
662 } else {
663 self.raw.flags &= !opt;
664 }
665 self
666 }
667
668 /// Flag indicating whether the sides of the diff will be reversed.
reverse(&mut self, reverse: bool) -> &mut DiffOptions669 pub fn reverse(&mut self, reverse: bool) -> &mut DiffOptions {
670 self.flag(raw::GIT_DIFF_REVERSE, reverse)
671 }
672
673 /// Flag indicating whether ignored files are included.
include_ignored(&mut self, include: bool) -> &mut DiffOptions674 pub fn include_ignored(&mut self, include: bool) -> &mut DiffOptions {
675 self.flag(raw::GIT_DIFF_INCLUDE_IGNORED, include)
676 }
677
678 /// Flag indicating whether ignored directories are traversed deeply or not.
recurse_ignored_dirs(&mut self, recurse: bool) -> &mut DiffOptions679 pub fn recurse_ignored_dirs(&mut self, recurse: bool) -> &mut DiffOptions {
680 self.flag(raw::GIT_DIFF_RECURSE_IGNORED_DIRS, recurse)
681 }
682
683 /// Flag indicating whether untracked files are in the diff
include_untracked(&mut self, include: bool) -> &mut DiffOptions684 pub fn include_untracked(&mut self, include: bool) -> &mut DiffOptions {
685 self.flag(raw::GIT_DIFF_INCLUDE_UNTRACKED, include)
686 }
687
688 /// Flag indicating whether untracked directories are deeply traversed or
689 /// not.
recurse_untracked_dirs(&mut self, recurse: bool) -> &mut DiffOptions690 pub fn recurse_untracked_dirs(&mut self, recurse: bool) -> &mut DiffOptions {
691 self.flag(raw::GIT_DIFF_RECURSE_UNTRACKED_DIRS, recurse)
692 }
693
694 /// Flag indicating whether unmodified files are in the diff.
include_unmodified(&mut self, include: bool) -> &mut DiffOptions695 pub fn include_unmodified(&mut self, include: bool) -> &mut DiffOptions {
696 self.flag(raw::GIT_DIFF_INCLUDE_UNMODIFIED, include)
697 }
698
699 /// If entrabled, then Typechange delta records are generated.
include_typechange(&mut self, include: bool) -> &mut DiffOptions700 pub fn include_typechange(&mut self, include: bool) -> &mut DiffOptions {
701 self.flag(raw::GIT_DIFF_INCLUDE_TYPECHANGE, include)
702 }
703
704 /// Event with `include_typechange`, the tree treturned generally shows a
705 /// deleted blow. This flag correctly labels the tree transitions as a
706 /// typechange record with the `new_file`'s mode set to tree.
707 ///
708 /// Note that the tree SHA will not be available.
include_typechange_trees(&mut self, include: bool) -> &mut DiffOptions709 pub fn include_typechange_trees(&mut self, include: bool) -> &mut DiffOptions {
710 self.flag(raw::GIT_DIFF_INCLUDE_TYPECHANGE_TREES, include)
711 }
712
713 /// Flag indicating whether file mode changes are ignored.
ignore_filemode(&mut self, ignore: bool) -> &mut DiffOptions714 pub fn ignore_filemode(&mut self, ignore: bool) -> &mut DiffOptions {
715 self.flag(raw::GIT_DIFF_IGNORE_FILEMODE, ignore)
716 }
717
718 /// Flag indicating whether all submodules should be treated as unmodified.
ignore_submodules(&mut self, ignore: bool) -> &mut DiffOptions719 pub fn ignore_submodules(&mut self, ignore: bool) -> &mut DiffOptions {
720 self.flag(raw::GIT_DIFF_IGNORE_SUBMODULES, ignore)
721 }
722
723 /// Flag indicating whether case insensitive filenames should be used.
ignore_case(&mut self, ignore: bool) -> &mut DiffOptions724 pub fn ignore_case(&mut self, ignore: bool) -> &mut DiffOptions {
725 self.flag(raw::GIT_DIFF_IGNORE_CASE, ignore)
726 }
727
728 /// If pathspecs are specified, this flag means that they should be applied
729 /// as an exact match instead of a fnmatch pattern.
disable_pathspec_match(&mut self, disable: bool) -> &mut DiffOptions730 pub fn disable_pathspec_match(&mut self, disable: bool) -> &mut DiffOptions {
731 self.flag(raw::GIT_DIFF_DISABLE_PATHSPEC_MATCH, disable)
732 }
733
734 /// Disable updating the `binary` flag in delta records. This is useful when
735 /// iterating over a diff if you don't need hunk and data callbacks and want
736 /// to avoid having to load a file completely.
skip_binary_check(&mut self, skip: bool) -> &mut DiffOptions737 pub fn skip_binary_check(&mut self, skip: bool) -> &mut DiffOptions {
738 self.flag(raw::GIT_DIFF_SKIP_BINARY_CHECK, skip)
739 }
740
741 /// When diff finds an untracked directory, to match the behavior of core
742 /// Git, it scans the contents for ignored and untracked files. If all
743 /// contents are ignored, then the directory is ignored; if any contents are
744 /// not ignored, then the directory is untracked. This is extra work that
745 /// may not matter in many cases.
746 ///
747 /// This flag turns off that scan and immediately labels an untracked
748 /// directory as untracked (changing the behavior to not match core git).
enable_fast_untracked_dirs(&mut self, enable: bool) -> &mut DiffOptions749 pub fn enable_fast_untracked_dirs(&mut self, enable: bool) -> &mut DiffOptions {
750 self.flag(raw::GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS, enable)
751 }
752
753 /// When diff finds a file in the working directory with stat information
754 /// different from the index, but the OID ends up being the same, write the
755 /// correct stat information into the index. Note: without this flag, diff
756 /// will always leave the index untouched.
update_index(&mut self, update: bool) -> &mut DiffOptions757 pub fn update_index(&mut self, update: bool) -> &mut DiffOptions {
758 self.flag(raw::GIT_DIFF_UPDATE_INDEX, update)
759 }
760
761 /// Include unreadable files in the diff
include_unreadable(&mut self, include: bool) -> &mut DiffOptions762 pub fn include_unreadable(&mut self, include: bool) -> &mut DiffOptions {
763 self.flag(raw::GIT_DIFF_INCLUDE_UNREADABLE, include)
764 }
765
766 /// Include unreadable files in the diff
include_unreadable_as_untracked(&mut self, include: bool) -> &mut DiffOptions767 pub fn include_unreadable_as_untracked(&mut self, include: bool) -> &mut DiffOptions {
768 self.flag(raw::GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED, include)
769 }
770
771 /// Treat all files as text, disabling binary attributes and detection.
force_text(&mut self, force: bool) -> &mut DiffOptions772 pub fn force_text(&mut self, force: bool) -> &mut DiffOptions {
773 self.flag(raw::GIT_DIFF_FORCE_TEXT, force)
774 }
775
776 /// Treat all files as binary, disabling text diffs
force_binary(&mut self, force: bool) -> &mut DiffOptions777 pub fn force_binary(&mut self, force: bool) -> &mut DiffOptions {
778 self.flag(raw::GIT_DIFF_FORCE_TEXT, force)
779 }
780
781 /// Ignore all whitespace
ignore_whitespace(&mut self, ignore: bool) -> &mut DiffOptions782 pub fn ignore_whitespace(&mut self, ignore: bool) -> &mut DiffOptions {
783 self.flag(raw::GIT_DIFF_IGNORE_WHITESPACE, ignore)
784 }
785
786 /// Ignore changes in the amount of whitespace
ignore_whitespace_change(&mut self, ignore: bool) -> &mut DiffOptions787 pub fn ignore_whitespace_change(&mut self, ignore: bool) -> &mut DiffOptions {
788 self.flag(raw::GIT_DIFF_IGNORE_WHITESPACE_CHANGE, ignore)
789 }
790
791 /// Ignore whitespace at the end of line
ignore_whitespace_eol(&mut self, ignore: bool) -> &mut DiffOptions792 pub fn ignore_whitespace_eol(&mut self, ignore: bool) -> &mut DiffOptions {
793 self.flag(raw::GIT_DIFF_IGNORE_WHITESPACE_EOL, ignore)
794 }
795
796 /// When generating patch text, include the content of untracked files.
797 ///
798 /// This automatically turns on `include_untracked` but it does not turn on
799 /// `recurse_untracked_dirs`. Add that flag if you want the content of every
800 /// single untracked file.
show_untracked_content(&mut self, show: bool) -> &mut DiffOptions801 pub fn show_untracked_content(&mut self, show: bool) -> &mut DiffOptions {
802 self.flag(raw::GIT_DIFF_SHOW_UNTRACKED_CONTENT, show)
803 }
804
805 /// When generating output, include the names of unmodified files if they
806 /// are included in the `Diff`. Normally these are skipped in the formats
807 /// that list files (e.g. name-only, name-status, raw). Even with this these
808 /// will not be included in the patch format.
show_unmodified(&mut self, show: bool) -> &mut DiffOptions809 pub fn show_unmodified(&mut self, show: bool) -> &mut DiffOptions {
810 self.flag(raw::GIT_DIFF_SHOW_UNMODIFIED, show)
811 }
812
813 /// Use the "patience diff" algorithm
patience(&mut self, patience: bool) -> &mut DiffOptions814 pub fn patience(&mut self, patience: bool) -> &mut DiffOptions {
815 self.flag(raw::GIT_DIFF_PATIENCE, patience)
816 }
817
818 /// Take extra time to find the minimal diff
minimal(&mut self, minimal: bool) -> &mut DiffOptions819 pub fn minimal(&mut self, minimal: bool) -> &mut DiffOptions {
820 self.flag(raw::GIT_DIFF_MINIMAL, minimal)
821 }
822
823 /// Include the necessary deflate/delta information so that `git-apply` can
824 /// apply given diff information to binary files.
show_binary(&mut self, show: bool) -> &mut DiffOptions825 pub fn show_binary(&mut self, show: bool) -> &mut DiffOptions {
826 self.flag(raw::GIT_DIFF_SHOW_BINARY, show)
827 }
828
829 /// Use a heuristic that takes indentation and whitespace into account
830 /// which generally can produce better diffs when dealing with ambiguous
831 /// diff hunks.
indent_heuristic(&mut self, heuristic: bool) -> &mut DiffOptions832 pub fn indent_heuristic(&mut self, heuristic: bool) -> &mut DiffOptions {
833 self.flag(raw::GIT_DIFF_INDENT_HEURISTIC, heuristic)
834 }
835
836 /// Set the number of unchanged lines that define the boundary of a hunk
837 /// (and to display before and after).
838 ///
839 /// The default value for this is 3.
context_lines(&mut self, lines: u32) -> &mut DiffOptions840 pub fn context_lines(&mut self, lines: u32) -> &mut DiffOptions {
841 self.raw.context_lines = lines;
842 self
843 }
844
845 /// Set the maximum number of unchanged lines between hunk boundaries before
846 /// the hunks will be merged into one.
847 ///
848 /// The default value for this is 0.
interhunk_lines(&mut self, lines: u32) -> &mut DiffOptions849 pub fn interhunk_lines(&mut self, lines: u32) -> &mut DiffOptions {
850 self.raw.interhunk_lines = lines;
851 self
852 }
853
854 /// The default value for this is `core.abbrev` or 7 if unset.
id_abbrev(&mut self, abbrev: u16) -> &mut DiffOptions855 pub fn id_abbrev(&mut self, abbrev: u16) -> &mut DiffOptions {
856 self.raw.id_abbrev = abbrev;
857 self
858 }
859
860 /// Maximum size (in bytes) above which a blob will be marked as binary
861 /// automatically.
862 ///
863 /// A negative value will disable this entirely.
864 ///
865 /// The default value for this is 512MB.
max_size(&mut self, size: i64) -> &mut DiffOptions866 pub fn max_size(&mut self, size: i64) -> &mut DiffOptions {
867 self.raw.max_size = size as raw::git_off_t;
868 self
869 }
870
871 /// The virtual "directory" to prefix old file names with in hunk headers.
872 ///
873 /// The default value for this is "a".
old_prefix<T: IntoCString>(&mut self, t: T) -> &mut DiffOptions874 pub fn old_prefix<T: IntoCString>(&mut self, t: T) -> &mut DiffOptions {
875 self.old_prefix = Some(t.into_c_string().unwrap());
876 self
877 }
878
879 /// The virtual "directory" to prefix new file names with in hunk headers.
880 ///
881 /// The default value for this is "b".
new_prefix<T: IntoCString>(&mut self, t: T) -> &mut DiffOptions882 pub fn new_prefix<T: IntoCString>(&mut self, t: T) -> &mut DiffOptions {
883 self.new_prefix = Some(t.into_c_string().unwrap());
884 self
885 }
886
887 /// Add to the array of paths/fnmatch patterns to constrain the diff.
pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut DiffOptions888 pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut DiffOptions {
889 let s = util::cstring_to_repo_path(pathspec).unwrap();
890 self.pathspec_ptrs.push(s.as_ptr());
891 self.pathspec.push(s);
892 self
893 }
894
895 /// Acquire a pointer to the underlying raw options.
896 ///
897 /// This function is unsafe as the pointer is only valid so long as this
898 /// structure is not moved, modified, or used elsewhere.
raw(&mut self) -> *const raw::git_diff_options899 pub unsafe fn raw(&mut self) -> *const raw::git_diff_options {
900 self.raw.old_prefix = self
901 .old_prefix
902 .as_ref()
903 .map(|s| s.as_ptr())
904 .unwrap_or(ptr::null());
905 self.raw.new_prefix = self
906 .new_prefix
907 .as_ref()
908 .map(|s| s.as_ptr())
909 .unwrap_or(ptr::null());
910 self.raw.pathspec.count = self.pathspec_ptrs.len() as size_t;
911 self.raw.pathspec.strings = self.pathspec_ptrs.as_ptr() as *mut _;
912 &self.raw as *const _
913 }
914
915 // TODO: expose ignore_submodules, notify_cb/notify_payload
916 }
917
918 impl<'diff> Iterator for Deltas<'diff> {
919 type Item = DiffDelta<'diff>;
next(&mut self) -> Option<DiffDelta<'diff>>920 fn next(&mut self) -> Option<DiffDelta<'diff>> {
921 self.range.next().and_then(|i| self.diff.get_delta(i))
922 }
size_hint(&self) -> (usize, Option<usize>)923 fn size_hint(&self) -> (usize, Option<usize>) {
924 self.range.size_hint()
925 }
926 }
927 impl<'diff> DoubleEndedIterator for Deltas<'diff> {
next_back(&mut self) -> Option<DiffDelta<'diff>>928 fn next_back(&mut self) -> Option<DiffDelta<'diff>> {
929 self.range.next_back().and_then(|i| self.diff.get_delta(i))
930 }
931 }
932 impl<'diff> ExactSizeIterator for Deltas<'diff> {}
933
934 impl<'a> DiffLine<'a> {
935 /// Line number in old file or `None` for added line
old_lineno(&self) -> Option<u32>936 pub fn old_lineno(&self) -> Option<u32> {
937 match unsafe { (*self.raw).old_lineno } {
938 n if n < 0 => None,
939 n => Some(n as u32),
940 }
941 }
942
943 /// Line number in new file or `None` for deleted line
new_lineno(&self) -> Option<u32>944 pub fn new_lineno(&self) -> Option<u32> {
945 match unsafe { (*self.raw).new_lineno } {
946 n if n < 0 => None,
947 n => Some(n as u32),
948 }
949 }
950
951 /// Number of newline characters in content
num_lines(&self) -> u32952 pub fn num_lines(&self) -> u32 {
953 unsafe { (*self.raw).num_lines as u32 }
954 }
955
956 /// Offset in the original file to the content
content_offset(&self) -> i64957 pub fn content_offset(&self) -> i64 {
958 unsafe { (*self.raw).content_offset as i64 }
959 }
960
961 /// Content of this line as bytes.
content(&self) -> &'a [u8]962 pub fn content(&self) -> &'a [u8] {
963 unsafe {
964 slice::from_raw_parts(
965 (*self.raw).content as *const u8,
966 (*self.raw).content_len as usize,
967 )
968 }
969 }
970
971 /// Sigil showing the origin of this `DiffLine`.
972 ///
973 /// * ` ` - Line context
974 /// * `+` - Line addition
975 /// * `-` - Line deletion
976 /// * `=` - Context (End of file)
977 /// * `>` - Add (End of file)
978 /// * `<` - Remove (End of file)
979 /// * `F` - File header
980 /// * `H` - Hunk header
981 /// * `B` - Line binary
origin(&self) -> char982 pub fn origin(&self) -> char {
983 match unsafe { (*self.raw).origin as raw::git_diff_line_t } {
984 raw::GIT_DIFF_LINE_CONTEXT => ' ',
985 raw::GIT_DIFF_LINE_ADDITION => '+',
986 raw::GIT_DIFF_LINE_DELETION => '-',
987 raw::GIT_DIFF_LINE_CONTEXT_EOFNL => '=',
988 raw::GIT_DIFF_LINE_ADD_EOFNL => '>',
989 raw::GIT_DIFF_LINE_DEL_EOFNL => '<',
990 raw::GIT_DIFF_LINE_FILE_HDR => 'F',
991 raw::GIT_DIFF_LINE_HUNK_HDR => 'H',
992 raw::GIT_DIFF_LINE_BINARY => 'B',
993 _ => ' ',
994 }
995 }
996 }
997
998 impl<'a> Binding for DiffLine<'a> {
999 type Raw = *const raw::git_diff_line;
from_raw(raw: *const raw::git_diff_line) -> DiffLine<'a>1000 unsafe fn from_raw(raw: *const raw::git_diff_line) -> DiffLine<'a> {
1001 DiffLine {
1002 raw: raw,
1003 _marker: marker::PhantomData,
1004 }
1005 }
raw(&self) -> *const raw::git_diff_line1006 fn raw(&self) -> *const raw::git_diff_line {
1007 self.raw
1008 }
1009 }
1010
1011 impl<'a> std::fmt::Debug for DiffLine<'a> {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error>1012 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
1013 let mut ds = f.debug_struct("DiffLine");
1014 if let Some(old_lineno) = &self.old_lineno() {
1015 ds.field("old_lineno", old_lineno);
1016 }
1017 if let Some(new_lineno) = &self.new_lineno() {
1018 ds.field("new_lineno", new_lineno);
1019 }
1020 ds.field("num_lines", &self.num_lines())
1021 .field("content_offset", &self.content_offset())
1022 .field("content", &self.content())
1023 .field("origin", &self.origin())
1024 .finish()
1025 }
1026 }
1027
1028 impl<'a> DiffHunk<'a> {
1029 /// Starting line number in old_file
old_start(&self) -> u321030 pub fn old_start(&self) -> u32 {
1031 unsafe { (*self.raw).old_start as u32 }
1032 }
1033
1034 /// Number of lines in old_file
old_lines(&self) -> u321035 pub fn old_lines(&self) -> u32 {
1036 unsafe { (*self.raw).old_lines as u32 }
1037 }
1038
1039 /// Starting line number in new_file
new_start(&self) -> u321040 pub fn new_start(&self) -> u32 {
1041 unsafe { (*self.raw).new_start as u32 }
1042 }
1043
1044 /// Number of lines in new_file
new_lines(&self) -> u321045 pub fn new_lines(&self) -> u32 {
1046 unsafe { (*self.raw).new_lines as u32 }
1047 }
1048
1049 /// Header text
header(&self) -> &'a [u8]1050 pub fn header(&self) -> &'a [u8] {
1051 unsafe {
1052 slice::from_raw_parts(
1053 (*self.raw).header.as_ptr() as *const u8,
1054 (*self.raw).header_len as usize,
1055 )
1056 }
1057 }
1058 }
1059
1060 impl<'a> Binding for DiffHunk<'a> {
1061 type Raw = *const raw::git_diff_hunk;
from_raw(raw: *const raw::git_diff_hunk) -> DiffHunk<'a>1062 unsafe fn from_raw(raw: *const raw::git_diff_hunk) -> DiffHunk<'a> {
1063 DiffHunk {
1064 raw: raw,
1065 _marker: marker::PhantomData,
1066 }
1067 }
raw(&self) -> *const raw::git_diff_hunk1068 fn raw(&self) -> *const raw::git_diff_hunk {
1069 self.raw
1070 }
1071 }
1072
1073 impl<'a> std::fmt::Debug for DiffHunk<'a> {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error>1074 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
1075 f.debug_struct("DiffHunk")
1076 .field("old_start", &self.old_start())
1077 .field("old_lines", &self.old_lines())
1078 .field("new_start", &self.new_start())
1079 .field("new_lines", &self.new_lines())
1080 .field("header", &self.header())
1081 .finish()
1082 }
1083 }
1084
1085 impl DiffStats {
1086 /// Get the total number of files chaned in a diff.
files_changed(&self) -> usize1087 pub fn files_changed(&self) -> usize {
1088 unsafe { raw::git_diff_stats_files_changed(&*self.raw) as usize }
1089 }
1090
1091 /// Get the total number of insertions in a diff
insertions(&self) -> usize1092 pub fn insertions(&self) -> usize {
1093 unsafe { raw::git_diff_stats_insertions(&*self.raw) as usize }
1094 }
1095
1096 /// Get the total number of deletions in a diff
deletions(&self) -> usize1097 pub fn deletions(&self) -> usize {
1098 unsafe { raw::git_diff_stats_deletions(&*self.raw) as usize }
1099 }
1100
1101 /// Print diff statistics to a Buf
to_buf(&self, format: DiffStatsFormat, width: usize) -> Result<Buf, Error>1102 pub fn to_buf(&self, format: DiffStatsFormat, width: usize) -> Result<Buf, Error> {
1103 let buf = Buf::new();
1104 unsafe {
1105 try_call!(raw::git_diff_stats_to_buf(
1106 buf.raw(),
1107 self.raw,
1108 format.bits(),
1109 width as size_t
1110 ));
1111 }
1112 Ok(buf)
1113 }
1114 }
1115
1116 impl Binding for DiffStats {
1117 type Raw = *mut raw::git_diff_stats;
1118
from_raw(raw: *mut raw::git_diff_stats) -> DiffStats1119 unsafe fn from_raw(raw: *mut raw::git_diff_stats) -> DiffStats {
1120 DiffStats { raw: raw }
1121 }
raw(&self) -> *mut raw::git_diff_stats1122 fn raw(&self) -> *mut raw::git_diff_stats {
1123 self.raw
1124 }
1125 }
1126
1127 impl Drop for DiffStats {
drop(&mut self)1128 fn drop(&mut self) {
1129 unsafe { raw::git_diff_stats_free(self.raw) }
1130 }
1131 }
1132
1133 impl std::fmt::Debug for DiffStats {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error>1134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
1135 f.debug_struct("DiffStats")
1136 .field("files_changed", &self.files_changed())
1137 .field("insertions", &self.insertions())
1138 .field("deletions", &self.deletions())
1139 .finish()
1140 }
1141 }
1142
1143 impl<'a> DiffBinary<'a> {
1144 /// Returns whether there is data in this binary structure or not.
1145 ///
1146 /// If this is `true`, then this was produced and included binary content.
1147 /// If this is `false` then this was generated knowing only that a binary
1148 /// file changed but without providing the data, probably from a patch that
1149 /// said `Binary files a/file.txt and b/file.txt differ`.
contains_data(&self) -> bool1150 pub fn contains_data(&self) -> bool {
1151 unsafe { (*self.raw).contains_data == 1 }
1152 }
1153
1154 /// The contents of the old file.
old_file(&self) -> DiffBinaryFile<'a>1155 pub fn old_file(&self) -> DiffBinaryFile<'a> {
1156 unsafe { Binding::from_raw(&(*self.raw).old_file as *const _) }
1157 }
1158
1159 /// The contents of the new file.
new_file(&self) -> DiffBinaryFile<'a>1160 pub fn new_file(&self) -> DiffBinaryFile<'a> {
1161 unsafe { Binding::from_raw(&(*self.raw).new_file as *const _) }
1162 }
1163 }
1164
1165 impl<'a> Binding for DiffBinary<'a> {
1166 type Raw = *const raw::git_diff_binary;
from_raw(raw: *const raw::git_diff_binary) -> DiffBinary<'a>1167 unsafe fn from_raw(raw: *const raw::git_diff_binary) -> DiffBinary<'a> {
1168 DiffBinary {
1169 raw: raw,
1170 _marker: marker::PhantomData,
1171 }
1172 }
raw(&self) -> *const raw::git_diff_binary1173 fn raw(&self) -> *const raw::git_diff_binary {
1174 self.raw
1175 }
1176 }
1177
1178 impl<'a> DiffBinaryFile<'a> {
1179 /// The type of binary data for this file
kind(&self) -> DiffBinaryKind1180 pub fn kind(&self) -> DiffBinaryKind {
1181 unsafe { Binding::from_raw((*self.raw).kind) }
1182 }
1183
1184 /// The binary data, deflated
data(&self) -> &[u8]1185 pub fn data(&self) -> &[u8] {
1186 unsafe {
1187 slice::from_raw_parts((*self.raw).data as *const u8, (*self.raw).datalen as usize)
1188 }
1189 }
1190
1191 /// The length of the binary data after inflation
inflated_len(&self) -> usize1192 pub fn inflated_len(&self) -> usize {
1193 unsafe { (*self.raw).inflatedlen as usize }
1194 }
1195 }
1196
1197 impl<'a> Binding for DiffBinaryFile<'a> {
1198 type Raw = *const raw::git_diff_binary_file;
from_raw(raw: *const raw::git_diff_binary_file) -> DiffBinaryFile<'a>1199 unsafe fn from_raw(raw: *const raw::git_diff_binary_file) -> DiffBinaryFile<'a> {
1200 DiffBinaryFile {
1201 raw: raw,
1202 _marker: marker::PhantomData,
1203 }
1204 }
raw(&self) -> *const raw::git_diff_binary_file1205 fn raw(&self) -> *const raw::git_diff_binary_file {
1206 self.raw
1207 }
1208 }
1209
1210 impl Binding for DiffBinaryKind {
1211 type Raw = raw::git_diff_binary_t;
from_raw(raw: raw::git_diff_binary_t) -> DiffBinaryKind1212 unsafe fn from_raw(raw: raw::git_diff_binary_t) -> DiffBinaryKind {
1213 match raw {
1214 raw::GIT_DIFF_BINARY_NONE => DiffBinaryKind::None,
1215 raw::GIT_DIFF_BINARY_LITERAL => DiffBinaryKind::Literal,
1216 raw::GIT_DIFF_BINARY_DELTA => DiffBinaryKind::Delta,
1217 _ => panic!("Unknown git diff binary kind"),
1218 }
1219 }
raw(&self) -> raw::git_diff_binary_t1220 fn raw(&self) -> raw::git_diff_binary_t {
1221 match *self {
1222 DiffBinaryKind::None => raw::GIT_DIFF_BINARY_NONE,
1223 DiffBinaryKind::Literal => raw::GIT_DIFF_BINARY_LITERAL,
1224 DiffBinaryKind::Delta => raw::GIT_DIFF_BINARY_DELTA,
1225 }
1226 }
1227 }
1228
1229 impl Default for DiffFindOptions {
default() -> Self1230 fn default() -> Self {
1231 Self::new()
1232 }
1233 }
1234
1235 impl DiffFindOptions {
1236 /// Creates a new set of empty diff find options.
1237 ///
1238 /// All flags and other options are defaulted to false or their otherwise
1239 /// zero equivalents.
new() -> DiffFindOptions1240 pub fn new() -> DiffFindOptions {
1241 let mut opts = DiffFindOptions {
1242 raw: unsafe { mem::zeroed() },
1243 };
1244 assert_eq!(
1245 unsafe { raw::git_diff_find_init_options(&mut opts.raw, 1) },
1246 0
1247 );
1248 opts
1249 }
1250
flag(&mut self, opt: u32, val: bool) -> &mut DiffFindOptions1251 fn flag(&mut self, opt: u32, val: bool) -> &mut DiffFindOptions {
1252 if val {
1253 self.raw.flags |= opt;
1254 } else {
1255 self.raw.flags &= !opt;
1256 }
1257 self
1258 }
1259
1260 /// Reset all flags back to their unset state, indicating that
1261 /// `diff.renames` should be used instead. This is overridden once any flag
1262 /// is set.
by_config(&mut self) -> &mut DiffFindOptions1263 pub fn by_config(&mut self) -> &mut DiffFindOptions {
1264 self.flag(0xffffffff, false)
1265 }
1266
1267 /// Look for renames?
renames(&mut self, find: bool) -> &mut DiffFindOptions1268 pub fn renames(&mut self, find: bool) -> &mut DiffFindOptions {
1269 self.flag(raw::GIT_DIFF_FIND_RENAMES, find)
1270 }
1271
1272 /// Consider old side of modified for renames?
renames_from_rewrites(&mut self, find: bool) -> &mut DiffFindOptions1273 pub fn renames_from_rewrites(&mut self, find: bool) -> &mut DiffFindOptions {
1274 self.flag(raw::GIT_DIFF_FIND_RENAMES_FROM_REWRITES, find)
1275 }
1276
1277 /// Look for copies?
copies(&mut self, find: bool) -> &mut DiffFindOptions1278 pub fn copies(&mut self, find: bool) -> &mut DiffFindOptions {
1279 self.flag(raw::GIT_DIFF_FIND_COPIES, find)
1280 }
1281
1282 /// Consider unmodified as copy sources?
1283 ///
1284 /// For this to work correctly, use `include_unmodified` when the initial
1285 /// diff is being generated.
copies_from_unmodified(&mut self, find: bool) -> &mut DiffFindOptions1286 pub fn copies_from_unmodified(&mut self, find: bool) -> &mut DiffFindOptions {
1287 self.flag(raw::GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED, find)
1288 }
1289
1290 /// Mark significant rewrites for split.
rewrites(&mut self, find: bool) -> &mut DiffFindOptions1291 pub fn rewrites(&mut self, find: bool) -> &mut DiffFindOptions {
1292 self.flag(raw::GIT_DIFF_FIND_REWRITES, find)
1293 }
1294
1295 /// Actually split large rewrites into delete/add pairs
break_rewrites(&mut self, find: bool) -> &mut DiffFindOptions1296 pub fn break_rewrites(&mut self, find: bool) -> &mut DiffFindOptions {
1297 self.flag(raw::GIT_DIFF_BREAK_REWRITES, find)
1298 }
1299
1300 #[doc(hidden)]
break_rewries(&mut self, find: bool) -> &mut DiffFindOptions1301 pub fn break_rewries(&mut self, find: bool) -> &mut DiffFindOptions {
1302 self.break_rewrites(find)
1303 }
1304
1305 /// Find renames/copies for untracked items in working directory.
1306 ///
1307 /// For this to work correctly use the `include_untracked` option when the
1308 /// initial diff is being generated.
for_untracked(&mut self, find: bool) -> &mut DiffFindOptions1309 pub fn for_untracked(&mut self, find: bool) -> &mut DiffFindOptions {
1310 self.flag(raw::GIT_DIFF_FIND_FOR_UNTRACKED, find)
1311 }
1312
1313 /// Turn on all finding features.
all(&mut self, find: bool) -> &mut DiffFindOptions1314 pub fn all(&mut self, find: bool) -> &mut DiffFindOptions {
1315 self.flag(raw::GIT_DIFF_FIND_ALL, find)
1316 }
1317
1318 /// Measure similarity ignoring leading whitespace (default)
ignore_leading_whitespace(&mut self, ignore: bool) -> &mut DiffFindOptions1319 pub fn ignore_leading_whitespace(&mut self, ignore: bool) -> &mut DiffFindOptions {
1320 self.flag(raw::GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE, ignore)
1321 }
1322
1323 /// Measure similarity ignoring all whitespace
ignore_whitespace(&mut self, ignore: bool) -> &mut DiffFindOptions1324 pub fn ignore_whitespace(&mut self, ignore: bool) -> &mut DiffFindOptions {
1325 self.flag(raw::GIT_DIFF_FIND_IGNORE_WHITESPACE, ignore)
1326 }
1327
1328 /// Measure similarity including all data
dont_ignore_whitespace(&mut self, dont: bool) -> &mut DiffFindOptions1329 pub fn dont_ignore_whitespace(&mut self, dont: bool) -> &mut DiffFindOptions {
1330 self.flag(raw::GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE, dont)
1331 }
1332
1333 /// Measure similarity only by comparing SHAs (fast and cheap)
exact_match_only(&mut self, exact: bool) -> &mut DiffFindOptions1334 pub fn exact_match_only(&mut self, exact: bool) -> &mut DiffFindOptions {
1335 self.flag(raw::GIT_DIFF_FIND_EXACT_MATCH_ONLY, exact)
1336 }
1337
1338 /// Do not break rewrites unless they contribute to a rename.
1339 ///
1340 /// Normally, `break_rewrites` and `rewrites` will measure the
1341 /// self-similarity of modified files and split the ones that have changed a
1342 /// lot into a delete/add pair. Then the sides of that pair will be
1343 /// considered candidates for rename and copy detection
1344 ///
1345 /// If you add this flag in and the split pair is not used for an actual
1346 /// rename or copy, then the modified record will be restored to a regular
1347 /// modified record instead of being split.
break_rewrites_for_renames_only(&mut self, b: bool) -> &mut DiffFindOptions1348 pub fn break_rewrites_for_renames_only(&mut self, b: bool) -> &mut DiffFindOptions {
1349 self.flag(raw::GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY, b)
1350 }
1351
1352 /// Remove any unmodified deltas after find_similar is done.
1353 ///
1354 /// Using `copies_from_unmodified` to emulate the `--find-copies-harder`
1355 /// behavior requires building a diff with the `include_unmodified` flag. If
1356 /// you do not want unmodified records in the final result, pas this flag to
1357 /// have them removed.
remove_unmodified(&mut self, remove: bool) -> &mut DiffFindOptions1358 pub fn remove_unmodified(&mut self, remove: bool) -> &mut DiffFindOptions {
1359 self.flag(raw::GIT_DIFF_FIND_REMOVE_UNMODIFIED, remove)
1360 }
1361
1362 /// Similarity to consider a file renamed (default 50)
rename_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions1363 pub fn rename_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions {
1364 self.raw.rename_threshold = thresh;
1365 self
1366 }
1367
1368 /// Similarity of modified to be glegible rename source (default 50)
rename_from_rewrite_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions1369 pub fn rename_from_rewrite_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions {
1370 self.raw.rename_from_rewrite_threshold = thresh;
1371 self
1372 }
1373
1374 /// Similarity to consider a file copy (default 50)
copy_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions1375 pub fn copy_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions {
1376 self.raw.copy_threshold = thresh;
1377 self
1378 }
1379
1380 /// Similarity to split modify into delete/add pair (default 60)
break_rewrite_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions1381 pub fn break_rewrite_threshold(&mut self, thresh: u16) -> &mut DiffFindOptions {
1382 self.raw.break_rewrite_threshold = thresh;
1383 self
1384 }
1385
1386 /// Maximum similarity sources to examine for a file (somewhat like
1387 /// git-diff's `-l` option or `diff.renameLimit` config)
1388 ///
1389 /// Defaults to 200
rename_limit(&mut self, limit: usize) -> &mut DiffFindOptions1390 pub fn rename_limit(&mut self, limit: usize) -> &mut DiffFindOptions {
1391 self.raw.rename_limit = limit as size_t;
1392 self
1393 }
1394
1395 // TODO: expose git_diff_similarity_metric
1396 }
1397
1398 impl Default for DiffFormatEmailOptions {
default() -> Self1399 fn default() -> Self {
1400 Self::new()
1401 }
1402 }
1403
1404 impl DiffFormatEmailOptions {
1405 /// Creates a new set of email options,
1406 /// initialized to the default values
new() -> Self1407 pub fn new() -> Self {
1408 let mut opts = DiffFormatEmailOptions {
1409 raw: unsafe { mem::zeroed() },
1410 };
1411 assert_eq!(
1412 unsafe { raw::git_diff_format_email_options_init(&mut opts.raw, 1) },
1413 0
1414 );
1415 opts
1416 }
1417
flag(&mut self, opt: u32, val: bool) -> &mut Self1418 fn flag(&mut self, opt: u32, val: bool) -> &mut Self {
1419 if val {
1420 self.raw.flags |= opt;
1421 } else {
1422 self.raw.flags &= !opt;
1423 }
1424 self
1425 }
1426
1427 /// Exclude `[PATCH]` from the subject header
exclude_subject_patch_header(&mut self, should_exclude: bool) -> &mut Self1428 pub fn exclude_subject_patch_header(&mut self, should_exclude: bool) -> &mut Self {
1429 self.flag(
1430 raw::GIT_DIFF_FORMAT_EMAIL_EXCLUDE_SUBJECT_PATCH_MARKER,
1431 should_exclude,
1432 )
1433 }
1434 }
1435
1436 #[cfg(test)]
1437 mod tests {
1438 use crate::{DiffOptions, Signature, Time};
1439 use std::borrow::Borrow;
1440 use std::fs::File;
1441 use std::io::Write;
1442 use std::path::Path;
1443
1444 #[test]
smoke()1445 fn smoke() {
1446 let (_td, repo) = crate::test::repo_init();
1447 let diff = repo.diff_tree_to_workdir(None, None).unwrap();
1448 assert_eq!(diff.deltas().len(), 0);
1449 let stats = diff.stats().unwrap();
1450 assert_eq!(stats.insertions(), 0);
1451 assert_eq!(stats.deletions(), 0);
1452 assert_eq!(stats.files_changed(), 0);
1453 }
1454
1455 #[test]
foreach_smoke()1456 fn foreach_smoke() {
1457 let (_td, repo) = crate::test::repo_init();
1458 let diff = t!(repo.diff_tree_to_workdir(None, None));
1459 let mut count = 0;
1460 t!(diff.foreach(
1461 &mut |_file, _progress| {
1462 count = count + 1;
1463 true
1464 },
1465 None,
1466 None,
1467 None
1468 ));
1469 assert_eq!(count, 0);
1470 }
1471
1472 #[test]
foreach_file_only()1473 fn foreach_file_only() {
1474 let path = Path::new("foo");
1475 let (td, repo) = crate::test::repo_init();
1476 t!(t!(File::create(&td.path().join(path))).write_all(b"bar"));
1477 let mut opts = DiffOptions::new();
1478 opts.include_untracked(true);
1479 let diff = t!(repo.diff_tree_to_workdir(None, Some(&mut opts)));
1480 let mut count = 0;
1481 let mut result = None;
1482 t!(diff.foreach(
1483 &mut |file, _progress| {
1484 count = count + 1;
1485 result = file.new_file().path().map(ToOwned::to_owned);
1486 true
1487 },
1488 None,
1489 None,
1490 None
1491 ));
1492 assert_eq!(result.as_ref().map(Borrow::borrow), Some(path));
1493 assert_eq!(count, 1);
1494 }
1495
1496 #[test]
foreach_file_and_hunk()1497 fn foreach_file_and_hunk() {
1498 let path = Path::new("foo");
1499 let (td, repo) = crate::test::repo_init();
1500 t!(t!(File::create(&td.path().join(path))).write_all(b"bar"));
1501 let mut index = t!(repo.index());
1502 t!(index.add_path(path));
1503 let mut opts = DiffOptions::new();
1504 opts.include_untracked(true);
1505 let diff = t!(repo.diff_tree_to_index(None, Some(&index), Some(&mut opts)));
1506 let mut new_lines = 0;
1507 t!(diff.foreach(
1508 &mut |_file, _progress| { true },
1509 None,
1510 Some(&mut |_file, hunk| {
1511 new_lines = hunk.new_lines();
1512 true
1513 }),
1514 None
1515 ));
1516 assert_eq!(new_lines, 1);
1517 }
1518
1519 #[test]
foreach_all_callbacks()1520 fn foreach_all_callbacks() {
1521 let fib = vec![0, 1, 1, 2, 3, 5, 8];
1522 // Verified with a node implementation of deflate, might be worth
1523 // adding a deflate lib to do this inline here.
1524 let deflated_fib = vec![120, 156, 99, 96, 100, 100, 98, 102, 229, 0, 0, 0, 53, 0, 21];
1525 let foo_path = Path::new("foo");
1526 let bin_path = Path::new("bin");
1527 let (td, repo) = crate::test::repo_init();
1528 t!(t!(File::create(&td.path().join(foo_path))).write_all(b"bar\n"));
1529 t!(t!(File::create(&td.path().join(bin_path))).write_all(&fib));
1530 let mut index = t!(repo.index());
1531 t!(index.add_path(foo_path));
1532 t!(index.add_path(bin_path));
1533 let mut opts = DiffOptions::new();
1534 opts.include_untracked(true).show_binary(true);
1535 let diff = t!(repo.diff_tree_to_index(None, Some(&index), Some(&mut opts)));
1536 let mut bin_content = None;
1537 let mut new_lines = 0;
1538 let mut line_content = None;
1539 t!(diff.foreach(
1540 &mut |_file, _progress| { true },
1541 Some(&mut |_file, binary| {
1542 bin_content = Some(binary.new_file().data().to_owned());
1543 true
1544 }),
1545 Some(&mut |_file, hunk| {
1546 new_lines = hunk.new_lines();
1547 true
1548 }),
1549 Some(&mut |_file, _hunk, line| {
1550 line_content = String::from_utf8(line.content().into()).ok();
1551 true
1552 })
1553 ));
1554 assert_eq!(bin_content, Some(deflated_fib));
1555 assert_eq!(new_lines, 1);
1556 assert_eq!(line_content, Some("bar\n".to_string()));
1557 }
1558
1559 #[test]
format_email_simple()1560 fn format_email_simple() {
1561 let (_td, repo) = crate::test::repo_init();
1562 const COMMIT_MESSAGE: &str = "Modify some content";
1563 const EXPECTED_EMAIL_START: &str = concat!(
1564 "From f1234fb0588b6ed670779a34ba5c51ef962f285f Mon Sep 17 00:00:00 2001\n",
1565 "From: Techcable <dummy@dummy.org>\n",
1566 "Date: Tue, 11 Jan 1972 17:46:40 +0000\n",
1567 "Subject: [PATCH] Modify some content\n",
1568 "\n",
1569 "---\n",
1570 " file1.txt | 8 +++++---\n",
1571 " 1 file changed, 5 insertions(+), 3 deletions(-)\n",
1572 "\n",
1573 "diff --git a/file1.txt b/file1.txt\n",
1574 "index 94aaae8..af8f41d 100644\n",
1575 "--- a/file1.txt\n",
1576 "+++ b/file1.txt\n",
1577 "@@ -1,15 +1,17 @@\n",
1578 " file1.txt\n",
1579 " file1.txt\n",
1580 "+_file1.txt_\n",
1581 " file1.txt\n",
1582 " file1.txt\n",
1583 " file1.txt\n",
1584 " file1.txt\n",
1585 "+\n",
1586 "+\n",
1587 " file1.txt\n",
1588 " file1.txt\n",
1589 " file1.txt\n",
1590 " file1.txt\n",
1591 " file1.txt\n",
1592 "-file1.txt\n",
1593 "-file1.txt\n",
1594 "-file1.txt\n",
1595 "+_file1.txt_\n",
1596 "+_file1.txt_\n",
1597 " file1.txt\n",
1598 "--\n"
1599 );
1600 const ORIGINAL_FILE: &str = concat!(
1601 "file1.txt\n",
1602 "file1.txt\n",
1603 "file1.txt\n",
1604 "file1.txt\n",
1605 "file1.txt\n",
1606 "file1.txt\n",
1607 "file1.txt\n",
1608 "file1.txt\n",
1609 "file1.txt\n",
1610 "file1.txt\n",
1611 "file1.txt\n",
1612 "file1.txt\n",
1613 "file1.txt\n",
1614 "file1.txt\n",
1615 "file1.txt\n"
1616 );
1617 const UPDATED_FILE: &str = concat!(
1618 "file1.txt\n",
1619 "file1.txt\n",
1620 "_file1.txt_\n",
1621 "file1.txt\n",
1622 "file1.txt\n",
1623 "file1.txt\n",
1624 "file1.txt\n",
1625 "\n",
1626 "\n",
1627 "file1.txt\n",
1628 "file1.txt\n",
1629 "file1.txt\n",
1630 "file1.txt\n",
1631 "file1.txt\n",
1632 "_file1.txt_\n",
1633 "_file1.txt_\n",
1634 "file1.txt\n"
1635 );
1636 const FILE_MODE: i32 = 0o100644;
1637 let original_file = repo.blob(ORIGINAL_FILE.as_bytes()).unwrap();
1638 let updated_file = repo.blob(UPDATED_FILE.as_bytes()).unwrap();
1639 let mut original_tree = repo.treebuilder(None).unwrap();
1640 original_tree
1641 .insert("file1.txt", original_file, FILE_MODE)
1642 .unwrap();
1643 let original_tree = original_tree.write().unwrap();
1644 let mut updated_tree = repo.treebuilder(None).unwrap();
1645 updated_tree
1646 .insert("file1.txt", updated_file, FILE_MODE)
1647 .unwrap();
1648 let updated_tree = updated_tree.write().unwrap();
1649 let time = Time::new(64_000_000, 0);
1650 let author = Signature::new("Techcable", "dummy@dummy.org", &time).unwrap();
1651 let updated_commit = repo
1652 .commit(
1653 None,
1654 &author,
1655 &author,
1656 COMMIT_MESSAGE,
1657 &repo.find_tree(updated_tree).unwrap(),
1658 &[], // NOTE: Have no parents to ensure stable hash
1659 )
1660 .unwrap();
1661 let updated_commit = repo.find_commit(updated_commit).unwrap();
1662 let mut diff = repo
1663 .diff_tree_to_tree(
1664 Some(&repo.find_tree(original_tree).unwrap()),
1665 Some(&repo.find_tree(updated_tree).unwrap()),
1666 None,
1667 )
1668 .unwrap();
1669 let actual_email = diff.format_email(1, 1, &updated_commit, None).unwrap();
1670 let actual_email = actual_email.as_str().unwrap();
1671 assert!(
1672 actual_email.starts_with(EXPECTED_EMAIL_START),
1673 "Unexpected email:\n{}",
1674 actual_email
1675 );
1676 let mut remaining_lines = actual_email[EXPECTED_EMAIL_START.len()..].lines();
1677 let version_line = remaining_lines.next();
1678 assert!(
1679 version_line.unwrap().starts_with("libgit2"),
1680 "Invalid version line: {:?}",
1681 version_line
1682 );
1683 while let Some(line) = remaining_lines.next() {
1684 assert_eq!(line.trim(), "")
1685 }
1686 }
1687 }
1688