~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-02-11 04:02:41 UTC
  • mfrom: (5017.2.2 tariff)
  • Revision ID: pqm@pqm.ubuntu.com-20100211040241-w6n021dz0uus341n
(mbp) add import-tariff tests

Show diffs side-by-side

added added

removed removed

Lines of Context:
27
27
    merge3,
28
28
    osutils,
29
29
    patiencediff,
 
30
    progress,
30
31
    revision as _mod_revision,
31
32
    textfile,
32
33
    trace,
36
37
    ui,
37
38
    versionedfile
38
39
    )
39
 
from bzrlib.cleanup import OperationWithCleanups
40
40
from bzrlib.symbol_versioning import (
41
41
    deprecated_in,
42
42
    deprecated_method,
46
46
 
47
47
def transform_tree(from_tree, to_tree, interesting_ids=None):
48
48
    from_tree.lock_tree_write()
49
 
    operation = OperationWithCleanups(merge_inner)
50
 
    operation.add_cleanup(from_tree.unlock)
51
 
    operation.run_simple(from_tree.branch, to_tree, from_tree,
52
 
        ignore_zero=True, interesting_ids=interesting_ids, this_tree=from_tree)
 
49
    try:
 
50
        merge_inner(from_tree.branch, to_tree, from_tree, ignore_zero=True,
 
51
                    interesting_ids=interesting_ids, this_tree=from_tree)
 
52
    finally:
 
53
        from_tree.unlock()
53
54
 
54
55
 
55
56
class MergeHooks(hooks.Hooks):
93
94
        return ('not applicable', None)
94
95
 
95
96
 
96
 
class PerFileMerger(AbstractPerFileMerger):
97
 
    """Merge individual files when self.file_matches returns True.
98
 
 
99
 
    This class is intended to be subclassed.  The file_matches and
100
 
    merge_matching methods should be overridden with concrete implementations.
101
 
    """
102
 
 
103
 
    def file_matches(self, params):
104
 
        """Return True if merge_matching should be called on this file.
105
 
 
106
 
        Only called with merges of plain files with no clear winner.
107
 
 
108
 
        Subclasses must override this.
109
 
        """
110
 
        raise NotImplementedError(self.file_matches)
111
 
 
112
 
    def get_filename(self, params, tree):
113
 
        """Lookup the filename (i.e. basename, not path), given a Tree (e.g.
114
 
        self.merger.this_tree) and a MergeHookParams.
115
 
        """
116
 
        return osutils.basename(tree.id2path(params.file_id))
117
 
 
118
 
    def get_filepath(self, params, tree):
119
 
        """Calculate the path to the file in a tree.
120
 
 
121
 
        :param params: A MergeHookParams describing the file to merge
122
 
        :param tree: a Tree, e.g. self.merger.this_tree.
123
 
        """
124
 
        return tree.id2path(params.file_id)
125
 
 
126
 
    def merge_contents(self, params):
127
 
        """Merge the contents of a single file."""
128
 
        # Check whether this custom merge logic should be used.
129
 
        if (
130
 
            # OTHER is a straight winner, rely on default merge.
131
 
            params.winner == 'other' or
132
 
            # THIS and OTHER aren't both files.
133
 
            not params.is_file_merge() or
134
 
            # The filename doesn't match *.xml
135
 
            not self.file_matches(params)):
136
 
            return 'not_applicable', None
137
 
        return self.merge_matching(params)
138
 
 
139
 
    def merge_matching(self, params):
140
 
        """Merge the contents of a single file that has matched the criteria
141
 
        in PerFileMerger.merge_contents (is a conflict, is a file,
142
 
        self.file_matches is True).
143
 
 
144
 
        Subclasses must override this.
145
 
        """
146
 
        raise NotImplementedError(self.merge_matching)
147
 
 
148
 
 
149
 
class ConfigurableFileMerger(PerFileMerger):
 
97
class ConfigurableFileMerger(AbstractPerFileMerger):
150
98
    """Merge individual files when configured via a .conf file.
151
99
 
152
100
    This is a base class for concrete custom file merging logic. Concrete
175
123
        if self.name_prefix is None:
176
124
            raise ValueError("name_prefix must be set.")
177
125
 
178
 
    def file_matches(self, params):
 
126
    def filename_matches_config(self, params):
179
127
        """Check whether the file should call the merge hook.
180
128
 
181
129
        <name_prefix>_merge_files configuration variable is a list of files
183
131
        """
184
132
        affected_files = self.affected_files
185
133
        if affected_files is None:
186
 
            config = self.merger.this_branch.get_config()
 
134
            config = self.merger.this_tree.branch.get_config()
187
135
            # Until bzr provides a better policy for caching the config, we
188
136
            # just add the part we're interested in to the params to avoid
189
137
            # reading the config files repeatedly (bazaar.conf, location.conf,
195
143
                affected_files = self.default_files
196
144
            self.affected_files = affected_files
197
145
        if affected_files:
198
 
            filepath = self.get_filepath(params, self.merger.this_tree)
199
 
            if filepath in affected_files:
 
146
            filename = self.merger.this_tree.id2path(params.file_id)
 
147
            if filename in affected_files:
200
148
                return True
201
149
        return False
202
150
 
203
 
    def merge_matching(self, params):
 
151
    def merge_contents(self, params):
 
152
        """Merge the contents of a single file."""
 
153
        # First, check whether this custom merge logic should be used.  We
 
154
        # expect most files should not be merged by this handler.
 
155
        if (
 
156
            # OTHER is a straight winner, rely on default merge.
 
157
            params.winner == 'other' or
 
158
            # THIS and OTHER aren't both files.
 
159
            not params.is_file_merge() or
 
160
            # The filename isn't listed in the 'NAME_merge_files' config
 
161
            # option.
 
162
            not self.filename_matches_config(params)):
 
163
            return 'not_applicable', None
204
164
        return self.merge_text(params)
205
165
 
206
166
    def merge_text(self, params):
495
455
    def _add_parent(self):
496
456
        new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
497
457
        new_parent_trees = []
498
 
        operation = OperationWithCleanups(self.this_tree.set_parent_trees)
499
458
        for revision_id in new_parents:
500
459
            try:
501
460
                tree = self.revision_tree(revision_id)
503
462
                tree = None
504
463
            else:
505
464
                tree.lock_read()
506
 
                operation.add_cleanup(tree.unlock)
507
465
            new_parent_trees.append((revision_id, tree))
508
 
        operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
 
466
        try:
 
467
            self.this_tree.set_parent_trees(new_parent_trees,
 
468
                                            allow_leftmost_as_ghost=True)
 
469
        finally:
 
470
            for _revision_id, tree in new_parent_trees:
 
471
                if tree is not None:
 
472
                    tree.unlock()
509
473
 
510
474
    def set_other(self, other_revision, possible_transports=None):
511
475
        """Set the revision and tree to merge from.
662
626
                               change_reporter=self.change_reporter,
663
627
                               **kwargs)
