~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

Add a NEWS entry and prepare submission.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2008 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,
23
22
    errors,
24
23
    graph as _mod_graph,
25
 
    hooks,
26
24
    merge3,
27
25
    osutils,
28
26
    patiencediff,
29
27
    progress,
 
28
    registry,
30
29
    revision as _mod_revision,
31
30
    textfile,
32
31
    trace,
52
51
        from_tree.unlock()
53
52
 
54
53
 
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
 
 
206
54
class Merger(object):
207
 
 
208
 
    hooks = MergeHooks()
209
 
 
210
55
    def __init__(self, this_branch, other_tree=None, base_tree=None,
211
56
                 this_tree=None, pb=None, change_reporter=None,
212
57
                 recurse='down', revision_graph=None):
395
240
        if self.other_rev_id is None:
396
241
            other_basis_tree = self.revision_tree(self.other_basis)
397
242
            if other_basis_tree.has_changes(self.other_tree):
398
 
                raise errors.WorkingTreeNotRevision(self.this_tree)
 
243
                raise WorkingTreeNotRevision(self.this_tree)
399
244
            other_rev_id = self.other_basis
400
245
            self.other_tree = other_basis_tree
401
246
 
588
433
                  'other_tree': self.other_tree,
589
434
                  'interesting_ids': self.interesting_ids,
590
435
                  'interesting_files': self.interesting_files,
591
 
                  'pp': self.pp, 'this_branch': self.this_branch,
 
436
                  'pp': self.pp,
592
437
                  'do_merge': False}
593
438
        if self.merge_type.requires_base:
594
439
            kwargs['base_tree'] = self.base_tree
698
543
                 interesting_ids=None, reprocess=False, show_base=False,
699
544
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
700
545
                 interesting_files=None, do_merge=True,
701
 
                 cherrypick=False, lca_trees=None, this_branch=None):
 
546
                 cherrypick=False, lca_trees=None):
