~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

Merged bzr.dev into cmdline-parser.

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
19
19
    branch as _mod_branch,
20
20
    conflicts as _mod_conflicts,
21
21
    debug,
 
22
    decorators,
22
23
    errors,
23
24
    graph as _mod_graph,
 
25
    hooks,
24
26
    merge3,
25
27
    osutils,
26
28
    patiencediff,
50
52
        from_tree.unlock()
51
53
 
52
54
 
 
55
class MergeHooks(hooks.Hooks):
 
56
 
 
57
    def __init__(self):
 
58
        hooks.Hooks.__init__(self)
 
59
        self.create_hook(hooks.HookPoint('merge_file_content',
 
60
            "Called with a bzrlib.merge.Merger object to create a per file "
 
61
            "merge object when starting a merge. "
 
62
            "Should return either None or a subclass of "
 
63
            "``bzrlib.merge.AbstractPerFileMerger``. "
 
64
            "Such objects will then be called per file "
 
65
            "that needs to be merged (including when one "
 
66
            "side has deleted the file and the other has changed it). "
 
67
            "See the AbstractPerFileMerger API docs for details on how it is "
 
68
            "used by merge.",
 
69
            (2, 1), None))
 
70
 
 
71
 
 
72
class AbstractPerFileMerger(object):
 
73
    """PerFileMerger objects are used by plugins extending merge for bzrlib.
 
74
 
 
75
    See ``bzrlib.plugins.news_merge.news_merge`` for an example concrete class.
 
76
    
 
77
    :ivar merger: The Merge3Merger performing the merge.
 
78
    """
 
79
 
 
80
    def __init__(self, merger):
 
81
        """Create a PerFileMerger for use with merger."""
 
82
        self.merger = merger
 
83
 
 
84
    def merge_contents(self, merge_params):
 
85
        """Attempt to merge the contents of a single file.
 
86
        
 
87
        :param merge_params: A bzrlib.merge.MergeHookParams
 
88
        :return : A tuple of (status, chunks), where status is one of
 
89
            'not_applicable', 'success', 'conflicted', or 'delete'.  If status
 
90
            is 'success' or 'conflicted', then chunks should be an iterable of
 
91
            strings for the new file contents.
 
92
        """
 
93
        return ('not applicable', None)
 
94
 
 
95
 
 
96
class ConfigurableFileMerger(AbstractPerFileMerger):
 
97
    """Merge individual files when configured via a .conf file.
 
98
 
 
99
    This is a base class for concrete custom file merging logic. Concrete
 
100
    classes should implement ``merge_text``.
 
101
 
 
102
    :ivar affected_files: The configured file paths to merge.
 
103
    :cvar name_prefix: The prefix to use when looking up configuration
 
104
        details.
 
105
    :cvar default_files: The default file paths to merge when no configuration
 
106
        is present.
 
107
    """
 
108
 
 
109
    name_prefix = None
 
110
    default_files = None
 
111
 
 
112
    def __init__(self, merger):
 
113
        super(ConfigurableFileMerger, self).__init__(merger)
 
114
        self.affected_files = None
 
115
        self.default_files = self.__class__.default_files or []
 
116
        self.name_prefix = self.__class__.name_prefix
 
117
        if self.name_prefix is None:
 
118
            raise ValueError("name_prefix must be set.")
 
119
 
 
120
    def filename_matches_config(self, params):
 
121
        affected_files = self.affected_files
 
122
        if affected_files is None:
 
123
            config = self.merger.this_tree.branch.get_config()
 
124
            # Until bzr provides a better policy for caching the config, we
 
125
            # just add the part we're interested in to the params to avoid
 
126
            # reading the config files repeatedly (bazaar.conf, location.conf,
 
127
            # branch.conf).
 
128
            config_key = self.name_prefix + '_merge_files'
 
129
            affected_files = config.get_user_option_as_list(config_key)
 
130
            if affected_files is None:
 
131
                # If nothing was specified in the config, use the default.
 
132
                affected_files = self.default_files
 
133
            self.affected_files = affected_files
 
134
        if affected_files:
 
135
            filename = self.merger.this_tree.id2path(params.file_id)
 
136
            if filename in affected_files:
 
137
                return True
 
138
        return False
 
139
 
 
140
    def merge_contents(self, params):
 
141
        """Merge the contents of a single file."""
 