664
628
 
665
 
    def _do_merge_to(self):
666
 
        merge = self.make_merger()
 
629
    def _do_merge_to(self, merge):
667
630
        if self.other_branch is not None:
668
631
            self.other_branch.update_references(self.this_branch)
669
632
        merge.do_merge()
683
646
                    sub_tree.branch.repository.revision_tree(base_revision)
684
647
                sub_merge.base_rev_id = base_revision
685
648
                sub_merge.do_merge()
686
 
        return merge
687
649
 
688
650
    def do_merge(self):
689
 
        operation = OperationWithCleanups(self._do_merge_to)
690
651
        self.this_tree.lock_tree_write()
691
 
        operation.add_cleanup(self.this_tree.unlock)
692
 
        if self.base_tree is not None:
693
 
            self.base_tree.lock_read()
694
 
            operation.add_cleanup(self.base_tree.unlock)
695
 
        if self.other_tree is not None:
696
 
            self.other_tree.lock_read()
697
 
            operation.add_cleanup(self.other_tree.unlock)
698
 
        merge = operation.run_simple()
 
652
        try:
 
653
            if self.base_tree is not None:
 
654
                self.base_tree.lock_read()
 
655
            try:
 
656
                if self.other_tree is not None:
 
657
                    self.other_tree.lock_read()
 
658
                try:
 
659
                    merge = self.make_merger()
 
660
                    self._do_merge_to(merge)
 
661
                finally:
 
662
                    if self.other_tree is not None:
 
663
                        self.other_tree.unlock()
 
664
            finally:
 
665
                if self.base_tree is not None:
 
666
                    self.base_tree.unlock()
 
667
        finally:
 
668
            self.this_tree.unlock()
699
669
        if len(merge.cooked_conflicts) == 0:
700
670
            if not self.ignore_zero and not trace.is_quiet():
701
671
                trace.note("All changes applied successfully.")
745
715
        :param this_tree: The local tree in the merge operation
746
716
        :param base_tree: The common tree in the merge operation
747
717
        :param other_tree: The other tree to merge changes from
748
 
        :param this_branch: The branch associated with this_tree.  Defaults to
749
 
            this_tree.branch if not supplied.
 
718
        :param this_branch: The branch associated with this_tree
