1use 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#[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 pub fn as_slice(&self) -> &[u8] {
58 self.0.as_slice()
59 }
60
61 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#[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 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 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 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 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#[derive(Accessors)]
176pub struct Diff {
177 #[access(get(ty(&[DiffEntry<TextDiff<'static, 'static, [u8]>>])))]
179 entries: Vec<DiffEntry<TextDiff<'static, 'static, [u8]>>>,
180}
181
182#[derive(Accessors)]
184pub struct TreeDiff {
185 #[access(get(ty(&[DiffEntry<(ObjectId, ObjectId)>])))]
187 entries: Vec<DiffEntry<(ObjectId, ObjectId)>>,
188}
189
190impl TreeDiff {
191 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 #[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 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 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 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 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}