142
        # First, check whether this custom merge logic should be used.  We
 
143
        # expect most files should not be merged by this handler.
 
144
        if (
 
145
            # OTHER is a straight winner, rely on default merge.
 
146
            params.winner == 'other' or
 
147
            # THIS and OTHER aren't both files.
 
148
            not params.is_file_merge() or
 
149
            # The filename isn't listed in the 'NAME_merge_files' config
 
150
            # option.
 
151
            not self.filename_matches_config(params)):
 
152
            return 'not_applicable', None
 
153
        return self.merge_text(self, params)
 
154
 
 
155
    def merge_text(self, params):
 
156
        """Merge the byte contents of a single file.
 
157
 
 
158
        This is called after checking that the merge should be performed in
 
159
        merge_contents, and it should behave as per
 
160
        ``bzrlib.merge.AbstractPerFileMerger.merge_contents``.
 
161
        """
 
162
        raise NotImplementedError(self.merge_text)
 
163
 
 
164
 
 
165
class MergeHookParams(object):
 
166
    """Object holding parameters passed to merge_file_content hooks.
 
167
 
 
168
    There are some fields hooks can access:
 
169
 
 
170
    :ivar file_id: the file ID of the file being merged
 
171
    :ivar trans_id: the transform ID for the merge of this file
 
172
    :ivar this_kind: kind of file_id in 'this' tree
 
173
    :ivar other_kind: kind of file_id in 'other' tree
 
174
    :ivar winner: one of 'this', 'other', 'conflict'
 
175
    """
 
176
 
 
177
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
 
178
            winner):
 
179
        self._merger = merger
 
180
        self.file_id = file_id
 
181
        self.trans_id = trans_id
 
182
        self.this_kind = this_kind
 
183
        self.other_kind = other_kind
 
184
        self.winner = winner
 
185
 
 
186
    def is_file_merge(self):
 
187
        """True if this_kind and other_kind are both 'file'."""
 
188
        return self.this_kind == 'file' and self.other_kind == 'file'
 
189
 
 
190
    @decorators.cachedproperty
 
191
    def base_lines(self):
 
192
        """The lines of the 'base' version of the file."""
 
193
        return self._merger.get_lines(self._merger.base_tree, self.file_id)
 
194
 
 
195
    @decorators.cachedproperty
 
196
    def this_lines(self):
 
197
        """The lines of the 'this' version of the file."""
 
198
        return self._merger.get_lines(self._merger.this_tree, self.file_id)
 
199
 
 
200
    @decorators.cachedproperty
 
201
    def other_lines(self):
 
202
        """The lines of the 'other' version of the file."""
 
203
        return self._merger.get_lines(self._merger.other_tree, self.file_id)
 
204
 
 
205
 
53
206
class Merger(object):
 
207
 
 
208
    hooks = MergeHooks()
 
209
 
54
210
    def __init__(self, this_branch, other_tree=None, base_tree=None,
55
211
                 this_tree=None, pb=None, change_reporter=None,
56
212
                 recurse='down', revision_graph=None):
432
588
                  'other_tree': self.other_tree,
433
589
                  'interesting_ids': self.interesting_ids,
434
590
                  'interesting_files': self.interesting_files,
435
 
                  'pp': self.pp,
 
591
                  'pp': self.pp, 'this_branch': self.this_branch,
436
592
                  'do_merge': False}
437
593
        if self.merge_type.requires_base:
438
594
            kwargs['base_tree'] = self.base_tree
542
698
                 interesting_ids=None, reprocess=False, show_base=False,
543
699
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
544
700
                 interesting_files=None, do_merge=True,
545
 
                 cherrypick=False, lca_trees=None):
 
701
                 cherrypick=False, lca_trees=None, this_branch=None):