750
719
        :param interesting_ids: The file_ids of files that should be
751
720
            participate in the merge.  May not be combined with
752
721
            interesting_files.
770
739
        if interesting_files is not None and interesting_ids is not None:
771
740
            raise ValueError(
772
741
                'specify either interesting_ids or interesting_files')
773
 
        if this_branch is None:
774
 
            this_branch = this_tree.branch
775
742
        self.interesting_ids = interesting_ids
776
743
        self.interesting_files = interesting_files
777
744
        self.this_tree = working_tree
798
765
            warnings.warn("pb argument to Merge3Merger is deprecated")
799
766
 
800
767
    def do_merge(self):
801
 
        operation = OperationWithCleanups(self._do_merge)
802
768
        self.this_tree.lock_tree_write()
803
 
        operation.add_cleanup(self.this_tree.unlock)
804
769
        self.base_tree.lock_read()
805
 
        operation.add_cleanup(self.base_tree.unlock)
806
770
        self.other_tree.lock_read()
807
 
        operation.add_cleanup(self.other_tree.unlock)
808
 
        operation.run()
809
 
 
810
 
    def _do_merge(self, operation):
811
 
        self.tt = transform.TreeTransform(self.this_tree, None)
812
 
        operation.add_cleanup(self.tt.finalize)
813
 
        self._compute_transform()
814
 
        results = self.tt.apply(no_conflicts=True)
815
 
        self.write_modified(results)
816
771
        try:
817
 
            self.this_tree.add_conflicts(self.cooked_conflicts)
818
 
        except errors.UnsupportedOperation:
819
 
            pass
 
772
            self.tt = transform.TreeTransform(self.this_tree, None)
 
773
            try:
 
774
                self._compute_transform()
 
775
                results = self.tt.apply(no_conflicts=True)
 
776
                self.write_modified(results)
 
777
                try:
 
778
                    self.this_tree.add_conflicts(self.cooked_conflicts)
 
779
                except errors.UnsupportedOperation:
 
780
                    pass
 
781
            finally:
 
782
                self.tt.finalize()
 
783
        finally:
 
784
            self.other_tree.unlock()
 
785
            self.base_tree.unlock()
 
786
            self.this_tree.unlock()
820
787
 
821
788
    def make_preview_transform(self):
822
 
        operation = OperationWithCleanups(self._make_preview_transform)
823
789
        self.base_tree.lock_read()
824
 
        operation.add_cleanup(self.base_tree.unlock)
825
790
        self.other_tree.lock_read()
826
 
        operation.add_cleanup(self.other_tree.unlock)
827
 
        return operation.run_simple()
828
 
 
829
 
    def _make_preview_transform(self):
830
791
        self.tt = transform.TransformPreview(self.this_tree)
831
 
        self._compute_transform()
 
792
        try:
 
793
            self._compute_transform()
 
794
        finally:
 
795
            self.other_tree.unlock()
 
796
            self.base_tree.unlock()
832
797
        return self.tt
833
798
 
834
799
    def _compute_transform(self):
1059
1024
                        continue
1060
1025
                else:
1061
1026
                    raise AssertionError('unhandled kind: %s' % other_ie.kind)
 
1027
                # XXX: We need to handle kind == 'symlink'
1062
1028
 
1063
1029
            # If we have gotten this far, that means something has changed
