Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/copies.py @ 44271: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
44270:51c86c6167c1 | 44271: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) :] |