/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Martin Pool
  • Date: 2010-02-09 19:04:02 UTC
  • mfrom: (5010 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5019.
  • Revision ID: mbp@canonical.com-20100209190402-2xbzrchmb4dfi2j7
Resolve conflicts with trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2008 Canonical Ltd
 
1
# Copyright (C) 2005-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
20
20
    branch as _mod_branch,
21
21
    conflicts as _mod_conflicts,
22
22
    debug,
 
23
    decorators,
23
24
    errors,
24
25
    graph as _mod_graph,
 
26
    hooks,
25
27
    merge3,
26
28
    osutils,
27
29
    patiencediff,
51
53
        from_tree.unlock()
52
54
 
53
55
 
 
56
class MergeHooks(hooks.Hooks):
 
57
 
 
58
    def __init__(self):
 
59
        hooks.Hooks.__init__(self)
 
60
        self.create_hook(hooks.HookPoint('merge_file_content',
 
61
            "Called with a bzrlib.merge.Merger object to create a per file "
 
62
            "merge object when starting a merge. "
 
63
            "Should return either None or a subclass of "
 
64
            "``bzrlib.merge.AbstractPerFileMerger``. "
 
65
            "Such objects will then be called per file "
 
66
            "that needs to be merged (including when one "
 
67
            "side has deleted the file and the other has changed it). "
 
68
            "See the AbstractPerFileMerger API docs for details on how it is "
 
69
            "used by merge.",
 
70
            (2, 1), None))
 
71
 
 
72
 
 
73
class AbstractPerFileMerger(object):
 
74
    """PerFileMerger objects are used by plugins extending merge for bzrlib.
 
75
 
 
76
    See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
 
77
    
 
78
    :ivar merger: The Merge3Merger performing the merge.
 
79
    """
 
80
 
 
81
    def __init__(self, merger):
 
82
        """Create a PerFileMerger for use with merger."""
 
83
        self.merger = merger
 
84
 
 
85
    def merge_contents(self, merge_params):
 
86
        """Attempt to merge the contents of a single file.
 
87
        
 
88
        :param merge_params: A bzrlib.merge.MergeHookParams
 
89
        :return : A tuple of (status, chunks), where status is one of
 
90
            'not_applicable', 'success', 'conflicted', or 'delete'.  If status
 
91
            is 'success' or 'conflicted', then chunks should be an iterable of
 
92
            strings for the new file contents.
 
93
        """
 
94
        return ('not applicable', None)
 
95
 
 
96
 
 
97
class ConfigurableFileMerger(AbstractPerFileMerger):
 
98
    """Merge individual files when configured via a .conf file.
 
99
 
 
100
    This is a base class for concrete custom file merging logic. Concrete
 
101
    classes should implement ``merge_text``.
 
102
 
 
103
    See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
 
104
    
 
105
    :ivar affected_files: The configured file paths to merge.
 
106
 
 
107
    :cvar name_prefix: The prefix to use when looking up configuration
 
108
        details. <name_prefix>_merge_files describes the files targeted by the
 
109
        hook for example.
 
110
        
 
111
    :cvar default_files: The default file paths to merge when no configuration
 
112
        is present.
 
113
    """
 
114
 
 
115
    name_prefix = None
 
116
    default_files = None
 
117
 
 
118
    def __init__(self, merger):
 
119
        super(ConfigurableFileMerger, self).__init__(merger)
 
120
        self.affected_files = None
 
121
        self.default_files = self.__class__.default_files or []
 
122
        self.name_prefix = self.__class__.name_prefix
 
123
        if self.name_prefix is None:
 
124
            raise ValueError("name_prefix must be set.")
 
125
 
 
126
    def filename_matches_config(self, params):
 
127
        """Check whether the file should call the merge hook.
 
128
 
 
129
        <name_prefix>_merge_files configuration variable is a list of files
 
130
        that should use the hook.
 
131
        """
 
132
        affected_files = self.affected_files
 
133
        if affected_files is None:
 
134
            config = self.merger.this_tree.branch.get_config()
 
135
            # Until bzr provides a better policy for caching the config, we
 
136
            # just add the part we're interested in to the params to avoid
 
137
            # reading the config files repeatedly (bazaar.conf, location.conf,
 
138
            # branch.conf).
 
139
            config_key = self.name_prefix + '_merge_files'
 
140
            affected_files = config.get_user_option_as_list(config_key)
 
141
            if affected_files is None:
 
142
                # If nothing was specified in the config, use the default.
 
143
                affected_files = self.default_files
 
144
            self.affected_files = affected_files
 
145
        if affected_files:
 
146
            filename = self.merger.this_tree.id2path(params.file_id)
 
147
            if filename in affected_files:
 
148
                return True
 
149
        return False
 
150
 
 
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
 
164
        return self.merge_text(params)
 
165
 
 
166
    def merge_text(self, params):
 
167
        """Merge the byte contents of a single file.
 
168
 
 
169
        This is called after checking that the merge should be performed in
 
170
        merge_contents, and it should behave as per
 
171
        ``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
 
172
        """
 
173
        raise NotImplementedError(self.merge_text)
 
174
 
 
175
 
 
176
class MergeHookParams(object):
 
177
    """Object holding parameters passed to merge_file_content hooks.
 
178
 
 
179
    There are some fields hooks can access:
 
180
 
 
181
    :ivar file_id: the file ID of the file being merged
 
182
    :ivar trans_id: the transform ID for the merge of this file
 
183
    :ivar this_kind: kind of file_id in 'this' tree
 
184
    :ivar other_kind: kind of file_id in 'other' tree
 
185
    :ivar winner: one of 'this', 'other', 'conflict'
 
186
    """
 
187
 
 
188
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
 
189
            winner):
 
