Skip to main content

git_async/
diff.rs

1//! A module for computing diffs between git trees
2//!
3//! # Usage
4//!
5//! First, construct a [`TreeDiff`] object, which walks the specified trees and
6//! finds differing files. Then, use [`TreeDiff::to_text_diff`] to perform a
7//! line-by-line diff on each differing file.
8//!
9//! # Example
10//!
11//! ```
12//! # use git_async::{diff::{TreeDiff, Diff}, error::GResult, object::Tree, Repo, file_system::FileSystem};
13//! async fn get_diff<F: FileSystem>(repo: &Repo<F>, left: &Tree, right: &Tree) -> GResult<Diff> {
14//!     let tree_diff = TreeDiff::new(repo, left, right).await?;
15//!     tree_diff.to_text_diff(repo).await
16//! }
17//! ```
18//!
19//! # Notes
20//!
21//! This algorithm is relatively naive, in that it simply loads each object in
22//! full and then computes their diff. You will find that using `git diff` on
23//! the command line is much faster. This is likely because because `git diff`
24//! may be aware of the packfile delta encoding and may use it to compute
25//! efficient diffs.
26
27use crate::{
28    Repo,
29    error::{Error, GResult},
30    file_system::FileSystem,
31    object::{Object, ObjectId, Tree, TreeEntry, TreeEntryType},
32};
33use accessory::Accessors;
34use alloc::format;
35use alloc::{string::String, vec::Vec};
36use core::convert::Infallible;
37use similar::{TextDiff, TextDiffConfig};
38
39/// A path for a file in a diff
40#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
41pub struct Path(Vec<u8>);
42
43impl core::fmt::Debug for Path {
44    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
45        match str::from_utf8(&self.0) {
46            Ok(p) => f.debug_tuple("Path").field(&p).finish(),
47            Err(_) => f
48                .debug_tuple("Path")
49                .field(&String::from_utf8_lossy(&self.0))
50                .finish(),
51        }
52    }
53}
54
55impl Path {
56    /// View the path as a slice of bytes
57    pub fn as_slice(&self) -> &[u8] {
58        self.0.as_slice()
59    }
60
61    /// Consume the path and return its inner [`Vec<u8>`]
62    pub fn inner(self) -> Vec<u8> {
63        self.0
64    }
65}
66
67fn join(path: Option<&Path>, component: &[u8]) -> Path {
68    match path {
69        Some(p) => {
70            let mut out = Vec::with_capacity(p.0.len() + 1 + component.len());
71            out.extend_from_slice(&p.0);
72            out.push(b'/');
73            out.extend_from_slice(component);
74            Path(out)
75        }
76        None => Path(component.to_vec()),
77    }
78}
79
80/// Represents a diff of a single file
81///
82/// It is generic over the content of the file diff. For tree diffs, `Content`
83/// is a pair of [`ObjectId`]s, one of which may be zero. For full diffs,
84/// `Content` is a `similar::TextDiff`.
85#[expect(missing_docs)]
86#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
87pub enum DiffEntry<Content> {
88    LeftOnly {
89        path: Path,
90        entry_type: TreeEntryType,
91        content: Content,
92    },
93    Both {
94        path: Path,
95        left_type: TreeEntryType,
96        right_type: TreeEntryType,
97        content: Content,
98    },
99    RightOnly {
100        path: Path,
101        entry_type: TreeEntryType,
102        content: Content,
103    },
104}
105
106impl<Content> DiffEntry<Content> {
107    /// The content of the diff entry
108    pub fn content(&self) -> &Content {
109        match self {
110            DiffEntry::LeftOnly { content, .. }
111            | DiffEntry::Both { content, .. }
112            | DiffEntry::RightOnly { content, .. } => content,
113        }
114    }
115
116    /// The path of the file that the entry represents
117    pub fn path(&self) -> &Path {
118        match self {
119            DiffEntry::LeftOnly { path, .. }
120            | DiffEntry::Both { path, .. }
121            | DiffEntry::RightOnly { path, .. } => path,
122        }
123    }
124
125    /// Map a function over the content contained in the entry.
126    pub fn map_content<T>(&self, fun: impl Fn(&Content) -> T) -> DiffEntry<T> {
127        self.map_content_res(|c| Ok::<T, Infallible>(fun(c)))
128            .unwrap()
129    }
130
131    /// Map a fallible function over the content contained in the entry.
132    pub fn map_content_res<T, E>(
133        &self,
134        fun: impl Fn(&Content) -> Result<T, E>,
135    ) -> Result<DiffEntry<T>, E> {
136        use DiffEntry::*;
137        Ok(match self {
138            LeftOnly {
139                path,
140                entry_type,
141                content,
142            } => DiffEntry::LeftOnly {
143                path: path.clone(),
144                entry_type: *entry_type,
145                content: fun(content)?,
146            },
147            Both {
148                path,
149                left_type,
150                right_type,
151                content,
152            } => DiffEntry::Both {
153                path: path.clone(),
154                left_type: *left_type,
155                right_type: *right_type,
156                content: fun(content)?,
157            },
158            RightOnly {
159                path,
160                entry_type,
161                content,
162            } => DiffEntry::RightOnly {
163                path: path.clone(),
164                entry_type: *entry_type,
165                content: fun(content)?,
166            },
167        })
168    }
169}
170
171/// A "full" diff, i.e. one which encapsulates line changes between files
172///
173/// This is constructed by first creating a [`TreeDiff`] object and then calling
174/// the [`TreeDiff::to_text_diff`] method.
175#[derive(Accessors)]
176pub struct Diff {
177    /// The entries of the diff, one per differing path in the tree
178    #[access(get(ty(&[DiffEntry<TextDiff<'static, 'static, [u8]>>])))]
179    entries: Vec<DiffEntry<TextDiff<'static, 'static, [u8]>>>,
180}
181
182/// A diff of git trees, holding the [`ObjectId`]s of differing files
183#[derive(Accessors)]
184pub struct TreeDiff {
185    /// The entries of the diff, one per differing path in the tree
186    #[access(get(ty(&[DiffEntry<(ObjectId, ObjectId)>])))]
187    entries: Vec<DiffEntry<(ObjectId, ObjectId)>>,
188}
189
190impl TreeDiff {
191    /// Construct a [`TreeDiff`] by diffing two trees
192    pub async fn new<F: FileSystem>(repo: &Repo<F>, left: &Tree, right: &Tree) -> GResult<Self> {
193        Self::new_cancelable(repo, left, right, async || false).await
194    }
195
196    /// Construct a [`TreeDiff`] by diffing two trees
197    ///
198    /// The `cancel` parameter is a function which may cancel the diff operation
199    /// by returning `true` at any point. It is called regularly while the diff
200    /// operation is running.
201    ///
202    /// For example,
203    /// ```
204    /// # use git_async::{diff::TreeDiff, error::GResult, object::Tree, Repo, file_system::FileSystem};
205    /// # use std::rc::Rc;
206    /// # use core::cell::Cell;
207    /// struct CancelableDiffFactory { canceled: Rc<Cell<bool>> }
208    /// impl CancelableDiffFactory {
209    ///     pub async fn make_diff<F: FileSystem>(
210    ///         &self,
211    ///         repo: &Repo<F>,
212    ///         left: &Tree,
213    ///         right: &Tree
214    ///     ) -> GResult<TreeDiff> {
215    ///         let canceled = self.canceled.clone();
216    ///         let cancel = async move || canceled.get();
217    ///         TreeDiff::new_cancelable(repo, left, right, cancel).await
218    ///     }
219    ///
220    ///     pub fn cancel(&self) {
221    ///         self.canceled.set(true);
222    ///     }
223    /// }
224    /// ```
225    ///
226    /// In this example, a diff operation may be started by some async routine,
227    /// and then canceled by another by calling the
228    /// `CancelableDiffFactory::cancel` method.
229    #[allow(clippy::too_many_lines)]
230    pub async fn new_cancelable<F: FileSystem>(
231        repo: &Repo<F>,
232        left: &Tree,
233        right: &Tree,
234        mut cancel: impl AsyncFnMut() -> bool,
235    ) -> GResult<Self> {
236        if left.id() == right.id() {
237            return Ok(Self {
238                entries: Vec::new(),
239            });
240        }
241        let mut out: Vec<DiffEntry<(ObjectId, ObjectId)>> = Vec::new();
242        #[allow(clippy::type_complexity)]
243        let mut stack: Vec<(Option<Path>, Option<Tree>, Option<Tree>)> = Vec::new();
244        stack.push((None, Some(left.clone()), Some(right.clone())));
245
246        while let Some((parent_path, left, right)) = stack.pop() {
247            // Loop invariants:
248            // - one of left or right is Some()
249            // - left and right have different IDs
250            debug_assert!(left.is_some() || right.is_some());
251            debug_assert!(left.as_ref().map(Tree::id) != right.as_ref().map(Tree::id));
252            if cancel().await {
253                return Err(Error::DiffCanceled);
254            }
255            let (left, right) = match (left, right) {
256                (Some(left), Some(right)) => (left, right),
257                (Some(left), None) => {
258                    for entry in left.entries() {
259                        let path = join(parent_path.as_ref(), entry.name());
260                        if entry.entry_type() == TreeEntryType::Tree {
261                            let tree = tree(repo, entry.id()).await?;
262                            stack.push((Some(path), None, Some(tree)));
263                        } else {
264                            out.push(DiffEntry::LeftOnly {
265                                path,
266                                entry_type: entry.entry_type(),
267                                content: (entry.id(), ObjectId::zero()),
268                            });
269                        }
270                    }
271                    continue;
272                }
273                (None, Some(right)) => {
274                    for entry in right.entries() {
275                        let path = join(parent_path.as_ref(), entry.name());
276                        if entry.entry_type() == TreeEntryType::Tree {
277                            let tree = tree(repo, entry.id()).await?;
278                            stack.push((Some(path), None, Some(tree)));
279                        } else {
280                            out.push(DiffEntry::RightOnly {
281                                path,
282                                entry_type: entry.entry_type(),
283                                content: (ObjectId::zero(), entry.id()),
284                            });
285                        }
286                    }
287                    continue;
288                }
289                (None, None) => unreachable!(),
290            };
291
292            let mut left_only: Vec<TreeEntry> = Vec::new();
293            let mut right_only: Vec<TreeEntry> = Vec::new();
294            let mut both: Vec<(TreeEntry, TreeEntry)> = Vec::new();
295            for left_entry in left.entries() {
296                let right_entry = right.entries().find(|e| e.name() == left_entry.name());
297                match right_entry {
298                    Some(e) => both.push((left_entry, e)),
299                    None => left_only.push(left_entry),
300                }
301            }
302            for right_entry in right.entries() {
303                if both
304                    .iter()
305                    .find(|(_, e)| e.name() == right_entry.name())
306                    .is_none()
307                {
308                    right_only.push(right_entry);
309                }
310            }
311            for entry in left_only {
312                let path = join(parent_path.as_ref(), entry.name());
313                if entry.entry_type() == TreeEntryType::Tree {
314                    let left_tree = tree(repo, entry.id()).await?;
315                    stack.push((Some(path), Some(left_tree), None));
316                } else {
317                    out.push(DiffEntry::LeftOnly {
318                        path,
319                        entry_type: entry.entry_type(),
320                        content: (entry.id(), ObjectId::zero()),
321                    });
322                }
323            }
324            for entry in right_only {
325                let path = join(parent_path.as_ref(), entry.name());
326                if entry.entry_type() == TreeEntryType::Tree {
327                    let right_tree = tree(repo, entry.id()).await?;
328                    stack.push((Some(path), None, Some(right_tree)));
329                } else {
330                    out.push(DiffEntry::RightOnly {
331                        path,
332                        entry_type: entry.entry_type(),
333                        content: (ObjectId::zero(), entry.id()),
334                    });
335                }
336            }
337            for (left, right) in both {
338                if left.id() == right.id() {
339                    continue;
340                }
341                let name = left.name();
342                match (left.entry_type(), right.entry_type()) {
343                    (TreeEntryType::Tree, TreeEntryType::Tree) => {
344                        let left = tree(repo, left.id()).await?;
345                        let right = tree(repo, right.id()).await?;
346                        let path = join(parent_path.as_ref(), name);
347                        stack.push((Some(path), Some(left), Some(right)));
348                    }
349                    (TreeEntryType::Tree, _) => {
350                        let path = join(parent_path.as_ref(), name);
351                        out.push(DiffEntry::RightOnly {
352                            path: path.clone(),
353                            entry_type: right.entry_type(),
354                            content: (ObjectId::zero(), right.id()),
355                        });
356                        let left_tree = tree(repo, left.id()).await?;
357                        stack.push((Some(path), Some(left_tree), None));
358                    }
359                    (_, TreeEntryType::Tree) => {
360                        let path = join(parent_path.as_ref(), name);
361                        out.push(DiffEntry::LeftOnly {
362                            path: path.clone(),
363                            entry_type: left.entry_type(),
364                            content: (left.id(), ObjectId::zero()),
365                        });
366                        let right_tree = tree(repo, right.id()).await?;
367                        stack.push((Some(path), None, Some(right_tree)));
368                    }
369                    _ => {
370                        out.push(DiffEntry::Both {
371                            path: join(parent_path.as_ref(), name),
372                            left_type: left.entry_type(),
373                            right_type: right.entry_type(),
374                            content: (left.id(), right.id()),
375                        });
376                    }
377                }
378            }
379        }
380        Ok(Self { entries: out })
381    }
382
383    /// Turn the [`TreeDiff`] into a [`Diff`] by creating a line diff of each
384    /// file.
385    pub async fn to_text_diff<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<Diff> {
386        self.to_text_diff_full(repo, &TextDiffConfig::default(), async || false)
387            .await
388    }
389
390    /// Like [`TreeDiff::to_text_diff`] but accepts a
391    /// [`similar::TextDiffConfig`] parameter to configure the file diff
392    /// operations and a `cancel` parameter to externally cancel the diff.
393    ///
394    /// The `cancel` parameter is analogous to the `cancel` parameter on
395    /// [`TreeDiff::new_cancelable`]; see there for further details.
396    pub async fn to_text_diff_full<F: FileSystem>(
397        &self,
398        repo: &Repo<F>,
399        config: &TextDiffConfig,
400        mut cancel: impl AsyncFnMut() -> bool,
401    ) -> GResult<Diff> {
402        let mut out: Vec<_> = Vec::with_capacity(self.entries.len());
403        for entry in &self.entries {
404            if cancel().await {
405                return Err(Error::DiffCanceled);
406            }
407            let entry = entry.resolve(repo, config.clone()).await?;
408            out.push(entry);
409        }
410        Ok(Diff { entries: out })
411    }
412}
413
414async fn tree<F: FileSystem>(repo: &Repo<F>, id: ObjectId) -> GResult<Tree> {
415    repo.lookup_object(id)
416        .await?
417        .peel_to_tree(repo)
418        .await?
419        .ok_or_else(|| Error::MalformedObject(id))
420}
421
422impl DiffEntry<(ObjectId, ObjectId)> {
423    /// Look up the objects encoded in the diff entry and compute a diff of the
424    /// files.
425    pub async fn resolve<F: FileSystem>(
426        &self,
427        repo: &Repo<F>,
428        config: TextDiffConfig,
429    ) -> GResult<DiffEntry<TextDiff<'static, 'static, [u8]>>> {
430        match self {
431            DiffEntry::LeftOnly {
432                path,
433                entry_type,
434                content: (id, _),
435            } => {
436                let body = read_leaf(repo, *entry_type, *id).await?;
437                Ok(DiffEntry::LeftOnly {
438                    path: path.clone(),
439                    entry_type: *entry_type,
440                    content: config.diff_lines(body, Vec::new()),
441                })
442            }
443            DiffEntry::RightOnly {
444                path,
445                entry_type,
446                content: (_, id),
447            } => {
448                let body = read_leaf(repo, *entry_type, *id).await?;
449                Ok(DiffEntry::RightOnly {
450                    path: path.clone(),
451                    entry_type: *entry_type,
452                    content: config.diff_lines(Vec::new(), body),
453                })
454            }
455            DiffEntry::Both {
456                path,
457                left_type,
458                right_type,
459                content: (left_id, right_id),
460            } => {
461                let left_body = read_leaf(repo, *left_type, *left_id).await?;
462                let right_body = read_leaf(repo, *right_type, *right_id).await?;
463                let diff = config.diff_lines(left_body, right_body);
464                Ok(DiffEntry::Both {
465                    path: path.clone(),
466                    left_type: *left_type,
467                    right_type: *right_type,
468                    content: diff,
469                })
470            }
471        }
472    }
473}
474
475async fn read_leaf<F: FileSystem>(
476    repo: &Repo<F>,
477    entry_type: TreeEntryType,
478    id: ObjectId,
479) -> GResult<Vec<u8>> {
480    debug_assert!(entry_type != TreeEntryType::Tree);
481    if entry_type == TreeEntryType::Commit {
482        let s = format!("{id}");
483        return Ok(s.into_bytes());
484    }
485    let object = repo.lookup_object(id).await?;
486    if let Object::Blob(b) = object {
487        return Ok(b.data_owned());
488    }
489    unreachable!("Tree entry resolved object was not a blob")
490}
491
492#[cfg(test)]
493mod tests {
494    use crate::{
495        Repo,
496        reference::RefName,
497        test::{
498            helpers::{make_basic_repo, make_file},
499            impls::TestFileSystem,
500        },
501    };
502    use futures::executor::block_on;
503    use std::{
504        collections::BTreeSet,
505        fs::{create_dir, remove_file},
506        io::Write,
507        path::PathBuf,
508    };
509
510    use super::*;
511
512    fn head_tree(repo: &Repo<TestFileSystem>) -> Tree {
513        let head = block_on(repo.lookup_ref(&RefName::Head)).unwrap();
514        block_on(head.peel_to_tree(repo)).unwrap().unwrap()
515    }
516
517    #[test]
518    fn diff_same() {
519        let test_repo = make_basic_repo().unwrap();
520        let repo = test_repo.repo();
521        let tree = head_tree(&repo);
522        assert!(
523            block_on(TreeDiff::new(&repo, &tree, &tree))
524                .unwrap()
525                .entries()
526                .is_empty()
527        );
528    }
529
530    #[test]
531    fn basic_root_diff() {
532        let test_repo = make_basic_repo().unwrap();
533        let repo = test_repo.repo();
534        let mut file_a = make_file(&test_repo, "a").unwrap();
535        test_repo.run_git(["add", "--all"]).unwrap();
536        test_repo
537            .commit("a commit", "a user", "an-email", "2000-01-01T00:00:00Z")
538            .unwrap();
539        let before = head_tree(&repo);
540        file_a.write_all(b"some data").unwrap();
541        file_a.flush().unwrap();
542        let mut file_b = make_file(&test_repo, "b").unwrap();
543        file_b.write_all(b"some more data").unwrap();
544        test_repo.run_git(["add", "--all"]).unwrap();
545        test_repo
546            .commit("a commit", "a user", "an-email", "2000-01-01T00:00:00Z")
547            .unwrap();
548        let after = head_tree(&repo);
549        let the_diff = block_on(TreeDiff::new(&repo, &before, &after))
550            .unwrap()
551            .entries()
552            .iter()
553            .map(Clone::clone)
554            .collect::<BTreeSet<_>>();
555        assert_eq!(
556            the_diff,
557            vec![
558                DiffEntry::Both {
559                    path: Path(b"a".to_vec()),
560                    left_type: TreeEntryType::File,
561                    right_type: TreeEntryType::File,
562                    content: (
563                        ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
564                        ObjectId::from_hex(b"7c0646bfd53c1f0ed45ffd81563f30017717ca58").unwrap(),
565                    ),
566                },
567                DiffEntry::RightOnly {
568                    path: Path(b"b".to_vec()),
569                    entry_type: TreeEntryType::File,
570                    content: (
571                        ObjectId::zero(),
572                        ObjectId::from_hex(b"dfa37ec69ffae3abcf7efbb386226cb84b510fa8").unwrap()
573                    )
574                }
575            ]
576            .into_iter()
577            .collect()
578        );
579        let the_diff = block_on(TreeDiff::new(&repo, &after, &before))
580            .unwrap()
581            .entries()
582            .iter()
583            .map(Clone::clone)
584            .collect::<BTreeSet<_>>();
585        assert_eq!(
586            the_diff,
587            vec![
588                DiffEntry::Both {
589                    path: Path(b"a".to_vec()),
590                    left_type: TreeEntryType::File,
591                    right_type: TreeEntryType::File,
592                    content: (
593                        ObjectId::from_hex(b"7c0646bfd53c1f0ed45ffd81563f30017717ca58").unwrap(),
594                        ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
595                    ),
596                },
597                DiffEntry::LeftOnly {
598                    path: Path(b"b".to_vec()),
599                    entry_type: TreeEntryType::File,
600                    content: (
601                        ObjectId::from_hex(b"dfa37ec69ffae3abcf7efbb386226cb84b510fa8").unwrap(),
602                        ObjectId::zero()
603                    )
604                }
605            ]
606            .into_iter()
607            .collect()
608        );
609    }
610
611    #[test]
612    fn basic_subtree_diff() {
613        let test_repo = make_basic_repo().unwrap();
614        let repo = test_repo.repo();
615        create_dir(test_repo.location.path().join("dir")).unwrap();
616        let mut file_a = make_file(&test_repo, PathBuf::from("dir").join("a")).unwrap();
617        test_repo.run_git(["add", "--all"]).unwrap();
618        test_repo
619            .commit("a commit", "a user", "an-email", "2000-01-01T00:00:00Z")
620            .unwrap();
621        let before = head_tree(&repo);
622        file_a.write_all(b"some data").unwrap();
623        file_a.flush().unwrap();
624        test_repo.run_git(["add", "--all"]).unwrap();
625        test_repo
626            .commit("a commit", "a user", "an-email", "2000-01-01T00:00:00Z")
627            .unwrap();
628        let after = head_tree(&repo);
629        let the_diff = block_on(TreeDiff::new(&repo, &before, &after))
630            .unwrap()
631            .entries()
632            .iter()
633            .map(Clone::clone)
634            .collect::<BTreeSet<_>>();
635        assert_eq!(
636            the_diff,
637            vec![DiffEntry::Both {
638                path: Path(b"dir/a".to_vec()),
639                left_type: TreeEntryType::File,
640                right_type: TreeEntryType::File,
641                content: (
642                    ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
643                    ObjectId::from_hex(b"7c0646bfd53c1f0ed45ffd81563f30017717ca58").unwrap(),
644                )
645            },]
646            .into_iter()
647            .collect()
648        );
649        let the_diff = block_on(TreeDiff::new(&repo, &after, &before))
650            .unwrap()
651            .entries()
652            .iter()
653            .map(Clone::clone)
654            .collect::<BTreeSet<_>>();
655        assert_eq!(
656            the_diff,
657            vec![DiffEntry::Both {
658                path: Path(b"dir/a".to_vec()),
659                left_type: TreeEntryType::File,
660                right_type: TreeEntryType::File,
661                content: (
662                    ObjectId::from_hex(b"7c0646bfd53c1f0ed45ffd81563f30017717ca58").unwrap(),
663                    ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
664                ),
665            },]
666            .into_iter()
667            .collect()
668        );
669    }
670
671    #[test]
672    fn complex_subtree_diff() {
673        let test_repo = make_basic_repo().unwrap();
674        let repo = test_repo.repo();
675        make_file(&test_repo, "a").unwrap();
676        test_repo.run_git(["add", "--all"]).unwrap();
677        test_repo
678            .commit("a commit", "a user", "an-email", "2000-01-01T00:00:00Z")
679            .unwrap();
680        let before = head_tree(&repo);
681        remove_file(test_repo.location.path().join("a")).unwrap();
682        create_dir(test_repo.location.path().join("a")).unwrap();
683        make_file(&test_repo, PathBuf::from("a").join("b")).unwrap();
684        create_dir(test_repo.location.path().join("dir")).unwrap();
685        make_file(&test_repo, PathBuf::from("dir").join("c")).unwrap();
686        test_repo.run_git(["add", "--all"]).unwrap();
687        test_repo
688            .commit("a commit", "a user", "an-email", "2000-01-01T00:00:00Z")
689            .unwrap();
690        let after = head_tree(&repo);
691        let the_diff = block_on(TreeDiff::new(&repo, &before, &after))
692            .unwrap()
693            .entries()
694            .iter()
695            .map(Clone::clone)
696            .collect::<BTreeSet<_>>();
697        assert_eq!(
698            the_diff,
699            vec![
700                DiffEntry::RightOnly {
701                    path: Path(b"a/b".to_vec()),
702                    entry_type: TreeEntryType::File,
703                    content: (
704                        ObjectId::zero(),
705                        ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
706                    )
707                },
708                DiffEntry::LeftOnly {
709                    path: Path(b"a".to_vec()),
710                    entry_type: TreeEntryType::File,
711                    content: (
712                        ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
713                        ObjectId::zero()
714                    )
715                },
716                DiffEntry::RightOnly {
717                    path: Path(b"dir/c".to_vec()),
718                    entry_type: TreeEntryType::File,
719                    content: (
720                        ObjectId::zero(),
721                        ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
722                    )
723                },
724            ]
725            .into_iter()
726            .collect()
727        );
728        let the_diff = block_on(TreeDiff::new(&repo, &after, &before))
729            .unwrap()
730            .entries()
731            .iter()
732            .map(Clone::clone)
733            .collect::<BTreeSet<_>>();
734        assert_eq!(
735            the_diff,
736            vec![
737                DiffEntry::LeftOnly {
738                    path: Path(b"a/b".to_vec()),
739                    entry_type: TreeEntryType::File,
740                    content: (
741                        ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
742                        ObjectId::zero()
743                    )
744                },
745                DiffEntry::RightOnly {
746                    path: Path(b"a".to_vec()),
747                    entry_type: TreeEntryType::File,
748                    content: (
749                        ObjectId::zero(),
750                        ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
751                    )
752                },
753                DiffEntry::LeftOnly {
754                    path: Path(b"dir/c".to_vec()),
755                    entry_type: TreeEntryType::File,
756                    content: (
757                        ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
758                        ObjectId::zero()
759                    )
760                },
761            ]
762            .into_iter()
763            .collect()
764        );
765    }
766}