546
702
        """Initialize the merger object and perform the merge.
547
703
 
548
704
        :param working_tree: The working tree to apply the merge to
549
705
        :param this_tree: The local tree in the merge operation
550
706
        :param base_tree: The common tree in the merge operation
551
707
        :param other_tree: The other tree to merge changes from
 
708
        :param this_branch: The branch associated with this_tree
552
709
        :param interesting_ids: The file_ids of files that should be
553
710
            participate in the merge.  May not be combined with
554
711
            interesting_files.
577
734
        self.this_tree = working_tree
578
735
        self.base_tree = base_tree
579
736
        self.other_tree = other_tree
 
737
        self.this_branch = this_branch
580
738
        self._raw_conflicts = []
581
739
        self.cooked_conflicts = []
582
740
        self.reprocess = reprocess
643
801
            resolver = self._lca_multi_way
644
802
        child_pb = ui.ui_factory.nested_progress_bar()
645
803
        try:
 
804
            factories = Merger.hooks['merge_file_content']
 
805
            hooks = [factory(self) for factory in factories] + [self]
 
806
            self.active_hooks = [hook for hook in hooks if hook is not None]
646
807
            for num, (file_id, changed, parents3, names3,
647
808
                      executable3) in enumerate(entries):
648
809
                child_pb.update('Preparing file merge', num, len(entries))
649
810
                self._merge_names(file_id, parents3, names3, resolver=resolver)
650
811
                if changed:
651
 
                    file_status = self.merge_contents(file_id)
 
812
                    file_status = self._do_merge_contents(file_id)
652
813
                else:
653
814
                    file_status = 'unmodified'
654
815
                self._merge_executable(file_id,
892
1053
            self.tt.final_kind(other_root)
893
1054
        except errors.NoSuchFile:
894
1055
            return
895
 
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
 
1056
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
896
1057
            # the other tree's root is a non-root in the current tree
897
1058
            return
898
1059
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
940
1101
    @staticmethod
941
1102
    def executable(tree, file_id):
942
1103
        """Determine the executability of a file-id (used as a key method)."""
943
 
        if file_id not in tree:
 
1104
        if not tree.has_id(file_id):
944
1105
            return None
945
1106
        if tree.kind(file_id) != "file":
946
1107
            return False
949
1110
    @staticmethod
950
1111
    def kind(tree, file_id):
951
1112
        """Determine the kind of a file-id (used as a key method)."""
952
 
        if file_id not in tree:
 
1113
        if not tree.has_id(file_id):
953
1114
            return None
954
1115
        return tree.kind(file_id)
955
1116
 
1038
1199
 
1039
1200
    def merge_names(self, file_id):
1040
1201
        def get_entry(tree):
1041
 
            if file_id in tree.inventory:
 
1202
            if tree.has_id(file_id):
1042
1203
                return tree.inventory[file_id]
1043
1204
            else:
1044
1205
                return None
1093
1254
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1094
1255
                                parent_trans_id, trans_id)
1095
1256
 
1096
 
    def merge_contents(self, file_id):
 
1257
    def _do_merge_contents(self, file_id):
1097
1258
        """Performs a merge on file_id contents."""
1098
1259
        def contents_pair(tree):
1099
1260
            if file_id not in tree:
1107
1268
                contents = None
1108
1269
            return kind, contents
1109
1270
 
1110
 
        def contents_conflict():
1111
 
            trans_id = self.tt.trans_id_file_id(file_id)
1112
 
            name = self.tt.final_name(trans_id)
1113
 
            parent_id = self.tt.final_parent(trans_id)
1114
 
            if file_id in self.this_tree.inventory:
1115
 
                self.tt.unversion_file(trans_id)
1116
 
                if file_id in self.this_tree:
1117
 
                    self.tt.delete_contents(trans_id)
1118
 
            file_group = self._dump_conflicts(name, parent_id, file_id,
1119
 
                                              set_version=True)
1120
 
            self._raw_conflicts.append(('contents conflict', file_group))
1121
 
 
1122
1271
        # See SPOT run.  run, SPOT, run.
1123
1272
        # So we're not QUITE repeating ourselves; we do tricky things with
1124
1273
        # file kind...
1140
1289
        if winner == 'this':
1141
1290
            # No interesting changes introduced by OTHER
1142
1291
            return "unmodified"
 
1292
        # We have a hypothetical conflict, but if we have files, then we
 
1293
        # can try to merge the content
1143
1294
        trans_id = self.tt.trans_id_file_id(file_id)
1144
 
        if winner == 'other':
 
1295
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
 
1296
            other_pair[0], winner)
 
1297
        hooks = self.active_hooks
 
1298
        hook_status = 'not_applicable'
 
1299
        for hook in hooks:
 
1300
            hook_status, lines = hook.merge_contents(params)
 
1301
            if hook_status != 'not_applicable':
 
1302
                # Don't try any more hooks, this one applies.
 
1303
                break
 
1304
        result = "modified"
 
1305
        if hook_status == 'not_applicable':
 
1306
            # This is a contents conflict, because none of the available
 
1307
            # functions could merge it.
 
1308
            result = None
 
1309
            name = self.tt.final_name(trans_id)
 
1310
            parent_id = self.tt.final_parent(trans_id)
 
1311
            if self.this_tree.has_id(file_id):
 
1312
                self.tt.unversion_file(trans_id)
 
1313
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1314
                                              set_version=True)
 
1315
            self._raw_conflicts.append(('contents conflict', file_group))
 
1316
        elif hook_status == 'success':
 
1317
            self.tt.create_file(lines, trans_id)
 
1318
        elif hook_status == 'conflicted':
 
1319
            # XXX: perhaps the hook should be able to provide
 
1320
            # the BASE/THIS/OTHER files?
 
1321
            self.tt.create_file(lines, trans_id)
 
1322
            self._raw_conflicts.append(('text conflict', trans_id))
 
1323
            name = self.tt.final_name(trans_id)
 
1324
            parent_id = self.tt.final_parent(trans_id)
 
1325
            self._dump_conflicts(name, parent_id, file_id)
 
1326
        elif hook_status == 'delete':
 
1327
            self.tt.unversion_file(trans_id)
 
1328
            result = "deleted"
 
1329
        elif hook_status == 'done':
 
1330
            # The hook function did whatever it needs to do directly, no
 
1331
            # further action needed here.
 
1332
            pass
 
1333
        else:
 
1334
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
 
1335
        if not self.this_tree.has_id(file_id) and result == "modified":
 
1336
            self.tt.version_file(file_id, trans_id)
 
1337
        # The merge has been performed, so the old contents should not be
 
1338
        # retained.
 
1339
        try:
 
1340
            self.tt.delete_contents(trans_id)
 
1341
        except errors.NoSuchFile:
 
1342
            pass
 
1343
        return result
 
1344
 
 
1345
    def _default_other_winner_merge(self, merge_hook_params):
 
1346
        """Replace this contents with other."""
 
1347
        file_id = merge_hook_params.file_id
 
1348
        trans_id = merge_hook_params.trans_id
 
1349
        file_in_this = self.this_tree.has_id(file_id)
 
1350
        if self.other_tree.has_id(file_id):
 
1351
            # OTHER changed the file
 
1352
            wt = self.this_tree
 
1353
            if wt.supports_content_filtering():
 
1354
                # We get the path from the working tree if it exists.
 
1355
                # That fails though when OTHER is adding a file, so
 
1356
                # we fall back to the other tree to find the path if
 
1357
                # it doesn't exist locally.
 
1358
                try:
 
1359
                    filter_tree_path = wt.id2path(file_id)
 
1360
                except errors.NoSuchId:
 
1361
                    filter_tree_path = self.other_tree.id2path(file_id)
 
1362
            else:
 
1363
                # Skip the id2path lookup for older formats
 
1364
                filter_tree_path = None
 
1365
            transform.create_from_tree(self.tt, trans_id,
 
1366
                             self.other_tree, file_id,
 
1367
                             filter_tree_path=filter_tree_path)
 
1368
            return 'done', None
 
1369
        elif file_in_this:
 
1370
            # OTHER deleted the file
 
1371
            return 'delete', None
 
1372
        else:
 
1373
            raise AssertionError(
 
1374
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
 
1375
                % (file_id,))
 
1376
 
 
1377
    def merge_contents(self, merge_hook_params):
 
1378
        """Fallback merge logic after user installed hooks."""
 
1379
        # This function is used in merge hooks as the fallback instance.
 
1380
        # Perhaps making this function and the functions it calls be a 
 
1381
        # a separate class would be better.
 
1382
        if merge_hook_params.winner == 'other':
1145
1383
            # OTHER is a straight winner, so replace this contents with other
1146
 
            file_in_this = file_id in self.this_tree
1147
 
            if file_in_this:
1148
 
                # Remove any existing contents
1149
 
                self.tt.delete_contents(trans_id)
1150
 
            if file_id in self.other_tree:
1151
 
                # OTHER changed the file
1152
 
                wt = self.this_tree
1153
 
                if wt.supports_content_filtering():
1154
 
                    # We get the path from the working tree if it exists.
1155
 
                    # That fails though when OTHER is adding a file, so
1156
 
                    # we fall back to the other tree to find the path if
1157
 
                    # it doesn't exist locally.
1158
 
                    try:
1159
 
                        filter_tree_path = wt.id2path(file_id)
1160
 
                    except errors.NoSuchId:
1161
 
                        filter_tree_path = self.other_tree.id2path(file_id)
1162
 
                else:
1163
 
                    # Skip the id2path lookup for older formats
1164
 
                    filter_tree_path = None
1165
 
                transform.create_from_tree(self.tt, trans_id,
1166
 
                                 self.other_tree, file_id,
1167
 
                                 filter_tree_path=filter_tree_path)
1168
 
                if not file_in_this:
1169
 
                    self.tt.version_file(file_id, trans_id)
1170
 
                return "modified"
1171
 
            elif file_in_this:
1172
 
                # OTHER deleted the file
1173
 
                self.tt.unversion_file(trans_id)
1174
 
                return "deleted"
 
1384
            return self._default_other_winner_merge(merge_hook_params)
 
1385
        elif merge_hook_params.is_file_merge():
 
1386
            # THIS and OTHER are both files, so text merge.  Either
 
1387
            # BASE is a file, or both converted to files, so at least we
 
1388
            # have agreement that output should be a file.
 
1389
            try:
 
1390
                self.text_merge(merge_hook_params.file_id,
 
1391
                    merge_hook_params.trans_id)
 
1392
            except errors.BinaryFile:
 
1393
                return 'not_applicable', None
 
1394
            return 'done', None
1175
1395
        else:
1176
 
            # We have a hypothetical conflict, but if we have files, then we
1177
 
            # can try to merge the content
1178
 
            if this_pair[0] == 'file' and other_pair[0] == 'file':
1179
 
                # THIS and OTHER are both files, so text merge.  Either
1180
 
                # BASE is a file, or both converted to files, so at least we
1181
 
                # have agreement that output should be a file.
1182
 
                try:
1183
 
                    self.text_merge(file_id, trans_id)
1184
 
                except errors.BinaryFile:
1185
 
                    return contents_conflict()
1186
 
                if file_id not in self.this_tree:
1187
 
                    self.tt.version_file(file_id, trans_id)
1188
 
                try:
1189
 
                    self.tt.tree_kind(trans_id)
1190
 
                    self.tt.delete_contents(trans_id)
1191
 
                except errors.NoSuchFile:
1192
 
                    pass
1193
 
                return "modified"
1194
 
            else:
1195
 
                return contents_conflict()
 
1396
            return 'not_applicable', None
1196
1397
 
1197
1398
    def get_lines(self, tree, file_id):
1198
1399
        """Return the lines in a file, or an empty list."""
1199
 
        if file_id in tree:
 
1400
        if tree.has_id(file_id):
1200
1401
            return tree.get_file(file_id).readlines()
1201
1402
        else:
1202
1403
            return []
1205
1406
        """Perform a three-way text merge on a file_id"""
1206
1407
        # it's possible that we got here with base as a different type.
1207
1408
        # if so, we just want two-way text conflicts.
1208
 
        if file_id in self.base_tree and \
 
1409
        if self.base_tree.has_id(file_id) and \
1209
1410
            self.base_tree.kind(file_id) == "file":
1210
1411
            base_lines = self.get_lines(self.base_tree, file_id)
1211
1412
        else:
1274
1475
        versioned = False
1275
1476
        file_group = []
1276
1477
        for suffix, tree, lines in data:
1277
 
            if file_id in tree:
 
1478
            if tree.has_id(file_id):
1278
1479
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1279
1480
                                               suffix, lines, filter_tree_path)
1280
1481
                file_group.append(trans_id)
1324
1525
        if winner == "this":
1325
1526
            executability = this_executable
1326
1527
        else:
1327
 
            if file_id in self.other_tree:
 
1528
            if self.other_tree.has_id(file_id):
1328
1529
                executability = other_executable
1329
 
            elif file_id in self.this_tree:
 
1530
            elif self.this_tree.has_id(file_id):
1330
1531
                executability = this_executable
1331
 
            elif file_id in self.base_tree:
 
1532
            elif self.base_tree_has_id(file_id):
1332
1533
                executability = base_executable
1333
1534
        if executability is not None:
1334
1535
            trans_id = self.tt.trans_id_file_id(file_id)