190
        self._merger = merger
 
191
        self.file_id = file_id
 
192
        self.trans_id = trans_id
 
193
        self.this_kind = this_kind
 
194
        self.other_kind = other_kind
 
195
        self.winner = winner
 
196
 
 
197
    def is_file_merge(self):
 
198
        """True if this_kind and other_kind are both 'file'."""
 
199
        return self.this_kind == 'file' and self.other_kind == 'file'
 
200
 
 
201
    @decorators.cachedproperty
 
202
    def base_lines(self):
 
203
        """The lines of the 'base' version of the file."""
 
204
        return self._merger.get_lines(self._merger.base_tree, self.file_id)
 
205
 
 
206
    @decorators.cachedproperty
 
207
    def this_lines(self):
 
208
        """The lines of the 'this' version of the file."""
 
209
        return self._merger.get_lines(self._merger.this_tree, self.file_id)
 
210
 
 
211
    @decorators.cachedproperty
 
212
    def other_lines(self):
 
213
        """The lines of the 'other' version of the file."""
 
214
        return self._merger.get_lines(self._merger.other_tree, self.file_id)
 
215
 
 
216
 
54
217
class Merger(object):
 
218
 
 
219
    hooks = MergeHooks()
 
220
 
55
221
    def __init__(self, this_branch, other_tree=None, base_tree=None,
56
222
                 this_tree=None, pb=None, change_reporter=None,
57
223
                 recurse='down', revision_graph=None):
432
598
                  'other_tree': self.other_tree,
433
599
                  'interesting_ids': self.interesting_ids,
434
600
                  'interesting_files': self.interesting_files,
435
 
                  'pp': self.pp,
 
601
                  'this_branch': self.this_branch,
436
602
                  'do_merge': False}
437
603
        if self.merge_type.requires_base:
438
604
            kwargs['base_tree'] = self.base_tree
542
708
                 interesting_ids=None, reprocess=False, show_base=False,
543
709
                 pb=None, pp=None, change_reporter=None,
544
710
                 interesting_files=None, do_merge=True,
545
 
                 cherrypick=False, lca_trees=None):
 
711
                 cherrypick=False, lca_trees=None, this_branch=None):
