~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

  • Committer: Vincent Ladeuil
  • Date: 2010-01-20 16:31:24 UTC
  • mfrom: (4869.3.34 per-file-merge-hook)
  • mto: This revision was merged to the branch mainline in revision 4975.
  • Revision ID: v.ladeuil+lp@free.fr-20100120163124-fhf3nh8e1nu7w3y7
Implement a per-file merge hook

Show diffs side-by-side

added added

removed removed

Lines of Context:
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 when file content needs to be merged (including when one "
 
61
            "side has deleted the file and the other has changed it)."
 
62
            "merge_file_content is called with a "
 
63
            "bzrlib.merge.MergeHookParams. The function should return a tuple "
 
64
            "of (status, lines), where status is one of 'not_applicable', "
 
65
            "'success', 'conflicted', or 'delete'.  If status is success or "
 
66
            "conflicted, then lines should be an iterable of strings of the "
 
67
            "new file contents.",
 
68
            (2, 1), None))
 
69
 
 
70
 
 
71
class MergeHookParams(object):
 
72
    """Object holding parameters passed to merge_file_content hooks.
 
73
 
 
74
    There are 3 fields hooks can access:
 
75
 
 
76
    :ivar merger: the Merger object
 
77
    :ivar file_id: the file ID of the file being merged
 
78
    :ivar trans_id: the transform ID for the merge of this file
 
79
    :ivar this_kind: kind of file_id in 'this' tree
 
80
    :ivar other_kind: kind of file_id in 'other' tree
 
81
    :ivar winner: one of 'this', 'other', 'conflict'
 
82
    """
 
83
 
 
84
    def __init__(self, merger, file_id, trans_id, this_kind, other_kind,
 
85
            winner):
 
86
        self.merger = merger
 
87
        self.file_id = file_id
 
88
        self.trans_id = trans_id
 
89
        self.this_kind = this_kind
 
90
        self.other_kind = other_kind
 
91
        self.winner = winner
 
92
 
 
93
    def is_file_merge(self):
 
94
        """True if this_kind and other_kind are both 'file'."""
 
95
        return self.this_kind == 'file' and self.other_kind == 'file'
 
96
 
 
97
    @decorators.cachedproperty
 
98
    def base_lines(self):
 
99
        """The lines of the 'base' version of the file."""
 
100
        return self.merger.get_lines(self.merger.base_tree, self.file_id)
 
101
 
 
102
    @decorators.cachedproperty
 
103
    def this_lines(self):
 
104
        """The lines of the 'this' version of the file."""
 
105
        return self.merger.get_lines(self.merger.this_tree, self.file_id)
 
106
 
 
107
    @decorators.cachedproperty
 
108
    def other_lines(self):
 
109
        """The lines of the 'other' version of the file."""
 
110
        return self.merger.get_lines(self.merger.other_tree, self.file_id)
 
111
 
 
112
 
53
113
class Merger(object):
 
114
 
 
115
    hooks = MergeHooks()
 
116
 
54
117
    def __init__(self, this_branch, other_tree=None, base_tree=None,
55
118
                 this_tree=None, pb=None, change_reporter=None,
56
119
                 recurse='down', revision_graph=None):
432
495
                  'other_tree': self.other_tree,
433
496
                  'interesting_ids': self.interesting_ids,
434
497
                  'interesting_files': self.interesting_files,
435
 
                  'pp': self.pp,
 
498
                  'pp': self.pp, 'this_branch': self.this_branch,
436
499
                  'do_merge': False}
437
500
        if self.merge_type.requires_base:
438
501
            kwargs['base_tree'] = self.base_tree
542
605
                 interesting_ids=None, reprocess=False, show_base=False,
543
606
                 pb=progress.DummyProgress(), pp=None, change_reporter=None,
544
607
                 interesting_files=None, do_merge=True,
545
 
                 cherrypick=False, lca_trees=None):
 
608
                 cherrypick=False, lca_trees=None, this_branch=None):