702
547
        """Initialize the merger object and perform the merge.
703
548
 
704
549
        :param working_tree: The working tree to apply the merge to
705
550
        :param this_tree: The local tree in the merge operation
706
551
        :param base_tree: The common tree in the merge operation
707
552
        :param other_tree: The other tree to merge changes from
708
 
        :param this_branch: The branch associated with this_tree
709
553
        :param interesting_ids: The file_ids of files that should be
710
554
            participate in the merge.  May not be combined with
711
555
            interesting_files.
734
578
        self.this_tree = working_tree
735
579
        self.base_tree = base_tree
736
580
        self.other_tree = other_tree
737
 
        self.this_branch = this_branch
738
581
        self._raw_conflicts = []
739
582
        self.cooked_conflicts = []
740
583
        self.reprocess = reprocess
801
644
            resolver = self._lca_multi_way
802
645
        child_pb = ui.ui_factory.nested_progress_bar()
803
646
        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]
807
647
            for num, (file_id, changed, parents3, names3,
808
648
                      executable3) in enumerate(entries):
809
649
                child_pb.update('Preparing file merge', num, len(entries))
810
650
                self._merge_names(file_id, parents3, names3, resolver=resolver)
811
651
                if changed:
812
 
                    file_status = self._do_merge_contents(file_id)
 
652
                    file_status = self.merge_contents(file_id)
813
653
                else:
814
654
                    file_status = 'unmodified'
815
655
                self._merge_executable(file_id,
1053
893
            self.tt.final_kind(other_root)
1054
894
        except errors.NoSuchFile:
1055
895
            return
1056
 
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
 
896
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
1057
897
            # the other tree's root is a non-root in the current tree
1058
898
            return
1059
899
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
1101
941
    @staticmethod
1102
942
    def executable(tree, file_id):
1103
943
        """Determine the executability of a file-id (used as a key method)."""
1104
 
        if not tree.has_id(file_id):
 
944
        if file_id not in tree:
1105
945
            return None
1106
946
        if tree.kind(file_id) != "file":
1107
947
            return False
1110
950
    @staticmethod
1111
951
    def kind(tree, file_id):
1112
952
        """Determine the kind of a file-id (used as a key method)."""
1113
 
        if not tree.has_id(file_id):
 
953
        if file_id not in tree:
1114
954
            return None
1115
955
        return tree.kind(file_id)
1116
956
 
1199
1039
 
1200
1040
    def merge_names(self, file_id):
1201
1041
        def get_entry(tree):
1202
 
            if tree.has_id(file_id):
 
1042
            if file_id in tree.inventory:
1203
1043
                return tree.inventory[file_id]
1204
1044
            else:
1205
1045
                return None
1254
1094
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1255
1095
                                parent_trans_id, trans_id)
1256
1096
 
1257
 
    def _do_merge_contents(self, file_id):
 
1097
    def merge_contents(self, file_id):
1258
1098
        """Performs a merge on file_id contents."""
1259
1099
        def contents_pair(tree):
1260
1100
            if file_id not in tree:
1268
1108
                contents = None
1269
1109
            return kind, contents
1270
1110
 
 
1111
        def contents_conflict():
 
1112
            trans_id = self.tt.trans_id_file_id(file_id)
 
1113
            name = self.tt.final_name(trans_id)
 
1114
            parent_id = self.tt.final_parent(trans_id)
 
1115
            if file_id in self.this_tree.inventory:
 
1116
                self.tt.unversion_file(trans_id)
 
1117
                if file_id in self.this_tree:
 
1118
                    self.tt.delete_contents(trans_id)
 
1119
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1120
                                              set_version=True)
 
1121
            self._raw_conflicts.append(('contents conflict', file_group))
 
1122
 
1271
1123
        # See SPOT run.  run, SPOT, run.
1272
1124
        # So we're not QUITE repeating ourselves; we do tricky things with
1273
1125
        # file kind...
1289
1141
        if winner == 'this':
1290
1142
            # No interesting changes introduced by OTHER
1291
1143
            return "unmodified"
1292
 
        # We have a hypothetical conflict, but if we have files, then we
1293
 
        # can try to merge the content
1294
1144
        trans_id = self.tt.trans_id_file_id(file_id)
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
        if winner == 'other':
1383
1146
            # OTHER is a straight winner, so replace this contents with other
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
 
1147
            file_in_this = file_id in self.this_tree
 
1148
            if file_in_this:
 
1149
                # Remove any existing contents
 
1150
                self.tt.delete_contents(trans_id)
 
1151
            if file_id in self.other_tree:
 
1152
                # OTHER changed the file
 
1153
                wt = self.this_tree
 
1154
                if wt.supports_content_filtering():
 
1155
                    # We get the path from the working tree if it exists.
 
1156
                    # That fails though when OTHER is adding a file, so
 
1157
                    # we fall back to the other tree to find the path if
 
1158
                    # it doesn't exist locally.
 
1159
                    try:
 
1160
                        filter_tree_path = wt.id2path(file_id)
 
1161
                    except errors.NoSuchId:
 
1162
                        filter_tree_path = self.other_tree.id2path(file_id)
 
1163
                else:
 
1164
                    # Skip the id2path lookup for older formats
 
1165
                    filter_tree_path = None
 
1166
                transform.create_from_tree(self.tt, trans_id,
 
1167
                                 self.other_tree, file_id,
 
1168
                                 filter_tree_path=filter_tree_path)
 
1169
                if not file_in_this:
 
1170
                    self.tt.version_file(file_id, trans_id)
 
1171
                return "modified"
 
1172
            elif file_in_this:
 
1173
                # OTHER deleted the file
 
1174
                self.tt.unversion_file(trans_id)
 
1175
                return "deleted"
1395
1176
        else:
1396
 
            return 'not_applicable', None
 
1177
            # We have a hypothetical conflict, but if we have files, then we
 
1178
            # can try to merge the content
 
1179
            if this_pair[0] == 'file' and other_pair[0] == 'file':
 
1180
                # THIS and OTHER are both files, so text merge.  Either
 
1181
                # BASE is a file, or both converted to files, so at least we
 
1182
                # have agreement that output should be a file.
 
1183
                try:
 
1184
                    self.text_merge(file_id, trans_id)
 
1185
                except errors.BinaryFile:
 
1186
                    return contents_conflict()
 
1187
                if file_id not in self.this_tree:
 
1188
                    self.tt.version_file(file_id, trans_id)
 
1189
                try:
 
1190
                    self.tt.tree_kind(trans_id)
 
1191
                    self.tt.delete_contents(trans_id)
 
1192
                except errors.NoSuchFile:
 
1193
                    pass
 
1194
                return "modified"
 
1195
            else:
 
1196
                return contents_conflict()
1397
1197
 
1398
1198
    def get_lines(self, tree, file_id):
1399
1199
        """Return the lines in a file, or an empty list."""
1400
 
        if tree.has_id(file_id):
 
1200
        if file_id in tree:
1401
1201
            return tree.get_file(file_id).readlines()
1402
1202
        else:
1403
1203
            return []
1406
1206
        """Perform a three-way text merge on a file_id"""
1407
1207
        # it's possible that we got here with base as a different type.
1408
1208
        # if so, we just want two-way text conflicts.
1409
 
        if self.base_tree.has_id(file_id) and \
 
1209
        if file_id in self.base_tree and \
1410
1210
            self.base_tree.kind(file_id) == "file":
1411
1211
            base_lines = self.get_lines(self.base_tree, file_id)
1412
1212
        else:
1475
1275
        versioned = False
1476
1276
        file_group = []
1477
1277
        for suffix, tree, lines in data:
1478
 
            if tree.has_id(file_id):
 
1278
            if file_id in tree:
1479
1279
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1480
1280
                                               suffix, lines, filter_tree_path)
1481
1281
                file_group.append(trans_id)
1525
1325
        if winner == "this":
1526
1326
            executability = this_executable
1527
1327
        else:
1528
 
            if self.other_tree.has_id(file_id):
 
1328
            if file_id in self.other_tree:
1529
1329
                executability = other_executable
1530
 
            elif self.this_tree.has_id(file_id):
 
1330
            elif file_id in self.this_tree:
1531
1331
                executability = this_executable
1532
 
            elif self.base_tree_has_id(file_id):
 
1332
            elif file_id in self.base_tree:
1533
1333
                executability = base_executable
1534
1334
        if executability is not None:
1535
1335
            trans_id = self.tt.trans_id_file_id(file_id)