546
712
        """Initialize the merger object and perform the merge.
547
713
 
548
714
        :param working_tree: The working tree to apply the merge to
549
715
        :param this_tree: The local tree in the merge operation
550
716
        :param base_tree: The common tree in the merge operation
551
717
        :param other_tree: The other tree to merge changes from
 
718
        :param this_branch: The branch associated with this_tree
552
719
        :param interesting_ids: The file_ids of files that should be
553
720
            participate in the merge.  May not be combined with
554
721
            interesting_files.
577
744
        self.this_tree = working_tree
578
745
        self.base_tree = base_tree
579
746
        self.other_tree = other_tree
 
747
        self.this_branch = this_branch
580
748
        self._raw_conflicts = []
581
749
        self.cooked_conflicts = []
582
750
        self.reprocess = reprocess
637
805
            resolver = self._lca_multi_way
638
806
        child_pb = ui.ui_factory.nested_progress_bar()
639
807
        try:
 
808
            factories = Merger.hooks['merge_file_content']
 
809
            hooks = [factory(self) for factory in factories] + [self]
 
810
            self.active_hooks = [hook for hook in hooks if hook is not None]
640
811
            for num, (file_id, changed, parents3, names3,
641
812
                      executable3) in enumerate(entries):
642
813
                child_pb.update('Preparing file merge', num, len(entries))
643
814
                self._merge_names(file_id, parents3, names3, resolver=resolver)
644
815
                if changed:
645
 
                    file_status = self.merge_contents(file_id)
 
816
                    file_status = self._do_merge_contents(file_id)
646
817
                else:
647
818
                    file_status = 'unmodified'
648
819
                self._merge_executable(file_id,
885
1056
            self.tt.final_kind(other_root)
886
1057
        except errors.NoSuchFile:
887
1058
            return
888
 
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
 
1059
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
889
1060
            # the other tree's root is a non-root in the current tree
890
1061
            return
891
1062
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
933
1104
    @staticmethod
934
1105
    def executable(tree, file_id):
935
1106
        """Determine the executability of a file-id (used as a key method)."""
936
 
        if file_id not in tree:
 
1107
        if not tree.has_id(file_id):
937
1108
            return None
938
1109
        if tree.kind(file_id) != "file":
939
1110
            return False
942
1113
    @staticmethod
943
1114
    def kind(tree, file_id):
944
1115
        """Determine the kind of a file-id (used as a key method)."""
945
 
        if file_id not in tree:
 
1116
        if not tree.has_id(file_id):
946
1117
            return None
947
1118
        return tree.kind(file_id)
948
1119
 
1031
1202
 
1032
1203
    def merge_names(self, file_id):
1033
1204
        def get_entry(tree):
1034
 
            if file_id in tree.inventory:
 
1205
            if tree.has_id(file_id):
1035
1206
                return tree.inventory[file_id]
1036
1207
            else:
1037
1208
                return None
1086
1257
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1087
1258
                                parent_trans_id, trans_id)
1088
1259
 
1089
 
    def merge_contents(self, file_id):
 
1260
    def _do_merge_contents(self, file_id):
1090
1261
        """Performs a merge on file_id contents."""
1091
1262
        def contents_pair(tree):
1092
1263
            if file_id not in tree:
1100
1271
                contents = None
1101
1272
            return kind, contents
1102
1273
 
1103
 
        def contents_conflict():
1104
 
            trans_id = self.tt.trans_id_file_id(file_id)
1105
 
            name = self.tt.final_name(trans_id)
1106
 
            parent_id = self.tt.final_parent(trans_id)
1107
 
            if file_id in self.this_tree.inventory:
1108
 
                self.tt.unversion_file(trans_id)
1109
 
                if file_id in self.this_tree:
1110
 
                    self.tt.delete_contents(trans_id)
1111
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
1112
 
                                              set_version=True)
1113
 
            self._raw_conflicts.append(('contents conflict', file_group))
1114
 
 
1115
1274
        # See SPOT run.  run, SPOT, run.
1116
1275
        # So we're not QUITE repeating ourselves; we do tricky things with
1117
1276
        # file kind...
1133
1292
        if winner == 'this':
1134
1293
            # No interesting changes introduced by OTHER
1135
1294
            return "unmodified"
 
1295
        # We have a hypothetical conflict, but if we have files, then we
 
1296
        # can try to merge the content
1136
1297
        trans_id = self.tt.trans_id_file_id(file_id)
1137
 
        if winner == 'other':
 
1298
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
 
1299
            other_pair[0], winner)
 
1300
        hooks = self.active_hooks
 
1301
        hook_status = 'not_applicable'
 
1302
        for hook in hooks:
 
1303
            hook_status, lines = hook.merge_contents(params)
 
1304
            if hook_status != 'not_applicable':
 
1305
                # Don't try any more hooks, this one applies.
 
1306
                break
 
1307
        result = "modified"
 
1308
        if hook_status == 'not_applicable':
 
1309
            # This is a contents conflict, because none of the available
 
1310
            # functions could merge it.
 
1311
            result = None
 
1312
            name = self.tt.final_name(trans_id)
 
1313
            parent_id = self.tt.final_parent(trans_id)
 
1314
            if self.this_tree.has_id(file_id):
 
1315
                self.tt.unversion_file(trans_id)
 
1316
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1317
                                              set_version=True)
 
1318
            self._raw_conflicts.append(('contents conflict', file_group))
 
1319
        elif hook_status == 'success':
 
1320
            self.tt.create_file(lines, trans_id)
 
1321
        elif hook_status == 'conflicted':
 
1322
            # XXX: perhaps the hook should be able to provide
 
1323
            # the BASE/THIS/OTHER files?
 
1324
            self.tt.create_file(lines, trans_id)
 
1325
            self._raw_conflicts.append(('text conflict', trans_id))
 
1326
            name = self.tt.final_name(trans_id)
 
1327
            parent_id = self.tt.final_parent(trans_id)
 
1328
            self._dump_conflicts(name, parent_id, file_id)
 
1329
        elif hook_status == 'delete':
 
1330
            self.tt.unversion_file(trans_id)
 
1331
            result = "deleted"
 
1332
        elif hook_status == 'done':
 
1333
            # The hook function did whatever it needs to do directly, no
 
1334
            # further action needed here.
 
1335
            pass
 
1336
        else:
 
1337
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
 
1338
        if not self.this_tree.has_id(file_id) and result == "modified":
 
1339
            self.tt.version_file(file_id, trans_id)
 
1340
        # The merge has been performed, so the old contents should not be
 
1341
        # retained.
 
1342
        try:
 
1343
            self.tt.delete_contents(trans_id)
 
1344
        except errors.NoSuchFile:
 
1345
            pass
 
1346
        return result
 
1347
 
 
1348
    def _default_other_winner_merge(self, merge_hook_params):
 
1349
        """Replace this contents with other."""
 
1350
        file_id = merge_hook_params.file_id
 
1351
        trans_id = merge_hook_params.trans_id
 
1352
        file_in_this = self.this_tree.has_id(file_id)
 
1353
        if self.other_tree.has_id(file_id):
 
1354
            # OTHER changed the file
 
1355
            wt = self.this_tree
 
1356
            if wt.supports_content_filtering():
 
1357
                # We get the path from the working tree if it exists.
 
1358
                # That fails though when OTHER is adding a file, so
 
1359
                # we fall back to the other tree to find the path if
 
1360
                # it doesn't exist locally.
 
1361
                try:
 
1362
                    filter_tree_path = wt.id2path(file_id)
 
1363
                except errors.NoSuchId:
 
1364
                    filter_tree_path = self.other_tree.id2path(file_id)
 
1365
            else:
 
1366
                # Skip the id2path lookup for older formats
 
1367
                filter_tree_path = None
 
1368
            transform.create_from_tree(self.tt, trans_id,
 
1369
                             self.other_tree, file_id,
 
1370
                             filter_tree_path=filter_tree_path)
 
1371
            return 'done', None
 
1372
        elif file_in_this:
 
1373
            # OTHER deleted the file
 
1374
            return 'delete', None
 
1375
        else:
 
1376
            raise AssertionError(
 
1377
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
 
1378
                % (file_id,))
 
1379
 
 
1380
    def merge_contents(self, merge_hook_params):
 
1381
        """Fallback merge logic after user installed hooks."""
 
1382
        # This function is used in merge hooks as the fallback instance.
 
1383
        # Perhaps making this function and the functions it calls be a 
 
1384
        # a separate class would be better.
 
1385
        if merge_hook_params.winner == 'other':
1138
1386
            # OTHER is a straight winner, so replace this contents with other
1139
 
            file_in_this = file_id in self.this_tree
1140
 
            if file_in_this:
1141
 
                # Remove any existing contents
1142
 
                self.tt.delete_contents(trans_id)
1143
 
            if file_id in self.other_tree:
1144
 
                # OTHER changed the file
1145
 
                wt = self.this_tree
1146
 
                if wt.supports_content_filtering():
1147
 
                    # We get the path from the working tree if it exists.
1148
 
                    # That fails though when OTHER is adding a file, so
1149
 
                    # we fall back to the other tree to find the path if
1150
 
                    # it doesn't exist locally.
1151
 
                    try:
1152
 
                        filter_tree_path = wt.id2path(file_id)
1153
 
                    except errors.NoSuchId:
1154
 
                        filter_tree_path = self.other_tree.id2path(file_id)
1155
 
                else:
1156
 
                    # Skip the id2path lookup for older formats
1157
 
                    filter_tree_path = None
1158
 
                transform.create_from_tree(self.tt, trans_id,
1159
 
                                 self.other_tree, file_id,
1160
 
                                 filter_tree_path=filter_tree_path)
1161
 
                if not file_in_this:
1162
 
                    self.tt.version_file(file_id, trans_id)
1163
 
                return "modified"
1164
 
            elif file_in_this:
1165
 
                # OTHER deleted the file
1166
 
                self.tt.unversion_file(trans_id)
1167
 
                return "deleted"
 
1387
            return self._default_other_winner_merge(merge_hook_params)
 
1388
        elif merge_hook_params.is_file_merge():
 
1389
            # THIS and OTHER are both files, so text merge.  Either
 
1390
            # BASE is a file, or both converted to files, so at least we
 
1391
            # have agreement that output should be a file.
 
1392
            try:
 
1393
                self.text_merge(merge_hook_params.file_id,
 
1394
                    merge_hook_params.trans_id)
 
1395
            except errors.BinaryFile:
 
1396
                return 'not_applicable', None
 
1397
            return 'done', None
1168
1398
        else:
1169
 
            # We have a hypothetical conflict, but if we have files, then we
1170
 
            # can try to merge the content
1171
 
            if this_pair[0] == 'file' and other_pair[0] == 'file':
1172
 
                # THIS and OTHER are both files, so text merge.  Either
1173
 
                # BASE is a file, or both converted to files, so at least we
1174
 
                # have agreement that output should be a file.
1175
 
                try:
1176
 
                    self.text_merge(file_id, trans_id)
1177
 
                except errors.BinaryFile:
1178
 
                    return contents_conflict()
1179
 
                if file_id not in self.this_tree:
1180
 
                    self.tt.version_file(file_id, trans_id)
1181
 
                try:
1182
 
                    self.tt.tree_kind(trans_id)
1183
 
                    self.tt.delete_contents(trans_id)
1184
 
                except errors.NoSuchFile:
1185
 
                    pass
1186
 
                return "modified"
1187
 
            else:
1188
 
                return contents_conflict()
 
1399
            return 'not_applicable', None
1189
1400
 
1190
1401
    def get_lines(self, tree, file_id):
1191
1402
        """Return the lines in a file, or an empty list."""
1192
 
        if file_id in tree:
 
1403
        if tree.has_id(file_id):
1193
1404
            return tree.get_file(file_id).readlines()
1194
1405
        else:
1195
1406
            return []
1198
1409
        """Perform a three-way text merge on a file_id"""
1199
1410
        # it's possible that we got here with base as a different type.
1200
1411
        # if so, we just want two-way text conflicts.
1201
 
        if file_id in self.base_tree and \
 
1412
        if self.base_tree.has_id(file_id) and \
1202
1413
            self.base_tree.kind(file_id) == "file":
1203
1414
            base_lines = self.get_lines(self.base_tree, file_id)
1204
1415
        else:
1267
1478
        versioned = False
1268
1479
        file_group = []
1269
1480
        for suffix, tree, lines in data:
1270
 
            if file_id in tree:
 
1481
            if tree.has_id(file_id):
1271
1482
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1272
1483
                                               suffix, lines, filter_tree_path)
1273
1484
                file_group.append(trans_id)
1317
1528
        if winner == "this":
1318
1529
            executability = this_executable
1319
1530
        else:
1320
 
            if file_id in self.other_tree:
 
1531
            if self.other_tree.has_id(file_id):
1321
1532
                executability = other_executable
1322
 
            elif file_id in self.this_tree:
 
1533
            elif self.this_tree.has_id(file_id):
1323
1534
                executability = this_executable
1324
 
            elif file_id in self.base_tree:
 
1535
            elif self.base_tree_has_id(file_id):
1325
1536
                executability = base_executable
1326
1537
        if executability is not None:
1327
1538
            trans_id = self.tt.trans_id_file_id(file_id)