~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-01-12 08:03:28 UTC
  • mfrom: (4949.1.1 integration)
  • Revision ID: pqm@pqm.ubuntu.com-20100112080328-cb0tvu90uglxlrw0
(mbp) merge 2.0 back to trunk

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,
52
50
        from_tree.unlock()
53
51
 
54
52
 
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
53
class Merger(object):
207
 
 
208
 
    hooks = MergeHooks()
209
 
 
210
54
    def __init__(self, this_branch, other_tree=None, base_tree=None,
211
55
                 this_tree=None, pb=None, change_reporter=None,
212
56
                 recurse='down', revision_graph=None):
588
432
                  'other_tree': self.other_tree,
589
433
                  'interesting_ids': self.interesting_ids,
590
434
                  'interesting_files': self.interesting_files,
591
 
                  'pp': self.pp, 'this_branch': self.this_branch,
 
435
                  'pp': self.pp,
592
436
                  'do_merge': False}
593
437
        if self.merge_type.requires_base:
594
438
            kwargs['base_tree'] = self.base_tree
698
542
                 interesting_ids=None, reprocess=False, show_base=False,
699
543
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
700
544
                 interesting_files=None, do_merge=True,
701
 
                 cherrypick=False, lca_trees=None, this_branch=None):
 
545
                 cherrypick=False, lca_trees=None):
702
546
        """Initialize the merger object and perform the merge.
703
547
 
704
548
        :param working_tree: The working tree to apply the merge to
705
549
        :param this_tree: The local tree in the merge operation
706
550
        :param base_tree: The common tree in the merge operation
707
551
        :param other_tree: The other tree to merge changes from
708
 
        :param this_branch: The branch associated with this_tree
709
552
        :param interesting_ids: The file_ids of files that should be
710
553
            participate in the merge.  May not be combined with
711
554
            interesting_files.
734
577
        self.this_tree = working_tree
735
578
        self.base_tree = base_tree
736
579
        self.other_tree = other_tree
737
 
        self.this_branch = this_branch
738
580
        self._raw_conflicts = []
739
581
        self.cooked_conflicts = []
740
582
        self.reprocess = reprocess
801
643
            resolver = self._lca_multi_way
802
644
        child_pb = ui.ui_factory.nested_progress_bar()
803
645
        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
646
            for num, (file_id, changed, parents3, names3,
808
647
                      executable3) in enumerate(entries):
809
648
                child_pb.update('Preparing file merge', num, len(entries))
810
649
                self._merge_names(file_id, parents3, names3, resolver=resolver)
811
650
                if changed:
812
 
                    file_status = self._do_merge_contents(file_id)
 
651
                    file_status = self.merge_contents(file_id)
813
652
                else:
814
653
                    file_status = 'unmodified'
815
654
                self._merge_executable(file_id,
1053
892
            self.tt.final_kind(other_root)
1054
893
        except errors.NoSuchFile:
1055
894
            return
1056
 
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
 
895
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
1057
896
            # the other tree's root is a non-root in the current tree
1058
897
            return
1059
898
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
1101
940
    @staticmethod
1102
941
    def executable(tree, file_id):
1103
942
        """Determine the executability of a file-id (used as a key method)."""
1104
 
        if not tree.has_id(file_id):
 
943
        if file_id not in tree:
1105
944
            return None
1106
945
        if tree.kind(file_id) != "file":
1107
946
            return False
1110
949
    @staticmethod
1111
950
    def kind(tree, file_id):
1112
951
        """Determine the kind of a file-id (used as a key method)."""
1113
 
        if not tree.has_id(file_id):
 
952
        if file_id not in tree:
1114
953
            return None
1115
954
        return tree.kind(file_id)
1116
955
 
1199
1038
 
1200
1039
    def merge_names(self, file_id):
1201
1040
        def get_entry(tree):
1202
 
            if tree.has_id(file_id):
 
1041
            if file_id in tree.inventory:
1203
1042
                return tree.inventory[file_id]
1204
1043
            else:
1205
1044
                return None
1254
1093
            self.tt.adjust_path(names[self.winner_idx[name_winner]],
1255
1094
                                parent_trans_id, trans_id)
1256
1095
 
1257
 
    def _do_merge_contents(self, file_id):
 
1096
    def merge_contents(self, file_id):
1258
1097
        """Performs a merge on file_id contents."""
1259
1098
        def contents_pair(tree):
1260
1099
            if file_id not in tree:
1268
1107
                contents = None
1269
1108
            return kind, contents
1270
1109
 
 
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
 
1271
1122
        # See SPOT run.  run, SPOT, run.
1272
1123
        # So we're not QUITE repeating ourselves; we do tricky things with
1273
1124
        # file kind...
1289
1140
        if winner == 'this':
1290
1141
            # No interesting changes introduced by OTHER
1291
1142
            return "unmodified"
1292
 
        # We have a hypothetical conflict, but if we have files, then we
1293
 
        # can try to merge the content
1294
1143
        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':
 
1144
        if winner == 'other':
1383
1145
            # 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
 
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"
1395
1175
        else:
1396
 
            return 'not_applicable', None
 
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()
1397
1196
 
1398
1197
    def get_lines(self, tree, file_id):
1399
1198
        """Return the lines in a file, or an empty list."""
1400
 
        if tree.has_id(file_id):
 
1199
        if file_id in tree:
1401
1200
            return tree.get_file(file_id).readlines()
1402
1201
        else:
1403
1202
            return []
1406
1205
        """Perform a three-way text merge on a file_id"""
1407
1206
        # it's possible that we got here with base as a different type.
1408
1207
        # if so, we just want two-way text conflicts.
1409
 
        if self.base_tree.has_id(file_id) and \
 
1208
        if file_id in self.base_tree and \
1410
1209
            self.base_tree.kind(file_id) == "file":
1411
1210
            base_lines = self.get_lines(self.base_tree, file_id)
1412
1211
        else:
1475
1274
        versioned = False
1476
1275
        file_group = []
1477
1276
        for suffix, tree, lines in data:
1478
 
            if tree.has_id(file_id):
 
1277
            if file_id in tree:
1479
1278
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1480
1279
                                               suffix, lines, filter_tree_path)
1481
1280
                file_group.append(trans_id)
1525
1324
        if winner == "this":
1526
1325
            executability = this_executable
1527
1326
        else:
1528
 
            if self.other_tree.has_id(file_id):
 
1327
            if file_id in self.other_tree:
1529
1328
                executability = other_executable
1530
 
            elif self.this_tree.has_id(file_id):
 
1329
            elif file_id in self.this_tree:
1531
1330
                executability = this_executable
1532
 
            elif self.base_tree_has_id(file_id):
 
1331
            elif file_id in self.base_tree:
1533
1332
                executability = base_executable
1534
1333
        if executability is not None:
1535
1334
            trans_id = self.tt.trans_id_file_id(file_id)