1064
1030
            result.append((file_id, content_changed,
1086
1052
        other_root = self.tt.trans_id_file_id(other_root_file_id)
1087
1053
        if other_root == self.tt.root:
1088
1054
            return
1089
 
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
1090
 
            # the other tree's root is a non-root in the current tree (as when
1091
 
            # a previously unrelated branch is merged into another)
1092
 
            return
1093
1055
        try:
1094
1056
            self.tt.final_kind(other_root)
1095
 
            other_root_is_present = True
1096
1057
        except errors.NoSuchFile:
1097
 
            # other_root doesn't have a physical representation. We still need
1098
 
            # to move any references to the actual root of the tree.
1099
 
            other_root_is_present = False
1100
 
        # 'other_tree.inventory.root' is not present in this tree. We are
1101
 
        # calling adjust_path for children which *want* to be present with a
1102
 
        # correct place to go.
1103
 
        for thing, child in self.other_tree.inventory.root.children.iteritems():
 
1058
            return
 
1059
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
 
1060
            # the other tree's root is a non-root in the current tree
 
1061
            return
 
1062
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
 
1063
        self.tt.cancel_creation(other_root)
 
1064
        self.tt.cancel_versioning(other_root)
 
1065
 
 
1066
    def reparent_children(self, ie, target):
 
1067
        for thing, child in ie.children.iteritems():
1104
1068
            trans_id = self.tt.trans_id_file_id(child.file_id)
1105
 
            if not other_root_is_present:
1106
 
                # FIXME: Make final_kind returns None instead of raising
1107
 
                # NoSuchFile to avoid the ugly construct below -- vila 20100402
1108
 
                try:
1109
 
                    self.tt.final_kind(trans_id)
1110
 
                    # The item exist in the final tree and has a defined place
1111
 
                    # to go already.
1112
 
                    continue
1113
 
                except errors.NoSuchFile, e:
1114
 
                    pass
1115
 
            # Move the item into the root
1116
 
            self.tt.adjust_path(self.tt.final_name(trans_id),
1117
 
                                self.tt.root, trans_id)
1118
 
        if other_root_is_present:
1119
 
            self.tt.cancel_creation(other_root)
1120
 
            self.tt.cancel_versioning(other_root)
 
1069
            self.tt.adjust_path(self.tt.final_name(trans_id), target, trans_id)
1121
1070
 
1122
1071
    def write_modified(self, results):
1123
1072
        modified_hashes = {}
1170
1119
 
1171
1120
    @staticmethod
1172
1121
    def _three_way(base, other, this):
 
1122
        #if base == other, either they all agree, or only THIS has changed.
1173
1123
        if base == other:
1174
 
            # if 'base == other', either they all agree, or only 'this' has
1175
 
            # changed.
1176
1124
            return 'this'
1177
1125
        elif this not in (base, other):
1178
 
            # 'this' is neither 'base' nor 'other', so both sides changed
1179
1126
            return 'conflict'
 
1127
        # "Ambiguous clean merge" -- both sides have made the same change.
1180
1128
        elif this == other:
1181
 
            # "Ambiguous clean merge" -- both sides have made the same change.
1182
1129
            return "this"
 
1130
        # this == base: only other has changed.
1183
1131
        else:
1184
 
            # this == base: only other has changed.
1185
1132
            return "other"
1186
1133
 
1187
1134
    @staticmethod
1231
1178
                # only has an lca value
1232
1179
                return 'other'
1233
1180
 
1234
 
        # At this point, the lcas disagree, and the tip disagree
 
1181
        # At this point, the lcas disagree, and the tips disagree
1235
1182
        return 'conflict'
1236
1183
 
1237
1184
    @staticmethod
1238
 
    @deprecated_method(deprecated_in((2, 2, 0)))
1239
1185
    def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
1240
1186
        """Do a three-way test on a scalar.
1241
1187
        Return "this", "other" or "conflict", depending whether a value wins.
1291
1237
                parent_id_winner = "other"
1292
1238
        if name_winner == "this" and parent_id_winner == "this":
1293
1239
            return
1294
 
        if name_winner == 'conflict' or parent_id_winner == 'conflict':
1295
 
            # Creating helpers (.OTHER or .THIS) here cause problems down the
1296
 
            # road if a ContentConflict needs to be created so we should not do
1297
 
            # that
1298
 
            trans_id = self.tt.trans_id_file_id(file_id)
1299
 
            self._raw_conflicts.append(('path conflict', trans_id, file_id,
1300
 
                                        this_parent, this_name,
1301
 
                                        other_parent, other_name))
 
1240
        if name_winner == "conflict":
 
1241
            trans_id = self.tt.trans_id_file_id(file_id)
 
1242
            self._raw_conflicts.append(('name conflict', trans_id,
 
1243
                                        this_name, other_name))
 
1244
        if parent_id_winner == "conflict":
 
1245
            trans_id = self.tt.trans_id_file_id(file_id)
 
1246
            self._raw_conflicts.append(('parent conflict', trans_id,
 
1247
                                        this_parent, other_parent))
1302
1248
        if other_name is None:
1303
1249
            # it doesn't matter whether the result was 'other' or
1304
1250
            # 'conflict'-- if there's no 'other', we leave it alone.
1305
1251
            return
 
1252
        # if we get here, name_winner and parent_winner are set to safe values.
 
1253
        trans_id = self.tt.trans_id_file_id(file_id)
1306
1254
        parent_id = parents[self.winner_idx[parent_id_winner]]
1307
1255
        if parent_id is not None:
1308
 
            # if we get here, name_winner and parent_winner are set to safe
1309
 
            # values.
 
1256
            parent_trans_id = self.tt.trans_id_file_id(parent_id)
1310
1257
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1311
 
                                self.tt.trans_id_file_id(parent_id),
1312
 
                                self.tt.trans_id_file_id(file_id))
 
1258
                                parent_trans_id, trans_id)
1313
1259
 
1314
1260
    def _do_merge_contents(self, file_id):
1315
1261
        """Performs a merge on file_id contents."""
1594
1540
 
1595
1541
    def cook_conflicts(self, fs_conflicts):
1596
1542
        """Convert all conflicts into a form that doesn't depend on trans_id"""
 
1543
        name_conflicts = {}
1597
1544
        self.cooked_conflicts.extend(transform.cook_conflicts(
1598
1545
                fs_conflicts, self.tt))
1599
1546
        fp = transform.FinalPaths(self.tt)
1600
1547
        for conflict in self._raw_conflicts:
1601
1548
            conflict_type = conflict[0]
1602
 
            if conflict_type == 'path conflict':
1603
 
                (trans_id, file_id,
1604
 
                this_parent, this_name,
1605
 
                other_parent, other_name) = conflict[1:]
1606
 
                if this_parent is None or this_name is None:
1607
 
                    this_path = '<deleted>'
1608
 
                else:
1609
 
                    parent_path =  fp.get_path(
1610
 
                        self.tt.trans_id_file_id(this_parent))
1611
 
                    this_path = osutils.pathjoin(parent_path, this_name)
1612
 
                if other_parent is None or other_name is None:
1613
 
                    other_path = '<deleted>'
1614
 
                else:
1615
 
                    parent_path =  fp.get_path(
1616
 
                        self.tt.trans_id_file_id(other_parent))
1617
 
                    other_path = osutils.pathjoin(parent_path, other_name)
1618
 
                c = _mod_conflicts.Conflict.factory(
1619
 
                    'path conflict', path=this_path,
1620
 
                    conflict_path=other_path,
1621
 
                    file_id=file_id)
1622
 
            elif conflict_type == 'contents conflict':
 
1549
            if conflict_type in ('name conflict', 'parent conflict'):
 
1550
                trans_id = conflict[1]
 
1551
                conflict_args = conflict[2:]
 
1552
                if trans_id not in name_conflicts:
 
1553
                    name_conflicts[trans_id] = {}
 
1554
                transform.unique_add(name_conflicts[trans_id], conflict_type,
 
1555
                                     conflict_args)
 
1556
            if conflict_type == 'contents conflict':
1623
1557
                for trans_id in conflict[1]:
1624
1558
                    file_id = self.tt.final_file_id(trans_id)
1625
1559
                    if file_id is not None:
1631
1565
                        break
1632
1566
                c = _mod_conflicts.Conflict.factory(conflict_type,
1633
1567
                                                    path=path, file_id=file_id)
1634
 
            elif conflict_type == 'text conflict':
 
1568
                self.cooked_conflicts.append(c)
 
1569
            if conflict_type == 'text conflict':
1635
1570
                trans_id = conflict[1]
1636
1571
                path = fp.get_path(trans_id)
1637
1572
                file_id = self.tt.final_file_id(trans_id)
1638
1573
                c = _mod_conflicts.Conflict.factory(conflict_type,
1639
1574
                                                    path=path, file_id=file_id)
 
1575
                self.cooked_conflicts.append(c)
 
1576
 
 
1577
        for trans_id, conflicts in name_conflicts.iteritems():
 
1578
            try:
 
1579
                this_parent, other_parent = conflicts['parent conflict']
 
1580
                if this_parent == other_parent:
 
1581
                    raise AssertionError()
 
1582
            except KeyError:
 
1583
                this_parent = other_parent = \
 
1584
                    self.tt.final_file_id(self.tt.final_parent(trans_id))
 
1585
            try:
 
1586
                this_name, other_name = conflicts['name conflict']
 
1587
                if this_name == other_name:
 
1588
                    raise AssertionError()
 
1589
            except KeyError:
 
1590
                this_name = other_name = self.tt.final_name(trans_id)
 
1591
            other_path = fp.get_path(trans_id)
 
1592
            if this_parent is not None and this_name is not None:
 
1593
                this_parent_path = \
 
1594
                    fp.get_path(self.tt.trans_id_file_id(this_parent))
 
1595
                this_path = osutils.pathjoin(this_parent_path, this_name)
1640
1596
            else:
1641
 
                raise AssertionError('bad conflict type: %r' % (conflict,))
 
1597
                this_path = "<deleted>"
 
1598
            file_id = self.tt.final_file_id(trans_id)
 
1599
            c = _mod_conflicts.Conflict.factory('path conflict', path=this_path,
 
1600
                                                conflict_path=other_path,
 
1601
                                                file_id=file_id)
1642
1602
            self.cooked_conflicts.append(c)
1643
1603
        self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1644
1604