546
609
        """Initialize the merger object and perform the merge.
547
610
 
548
611
        :param working_tree: The working tree to apply the merge to
549
612
        :param this_tree: The local tree in the merge operation
550
613
        :param base_tree: The common tree in the merge operation
551
614
        :param other_tree: The other tree to merge changes from
 
615
        :param this_branch: The branch associated with this_tree
552
616
        :param interesting_ids: The file_ids of files that should be
553
617
            participate in the merge.  May not be combined with
554
618
            interesting_files.
577
641
        self.this_tree = working_tree
578
642
        self.base_tree = base_tree
579
643
        self.other_tree = other_tree
 
644
        self.this_branch = this_branch
580
645
        self._raw_conflicts = []
581
646
        self.cooked_conflicts = []
582
647
        self.reprocess = reprocess
892
957
            self.tt.final_kind(other_root)
893
958
        except errors.NoSuchFile:
894
959
            return
895
 
        if self.other_tree.inventory.root.file_id in self.this_tree.inventory:
 
960
        if self.this_tree.has_id(self.other_tree.inventory.root.file_id):
896
961
            # the other tree's root is a non-root in the current tree
897
962
            return
898
963
        self.reparent_children(self.other_tree.inventory.root, self.tt.root)
940
1005
    @staticmethod
941
1006
    def executable(tree, file_id):
942
1007
        """Determine the executability of a file-id (used as a key method)."""
943
 
        if file_id not in tree:
 
1008
        if not tree.has_id(file_id):
944
1009
            return None
945
1010
        if tree.kind(file_id) != "file":
946
1011
            return False
949
1014
    @staticmethod
950
1015
    def kind(tree, file_id):
951
1016
        """Determine the kind of a file-id (used as a key method)."""
952
 
        if file_id not in tree:
 
1017
        if not tree.has_id(file_id):
953
1018
            return None
954
1019
        return tree.kind(file_id)
955
1020
 
1038
1103
 
1039
1104
    def merge_names(self, file_id):
1040
1105
        def get_entry(tree):
1041
 
            if file_id in tree.inventory:
 
1106
            if tree.has_id(file_id):
1042
1107
                return tree.inventory[file_id]
1043
1108
            else:
1044
1109
                return None
1107
1172
                contents = None
1108
1173
            return kind, contents
1109
1174
 
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
1175
        # See SPOT run.  run, SPOT, run.
1123
1176
        # So we're not QUITE repeating ourselves; we do tricky things with
1124
1177
        # file kind...
1140
1193
        if winner == 'this':
1141
1194
            # No interesting changes introduced by OTHER
1142
1195
            return "unmodified"
 
1196
        # We have a hypothetical conflict, but if we have files, then we
 
1197
        # can try to merge the content
1143
1198
        trans_id = self.tt.trans_id_file_id(file_id)
1144
 
        if winner == 'other':
 
1199
        params = MergeHookParams(self, file_id, trans_id, this_pair[0],
 
1200
            other_pair[0], winner)
 
1201
        hooks = Merger.hooks['merge_file_content']
 
1202
        hooks = list(hooks) + [self.default_text_merge]
 
1203
        hook_status = 'not_applicable'
 
1204
        for hook in hooks:
 
1205
            hook_status, lines = hook(params)
 
1206
            if hook_status != 'not_applicable':
 
1207
                # Don't try any more hooks, this one applies.
 
1208
                break
 
1209
        result = "modified"
 
1210
        if hook_status == 'not_applicable':
 
1211
            # This is a contents conflict, because none of the available
 
1212
            # functions could merge it.
 
1213
            result = None
 
1214
            name = self.tt.final_name(trans_id)
 
1215
            parent_id = self.tt.final_parent(trans_id)
 
1216
            if self.this_tree.has_id(file_id):
 
1217
                self.tt.unversion_file(trans_id)
 
1218
            file_group = self._dump_conflicts(name, parent_id, file_id,
 
1219
                                              set_version=True)
 
1220
            self._raw_conflicts.append(('contents conflict', file_group))
 
1221
        elif hook_status == 'success':
 
1222
            self.tt.create_file(lines, trans_id)
 
1223
        elif hook_status == 'conflicted':
 
1224
            # XXX: perhaps the hook should be able to provide
 
1225
            # the BASE/THIS/OTHER files?
 
1226
            self.tt.create_file(lines, trans_id)
 
1227
            self._raw_conflicts.append(('text conflict', trans_id))
 
1228
            name = self.tt.final_name(trans_id)
 
1229
            parent_id = self.tt.final_parent(trans_id)
 
1230
            self._dump_conflicts(name, parent_id, file_id)
 
1231
        elif hook_status == 'delete':
 
1232
            self.tt.unversion_file(trans_id)
 
1233
            result = "deleted"
 
1234
        elif hook_status == 'done':
 
1235
            # The hook function did whatever it needs to do directly, no
 
1236
            # further action needed here.
 
1237
            pass
 
1238
        else:
 
1239
            raise AssertionError('unknown hook_status: %r' % (hook_status,))
 
1240
        if not self.this_tree.has_id(file_id) and result == "modified":
 
1241
            self.tt.version_file(file_id, trans_id)
 
1242
        # The merge has been performed, so the old contents should not be
 
1243
        # retained.
 
1244
        try:
 
1245
            self.tt.delete_contents(trans_id)
 
1246
        except errors.NoSuchFile:
 
1247
            pass
 
1248
        return result
 
1249
 
 
1250
    def _default_other_winner_merge(self, merge_hook_params):
 
1251
        """Replace this contents with other."""
 
1252
        file_id = merge_hook_params.file_id
 
1253
        trans_id = merge_hook_params.trans_id
 
1254
        file_in_this = self.this_tree.has_id(file_id)
 
1255
        if self.other_tree.has_id(file_id):
 
1256
            # OTHER changed the file
 
1257
            wt = self.this_tree
 
1258
            if wt.supports_content_filtering():
 
1259
                # We get the path from the working tree if it exists.
 
1260
                # That fails though when OTHER is adding a file, so
 
1261
                # we fall back to the other tree to find the path if
 
1262
                # it doesn't exist locally.
 
1263
                try:
 
1264
                    filter_tree_path = wt.id2path(file_id)
 
1265
                except errors.NoSuchId:
 
1266
                    filter_tree_path = self.other_tree.id2path(file_id)
 
1267
            else:
 
1268
                # Skip the id2path lookup for older formats
 
1269
                filter_tree_path = None
 
1270
            transform.create_from_tree(self.tt, trans_id,
 
1271
                             self.other_tree, file_id,
 
1272
                             filter_tree_path=filter_tree_path)
 
1273
            return 'done', None
 
1274
        elif file_in_this:
 
1275
            # OTHER deleted the file
 
1276
            return 'delete', None
 
1277
        else:
 
1278
            raise AssertionError(
 
1279
                'winner is OTHER, but file_id %r not in THIS or OTHER tree'
 
1280
                % (file_id,))
 
1281
 
 
1282
    def default_text_merge(self, merge_hook_params):
 
1283
        if merge_hook_params.winner == 'other':
1145
1284
            # 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"
 
1285
            return self._default_other_winner_merge(merge_hook_params)
 
1286
        elif merge_hook_params.is_file_merge():
 
1287
            # THIS and OTHER are both files, so text merge.  Either
 
1288
            # BASE is a file, or both converted to files, so at least we
 
1289
            # have agreement that output should be a file.
 
1290
            try:
 
1291
                self.text_merge(merge_hook_params.file_id,
 
1292
                    merge_hook_params.trans_id)
 
1293
            except errors.BinaryFile:
 
1294
                return 'not_applicable', None
 
1295
            return 'done', None
1175
1296
        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()
 
1297
            return 'not_applicable', None
1196
1298
 
1197
1299
    def get_lines(self, tree, file_id):
1198
1300
        """Return the lines in a file, or an empty list."""
1199
 
        if file_id in tree:
 
1301
        if tree.has_id(file_id):
1200
1302
            return tree.get_file(file_id).readlines()
1201
1303
        else:
1202
1304
            return []
1205
1307
        """Perform a three-way text merge on a file_id"""
1206
1308
        # it's possible that we got here with base as a different type.
1207
1309
        # if so, we just want two-way text conflicts.
1208
 
        if file_id in self.base_tree and \
 
1310
        if self.base_tree.has_id(file_id) and \
1209
1311
            self.base_tree.kind(file_id) == "file":
1210
1312
            base_lines = self.get_lines(self.base_tree, file_id)
1211
1313
        else:
1274
1376
        versioned = False
1275
1377
        file_group = []
1276
1378
        for suffix, tree, lines in data:
1277
 
            if file_id in tree:
 
1379
            if tree.has_id(file_id):
1278
1380
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1279
1381
                                               suffix, lines, filter_tree_path)
1280
1382
                file_group.append(trans_id)
1324
1426
        if winner == "this":
1325
1427
            executability = this_executable
1326
1428
        else:
1327
 
            if file_id in self.other_tree:
 
1429
            if self.other_tree.has_id(file_id):
1328
1430
                executability = other_executable
1329
 
            elif file_id in self.this_tree:
 
1431
            elif self.this_tree.has_id(file_id):
1330
1432
                executability = this_executable
1331
 
            elif file_id in self.base_tree:
 
1433
            elif self.base_tree_has_id(file_id):
1332
1434
                executability = base_executable
1333
1435
        if executability is not None:
1334
1436
            trans_id = self.tt.trans_id_file_id(file_id)