comparison mercurial/copies.py @ 44196:6ca9f45b32b0

copies: make mergecopies() distinguish between copies on each side I find it confusing that most of the dicts returned from `mergecopies()` have entries specific to one branch of the merge, but they're still combined into dict. For example, you can't tell if `copy = {"bar": "foo"}` means that "foo" was copied to "bar" on the first branch or the second. It also feels like there are bugs lurking here because we may mistake which side the copy happened on. However, for most of the dicts, it's not possible that there is disagreement. For example, `renamedelete` keeps track of renames that happened on one side of the merge where the other side deleted the file. There can't be a disagreement there (because we record that in the `diverge` dict instead). For regular copies/renames, there can be a disagreement. Let's say file "foo" was copied to "bar" on one branch and file "baz" was copied to "bar" on the other. Beacause we only return one `copy` dict, we end up replacing the `{"bar": "foo"}` entry by `{"bar": "baz"}`. The merge code (`manifestmerge()`) will then decide that that means "both renamed from 'baz'". We should probably treat it as a conflict instead. The next few patches will make `mergecopies()` return two instances of most of the returned copies. That will lead to a bit more code (~40 lines), but I think it makes both `copies.mergecopies()` and `merge.manifestmerge()` clearer. Differential Revision: https://phab.mercurial-scm.org/D7986
author Martin von Zweigbergk <martinvonz@google.com>
date Wed, 22 Jan 2020 15:31:17 -0800
parents 45192589555c
children 17e12938f8e7
comparison
equal deleted inserted replaced
44195:51c86c6167c1 44196:6ca9f45b32b0
571 for dst, src in copies1.items(): 571 for dst, src in copies1.items():
572 inversecopies1.setdefault(src, []).append(dst) 572 inversecopies1.setdefault(src, []).append(dst)
573 for dst, src in copies2.items(): 573 for dst, src in copies2.items():
574 inversecopies2.setdefault(src, []).append(dst) 574 inversecopies2.setdefault(src, []).append(dst)
575 575
576 copy = {} 576 copy1 = {}
577 copy2 = {}
577 diverge = {} 578 diverge = {}
578 renamedelete = {} 579 renamedelete1 = {}
580 renamedelete2 = {}
579 allsources = set(inversecopies1) | set(inversecopies2) 581 allsources = set(inversecopies1) | set(inversecopies2)
580 for src in allsources: 582 for src in allsources:
581 dsts1 = inversecopies1.get(src) 583 dsts1 = inversecopies1.get(src)
582 dsts2 = inversecopies2.get(src) 584 dsts2 = inversecopies2.get(src)
583 if dsts1 and dsts2: 585 if dsts1 and dsts2:
590 # consider it not divergent. For example, if side 1 copies 'a' 592 # consider it not divergent. For example, if side 1 copies 'a'
591 # to 'b' and 'c' and deletes 'a', and side 2 copies 'a' to 'c' 593 # to 'b' and 'c' and deletes 'a', and side 2 copies 'a' to 'c'
592 # and 'd' and deletes 'a'. 594 # and 'd' and deletes 'a'.
593 if dsts1 & dsts2: 595 if dsts1 & dsts2:
594 for dst in dsts1 & dsts2: 596 for dst in dsts1 & dsts2:
595 copy[dst] = src 597 copy1[dst] = src
598 copy2[dst] = src
596 else: 599 else:
597 diverge[src] = sorted(dsts1 | dsts2) 600 diverge[src] = sorted(dsts1 | dsts2)
598 elif src in m1 and src in m2: 601 elif src in m1 and src in m2:
599 # copied on both sides 602 # copied on both sides
600 dsts1 = set(dsts1) 603 dsts1 = set(dsts1)
601 dsts2 = set(dsts2) 604 dsts2 = set(dsts2)
602 for dst in dsts1 & dsts2: 605 for dst in dsts1 & dsts2:
603 copy[dst] = src 606 copy1[dst] = src
607 copy2[dst] = src
604 # TODO: Handle cases where it was renamed on one side and copied 608 # TODO: Handle cases where it was renamed on one side and copied
605 # on the other side 609 # on the other side
606 elif dsts1: 610 elif dsts1:
607 # copied/renamed only on side 1 611 # copied/renamed only on side 1
608 _checksinglesidecopies( 612 _checksinglesidecopies(
609 src, dsts1, m1, m2, mb, c2, base, copy, renamedelete 613 src, dsts1, m1, m2, mb, c2, base, copy1, renamedelete1
610 ) 614 )
611 elif dsts2: 615 elif dsts2:
612 # copied/renamed only on side 2 616 # copied/renamed only on side 2
613 _checksinglesidecopies( 617 _checksinglesidecopies(
614 src, dsts2, m2, m1, mb, c1, base, copy, renamedelete 618 src, dsts2, m2, m1, mb, c1, base, copy2, renamedelete2
615 ) 619 )
616 620
617 # find interesting file sets from manifests 621 # find interesting file sets from manifests
618 addedinm1 = m1.filesnotin(mb, repo.narrowmatch()) 622 addedinm1 = m1.filesnotin(mb, repo.narrowmatch())
619 addedinm2 = m2.filesnotin(mb, repo.narrowmatch()) 623 addedinm2 = m2.filesnotin(mb, repo.narrowmatch())
632 if repo.ui.debugflag: 636 if repo.ui.debugflag:
633 renamedeleteset = set() 637 renamedeleteset = set()
634 divergeset = set() 638 divergeset = set()
635 for dsts in diverge.values(): 639 for dsts in diverge.values():
636 divergeset.update(dsts) 640 divergeset.update(dsts)
637 for dsts in renamedelete.values(): 641 for dsts in renamedelete1.values():
642 renamedeleteset.update(dsts)
643 for dsts in renamedelete2.values():
638 renamedeleteset.update(dsts) 644 renamedeleteset.update(dsts)
639 645
640 repo.ui.debug( 646 repo.ui.debug(
641 b" all copies found (* = to merge, ! = divergent, " 647 b" all copies found (* = to merge, ! = divergent, "
642 b"% = renamed and deleted):\n" 648 b"% = renamed and deleted):\n"
643 ) 649 )
644 for f in sorted(fullcopy): 650 for f in sorted(fullcopy):
645 note = b"" 651 note = b""
646 if f in copy: 652 if f in copy1 or f in copy2:
647 note += b"*" 653 note += b"*"
648 if f in divergeset: 654 if f in divergeset:
649 note += b"!" 655 note += b"!"
650 if f in renamedeleteset: 656 if f in renamedeleteset:
651 note += b"%" 657 note += b"%"
655 del renamedeleteset 661 del renamedeleteset
656 del divergeset 662 del divergeset
657 663
658 repo.ui.debug(b" checking for directory renames\n") 664 repo.ui.debug(b" checking for directory renames\n")
659 665
660 dirmove, movewithdir = _dir_renames(repo, c1, c2, copy, fullcopy, u1, u2) 666 dirmove1, movewithdir2 = _dir_renames(repo, c1, copy1, copies1, u2)
661 667 dirmove2, movewithdir1 = _dir_renames(repo, c2, copy2, copies2, u1)
662 return copy, movewithdir, diverge, renamedelete, dirmove 668
663 669 copy1.update(copy2)
664 670 renamedelete1.update(renamedelete2)
665 def _dir_renames(repo, c1, c2, copy, fullcopy, u1, u2): 671 movewithdir1.update(movewithdir2)
672 dirmove1.update(dirmove2)
673
674 return copy1, movewithdir1, diverge, renamedelete1, dirmove1
675
676
677 def _dir_renames(repo, ctx, copy, fullcopy, addedfiles):
678 """Finds moved directories and files that should move with them.
679
680 ctx: the context for one of the sides
681 copy: files copied on the same side (as ctx)
682 fullcopy: files copied on the same side (as ctx), including those that
683 merge.manifestmerge() won't care about
684 addedfiles: added files on the other side (compared to ctx)
685 """
666 # generate a directory move map 686 # generate a directory move map
667 d1, d2 = c1.dirs(), c2.dirs() 687 d = ctx.dirs()
668 invalid = set() 688 invalid = set()
669 dirmove = {} 689 dirmove = {}
670 690
671 # examine each file copy for a potential directory move, which is 691 # examine each file copy for a potential directory move, which is
672 # when all the files in a directory are moved to a new directory 692 # when all the files in a directory are moved to a new directory
673 for dst, src in pycompat.iteritems(fullcopy): 693 for dst, src in pycompat.iteritems(fullcopy):
674 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst) 694 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
675 if dsrc in invalid: 695 if dsrc in invalid:
676 # already seen to be uninteresting 696 # already seen to be uninteresting
677 continue 697 continue
678 elif dsrc in d1 and ddst in d1: 698 elif dsrc in d and ddst in d:
679 # directory wasn't entirely moved locally 699 # directory wasn't entirely moved locally
680 invalid.add(dsrc)
681 elif dsrc in d2 and ddst in d2:
682 # directory wasn't entirely moved remotely
683 invalid.add(dsrc) 700 invalid.add(dsrc)
684 elif dsrc in dirmove and dirmove[dsrc] != ddst: 701 elif dsrc in dirmove and dirmove[dsrc] != ddst:
685 # files from the same directory moved to two different places 702 # files from the same directory moved to two different places
686 invalid.add(dsrc) 703 invalid.add(dsrc)
687 else: 704 else:
689 dirmove[dsrc] = ddst 706 dirmove[dsrc] = ddst
690 707
691 for i in invalid: 708 for i in invalid:
692 if i in dirmove: 709 if i in dirmove:
693 del dirmove[i] 710 del dirmove[i]
694 del d1, d2, invalid 711 del d, invalid
695 712
696 if not dirmove: 713 if not dirmove:
697 return {}, {} 714 return {}, {}
698 715
699 dirmove = {k + b"/": v + b"/" for k, v in pycompat.iteritems(dirmove)} 716 dirmove = {k + b"/": v + b"/" for k, v in pycompat.iteritems(dirmove)}
703 b" discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d]) 720 b" discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d])
704 ) 721 )
705 722
706 movewithdir = {} 723 movewithdir = {}
707 # check unaccounted nonoverlapping files against directory moves 724 # check unaccounted nonoverlapping files against directory moves
708 for f in u1 + u2: 725 for f in addedfiles:
709 if f not in fullcopy: 726 if f not in fullcopy:
710 for d in dirmove: 727 for d in dirmove:
711 if f.startswith(d): 728 if f.startswith(d):
712 # new file added in a directory that was moved, move it 729 # new file added in a directory that was moved, move it
713 df = dirmove[d] + f[len(d) :] 730 df = dirmove[d] + f[len(d) :]