~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transform.py

Moved the merge stuff into merge.py

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
import os
18
18
import errno
19
 
from tempfile import mkdtemp
20
 
from shutil import rmtree
21
19
from stat import S_ISREG
22
20
 
23
21
from bzrlib import BZRDIR
24
22
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
25
 
                           ReusingTransform, NotVersionedError, CantMoveRoot,
26
 
                           WorkingTreeNotRevision)
 
23
                           ReusingTransform, NotVersionedError, CantMoveRoot)
27
24
from bzrlib.inventory import InventoryEntry
28
25
from bzrlib.osutils import file_kind, supports_executable, pathjoin
29
 
from bzrlib.merge3 import Merge3
30
26
from bzrlib.trace import mutter
31
27
 
32
28
ROOT_PARENT = "root-parent"
986
982
                tt.create_directory(trans_id)
987
983
        elif c_type == 'unversioned parent':
988
984
            tt.version_file(tt.get_tree_file_id(conflict[1]), conflict[1])
989
 
 
990
 
 
991
 
class Merge3Merger(object):
992
 
    requires_base = True
993
 
    supports_reprocess = True
994
 
    supports_show_base = True
995
 
    history_based = False
996
 
    def __init__(self, working_tree, this_tree, base_tree, other_tree, 
997
 
                 reprocess=False, show_base=False):
998
 
        object.__init__(self)
999
 
        self.this_tree = working_tree
1000
 
        self.base_tree = base_tree
1001
 
        self.other_tree = other_tree
1002
 
        self._raw_conflicts = []
1003
 
        self.cooked_conflicts = []
1004
 
        self.reprocess = reprocess
1005
 
        self.show_base = show_base
1006
 
 
1007
 
        all_ids = set(base_tree)
1008
 
        all_ids.update(other_tree)
1009
 
        self.tt = TreeTransform(working_tree)
1010
 
        try:
1011
 
            for file_id in all_ids:
1012
 
                self.merge_names(file_id)
1013
 
                file_status = self.merge_contents(file_id)
1014
 
                self.merge_executable(file_id, file_status)
1015
 
                
1016
 
            resolve_conflicts(self.tt)
1017
 
            self.cook_conflicts()
1018
 
            self.tt.apply()
1019
 
        finally:
1020
 
            try:
1021
 
                self.tt.finalize()
1022
 
            except:
1023
 
                pass
1024
 
       
1025
 
    @staticmethod
1026
 
    def parent(entry, file_id):
1027
 
        if entry is None:
1028
 
            return None
1029
 
        return entry.parent_id
1030
 
 
1031
 
    @staticmethod
1032
 
    def name(entry, file_id):
1033
 
        if entry is None:
1034
 
            return None
1035
 
        return entry.name
1036
 
    
1037
 
    @staticmethod
1038
 
    def contents_sha1(tree, file_id):
1039
 
        if file_id not in tree:
1040
 
            return None
1041
 
        return tree.get_file_sha1(file_id)
1042
 
 
1043
 
    @staticmethod
1044
 
    def executable(tree, file_id):
1045
 
        if file_id not in tree:
1046
 
            return None
1047
 
        if tree.kind(file_id) != "file":
1048
 
            return False
1049
 
        return tree.is_executable(file_id)
1050
 
 
1051
 
    @staticmethod
1052
 
    def kind(tree, file_id):
1053
 
        if file_id not in tree:
1054
 
            return None
1055
 
        return tree.kind(file_id)
1056
 
 
1057
 
    @staticmethod
1058
 
    def scalar_three_way(this_tree, base_tree, other_tree, file_id, key):
1059
 
        """Do a three-way test on a scalar.
1060
 
        Return "this", "other" or "conflict", depending whether a value wins.
1061
 
        """
1062
 
        key_base = key(base_tree, file_id)
1063
 
        key_other = key(other_tree, file_id)
1064
 
        #if base == other, either they all agree, or only THIS has changed.
1065
 
        if key_base == key_other:
1066
 
            return "this"
1067
 
        key_this = key(this_tree, file_id)
1068
 
        if key_this not in (key_base, key_other):
1069
 
            return "conflict"
1070
 
        # "Ambiguous clean merge"
1071
 
        elif key_this == key_other:
1072
 
            return "this"
1073
 
        else:
1074
 
            assert key_this == key_base
1075
 
            return "other"
1076
 
 
1077
 
    def merge_names(self, file_id):
1078
 
        def get_entry(tree):
1079
 
            if file_id in tree.inventory:
1080
 
                return tree.inventory[file_id]
1081
 
            else:
1082
 
                return None
1083
 
        this_entry = get_entry(self.this_tree)
1084
 
        other_entry = get_entry(self.other_tree)
1085
 
        base_entry = get_entry(self.base_tree)
1086
 
        name_winner = self.scalar_three_way(this_entry, base_entry, 
1087
 
                                            other_entry, file_id, self.name)
1088
 
        parent_id_winner = self.scalar_three_way(this_entry, base_entry, 
1089
 
                                                 other_entry, file_id, 
1090
 
                                                 self.parent)
1091
 
        if this_entry is None:
1092
 
            if name_winner == "this":
1093
 
                name_winner = "other"
1094
 
            if parent_id_winner == "this":
1095
 
                parent_id_winner = "other"
1096
 
        if name_winner == "this" and parent_id_winner == "this":
1097
 
            return
1098
 
        if name_winner == "conflict":
1099
 
            trans_id = self.tt.get_trans_id(file_id)
1100
 
            self._raw_conflicts.append(('name conflict', trans_id, 
1101
 
                                        self.name(this_entry, file_id), 
1102
 
                                        self.name(other_entry, file_id)))
1103
 
        if parent_id_winner == "conflict":
1104
 
            trans_id = self.tt.get_trans_id(file_id)
1105
 
            self._raw_conflicts.append(('parent conflict', trans_id, 
1106
 
                                        self.parent(this_entry, file_id), 
1107
 
                                        self.parent(other_entry, file_id)))
1108
 
        if other_entry is None:
1109
 
            # it doesn't matter whether the result was 'other' or 
1110
 
            # 'conflict'-- if there's no 'other', we leave it alone.
1111
 
            return
1112
 
        # if we get here, name_winner and parent_winner are set to safe values.
1113
 
        winner_entry = {"this": this_entry, "other": other_entry, 
1114
 
                        "conflict": other_entry}
1115
 
        trans_id = self.tt.get_trans_id(file_id)
1116
 
        parent_id = winner_entry[parent_id_winner].parent_id
1117
 
        parent_trans_id = self.tt.get_trans_id(parent_id)
1118
 
        self.tt.adjust_path(winner_entry[name_winner].name, parent_trans_id,
1119
 
                            trans_id)
1120
 
 
1121
 
 
1122
 
    def merge_contents(self, file_id):
1123
 
        def contents_pair(tree):
1124
 
            if file_id not in tree:
1125
 
                return (None, None)
1126
 
            kind = tree.kind(file_id)
1127
 
            if kind == "file":
1128
 
                contents = tree.get_file_sha1(file_id)
1129
 
            elif kind == "symlink":
1130
 
                contents = tree.get_symlink_target(file_id)
1131
 
            else:
1132
 
                contents = None
1133
 
            return kind, contents
1134
 
        # See SPOT run.  run, SPOT, run.
1135
 
        # So we're not QUITE repeating ourselves; we do tricky things with
1136
 
        # file kind...
1137
 
        base_pair = contents_pair(self.base_tree)
1138
 
        other_pair = contents_pair(self.other_tree)
1139
 
        if base_pair == other_pair:
1140
 
            return "unmodified"
1141
 
        this_pair = contents_pair(self.this_tree)
1142
 
        if this_pair == other_pair:
1143
 
            return "unmodified"
1144
 
        else:
1145
 
            trans_id = self.tt.get_trans_id(file_id)
1146
 
            if this_pair == base_pair:
1147
 
                if file_id in self.this_tree:
1148
 
                    self.tt.delete_contents(trans_id)
1149
 
                if file_id in self.other_tree.inventory:
1150
 
                    create_by_entry(self.tt, 
1151
 
                                    self.other_tree.inventory[file_id], 
1152
 
                                    self.other_tree, trans_id)
1153
 
                    return "modified"
1154
 
                if file_id in self.this_tree:
1155
 
                    self.tt.unversion_file(trans_id)
1156
 
                    return "deleted"
1157
 
            elif this_pair[0] == "file" and other_pair[0] == "file":
1158
 
                # If this and other are both files, either base is a file, or
1159
 
                # both converted to files, so at least we have agreement that
1160
 
                # output should be a file.
1161
 
                self.text_merge(file_id, trans_id)
1162
 
                return "modified"
1163
 
            else:
1164
 
                trans_id = self.tt.get_trans_id(file_id)
1165
 
                name = self.tt.final_name(trans_id)
1166
 
                parent_id = self.tt.final_parent(trans_id)
1167
 
                if file_id in self.this_tree.inventory:
1168
 
                    self.tt.unversion_file(trans_id)
1169
 
                    self.tt.delete_contents(trans_id)
1170
 
                else:
1171
 
                    self.tt.cancel_versioning(trans_id)
1172
 
                file_group = self._dump_conflicts(name, parent_id, file_id, 
1173
 
                                                  set_version=True)
1174
 
                self._raw_conflicts.append(('contents conflict', file_group))
1175
 
 
1176
 
    def get_lines(self, tree, file_id):
1177
 
        if file_id in tree:
1178
 
            return tree.get_file(file_id).readlines()
1179
 
        else:
1180
 
            return []
1181
 
 
1182
 
    def text_merge(self, file_id, trans_id):
1183
 
        """Perform a three-way text merge on a file_id"""
1184
 
        # it's possible that we got here with base as a different type.
1185
 
        # if so, we just want two-way text conflicts.
1186
 
        if file_id in self.base_tree and \
1187
 
            self.base_tree.kind(file_id) == "file":
1188
 
            base_lines = self.get_lines(self.base_tree, file_id)
1189
 
        else:
1190
 
            base_lines = []
1191
 
        other_lines = self.get_lines(self.other_tree, file_id)
1192
 
        this_lines = self.get_lines(self.this_tree, file_id)
1193
 
        m3 = Merge3(base_lines, this_lines, other_lines)
1194
 
        start_marker = "!START OF MERGE CONFLICT!" + "I HOPE THIS IS UNIQUE"
1195
 
        if self.show_base is True:
1196
 
            base_marker = '|' * 7
1197
 
        else:
1198
 
            base_marker = None
1199
 
 
1200
 
        def iter_merge3(retval):
1201
 
            retval["text_conflicts"] = False
1202
 
            for line in m3.merge_lines(name_a = "TREE", 
1203
 
                                       name_b = "MERGE-SOURCE", 
1204
 
                                       name_base = "BASE-REVISION",
1205
 
                                       start_marker=start_marker, 
1206
 
                                       base_marker=base_marker,
1207
 
                                       reprocess=self.reprocess):
1208
 
                if line.startswith(start_marker):
1209
 
                    retval["text_conflicts"] = True
1210
 
                    yield line.replace(start_marker, '<' * 7)
1211
 
                else:
1212
 
                    yield line
1213
 
        retval = {}
1214
 
        merge3_iterator = iter_merge3(retval)
1215
 
        self.tt.create_file(merge3_iterator, trans_id)
1216
 
        if retval["text_conflicts"] is True:
1217
 
            self._raw_conflicts.append(('text conflict', trans_id))
1218
 
            name = self.tt.final_name(trans_id)
1219
 
            parent_id = self.tt.final_parent(trans_id)
1220
 
            file_group = self._dump_conflicts(name, parent_id, file_id, 
1221
 
                                              this_lines, base_lines,
1222
 
                                              other_lines)
1223
 
            file_group.append(trans_id)
1224
 
 
1225
 
    def _dump_conflicts(self, name, parent_id, file_id, this_lines=None, 
1226
 
                        base_lines=None, other_lines=None, set_version=False,
1227
 
                        no_base=False):
1228
 
        data = [('OTHER', self.other_tree, other_lines), 
1229
 
                ('THIS', self.this_tree, this_lines)]
1230
 
        if not no_base:
1231
 
            data.append(('BASE', self.base_tree, base_lines))
1232
 
        versioned = False
1233
 
        file_group = []
1234
 
        for suffix, tree, lines in data:
1235
 
            if file_id in tree:
1236
 
                trans_id = self._conflict_file(name, parent_id, tree, file_id,
1237
 
                                               suffix, lines)
1238
 
                file_group.append(trans_id)
1239
 
                if set_version and not versioned:
1240
 
                    self.tt.version_file(file_id, trans_id)
1241
 
                    versioned = True
1242
 
        return file_group
1243
 
           
1244
 
    def _conflict_file(self, name, parent_id, tree, file_id, suffix, 
1245
 
                       lines=None):
1246
 
        name = name + '.' + suffix
1247
 
        trans_id = self.tt.create_path(name, parent_id)
1248
 
        entry = tree.inventory[file_id]
1249
 
        create_by_entry(self.tt, entry, tree, trans_id, lines)
1250
 
        return trans_id
1251
 
 
1252
 
    def merge_executable(self, file_id, file_status):
1253
 
        if file_status == "deleted":
1254
 
            return
1255
 
        trans_id = self.tt.get_trans_id(file_id)
1256
 
        try:
1257
 
            if self.tt.final_kind(trans_id) != "file":
1258
 
                return
1259
 
        except NoSuchFile:
1260
 
            return
1261
 
        winner = self.scalar_three_way(self.this_tree, self.base_tree, 
1262
 
                                       self.other_tree, file_id, 
1263
 
                                       self.executable)
1264
 
        if winner == "conflict":
1265
 
        # There must be a None in here, if we have a conflict, but we
1266
 
        # need executability since file status was not deleted.
1267
 
            if self.other_tree.is_executable(file_id) is None:
1268
 
                winner == "this"
1269
 
            else:
1270
 
                winner == "other"
1271
 
        if winner == "this":
1272
 
            if file_status == "modified":
1273
 
                executability = self.this_tree.is_executable(file_id)
1274
 
                if executability is not None:
1275
 
                    trans_id = self.tt.get_trans_id(file_id)
1276
 
                    self.tt.set_executability(executability, trans_id)
1277
 
        else:
1278
 
            assert winner == "other"
1279
 
            if file_id in self.other_tree:
1280
 
                executability = self.other_tree.is_executable(file_id)
1281
 
            elif file_id in self.this_tree:
1282
 
                executability = self.this_tree.is_executable(file_id)
1283
 
            elif file_id in self.base_tree:
1284
 
                executability = self.base_tree.is_executable(file_id)
1285
 
            if executability is not None:
1286
 
                trans_id = self.tt.get_trans_id(file_id)
1287
 
                self.tt.set_executability(executability, trans_id)
1288
 
 
1289
 
    def cook_conflicts(self):
1290
 
        """Convert all conflicts into a form that doesn't depend on trans_id"""
1291
 
        name_conflicts = {}
1292
 
        fp = FinalPaths(self.tt)
1293
 
        for conflict in self._raw_conflicts:
1294
 
            conflict_type = conflict[0]
1295
 
            if conflict_type in ('name conflict', 'parent conflict'):
1296
 
                trans_id = conflict[1]
1297
 
                conflict_args = conflict[2:]
1298
 
                if trans_id not in name_conflicts:
1299
 
                    name_conflicts[trans_id] = {}
1300
 
                unique_add(name_conflicts[trans_id], conflict_type, 
1301
 
                           conflict_args)
1302
 
            if conflict_type == 'contents conflict':
1303
 
                for trans_id in conflict[1]:
1304
 
                    file_id = self.tt.final_file_id(trans_id)
1305
 
                    if file_id is not None:
1306
 
                        break
1307
 
                path = fp.get_path(trans_id)
1308
 
                for suffix in ('.BASE', '.THIS', '.OTHER'):
1309
 
                    if path.endswith(suffix):
1310
 
                        path = path[:-len(suffix)]
1311
 
                        break
1312
 
                self.cooked_conflicts.append((conflict_type, file_id, path))
1313
 
            if conflict_type == 'text conflict':
1314
 
                trans_id = conflict[1]
1315
 
                path = fp.get_path(trans_id)
1316
 
                file_id = self.tt.final_file_id(trans_id)
1317
 
                self.cooked_conflicts.append((conflict_type, file_id, path))
1318
 
 
1319
 
        for trans_id, conflicts in name_conflicts.iteritems():
1320
 
            try:
1321
 
                this_parent, other_parent = conflicts['parent conflict']
1322
 
                assert this_parent != other_parent
1323
 
            except KeyError:
1324
 
                this_parent = other_parent = \
1325
 
                    self.tt.final_file_id(self.tt.final_parent(trans_id))
1326
 
            try:
1327
 
                this_name, other_name = conflicts['name conflict']
1328
 
                assert this_name != other_name
1329
 
            except KeyError:
1330
 
                this_name = other_name = self.tt.final_name(trans_id)
1331
 
            other_path = fp.get_path(trans_id)
1332
 
            if this_parent is not None:
1333
 
                this_parent_path = \
1334
 
                    fp.get_path(self.tt.get_trans_id(this_parent))
1335
 
                this_path = os.path.join(this_parent_path, this_name)
1336
 
            else:
1337
 
                this_path = "<deleted>"
1338
 
            file_id = self.tt.final_file_id(trans_id)
1339
 
            self.cooked_conflicts.append(('path conflict', file_id, this_path, 
1340
 
                                         other_path))
1341
 
            
1342
 
 
1343
 
class WeaveMerger(Merge3Merger):
1344
 
    supports_reprocess = False
1345
 
    supports_show_base = False
1346
 
 
1347
 
    def __init__(self, working_tree, this_tree, base_tree, other_tree):
1348
 
        self.this_revision_tree = self._get_revision_tree(this_tree)
1349
 
        self.other_revision_tree = self._get_revision_tree(other_tree)
1350
 
        super(WeaveMerger, self).__init__(working_tree, this_tree, 
1351
 
                                          base_tree, other_tree)
1352
 
 
1353
 
    def _get_revision_tree(self, tree):
1354
 
        if getattr(tree, 'get_weave', False) is False:
1355
 
            # If we have a WorkingTree, try using the basis
1356
 
            return tree.branch.basis_tree()
1357
 
        else:
1358
 
            return tree
1359
 
 
1360
 
    def _check_file(self, file_id):
1361
 
        """Check that the revision tree's version of the file matches."""
1362
 
        for tree, rt in ((self.this_tree, self.this_revision_tree), 
1363
 
                         (self.other_tree, self.other_revision_tree)):
1364
 
            if rt is tree:
1365
 
                continue
1366
 
            if tree.get_file_sha1(file_id) != rt.get_file_sha1(file_id):
1367
 
                raise WorkingTreeNotRevision(self.this_tree)
1368
 
 
1369
 
    def _merged_lines(self, file_id):
1370
 
        """Generate the merged lines.
1371
 
        There is no distinction between lines that are meant to contain <<<<<<<
1372
 
        and conflicts.
1373
 
        """
1374
 
        weave = self.this_revision_tree.get_weave(file_id)
1375
 
        this_revision_id = self.this_revision_tree.inventory[file_id].revision
1376
 
        other_revision_id = \
1377
 
            self.other_revision_tree.inventory[file_id].revision
1378
 
        this_i = weave.lookup(this_revision_id)
1379
 
        other_i = weave.lookup(other_revision_id)
1380
 
        plan =  weave.plan_merge(this_i, other_i)
1381
 
        return weave.weave_merge(plan)
1382
 
 
1383
 
    def text_merge(self, file_id, trans_id):
1384
 
        self._check_file(file_id)
1385
 
        lines = self._merged_lines(file_id)
1386
 
        conflicts = '<<<<<<<\n' in lines
1387
 
        self.tt.create_file(lines, trans_id)
1388
 
        if conflicts:
1389
 
            self._raw_conflicts.append(('text conflict', trans_id))
1390
 
            name = self.tt.final_name(trans_id)
1391
 
            parent_id = self.tt.final_parent(trans_id)
1392
 
            file_group = self._dump_conflicts(name, parent_id, file_id, 
1393
 
                                              no_base=True)
1394
 
            file_group.append(trans_id)
1395
 
 
1396
 
 
1397
 
class Diff3Merger(Merge3Merger):
1398
 
    """Use good ol' diff3 to do text merges"""
1399
 
    def dump_file(self, temp_dir, name, tree, file_id):
1400
 
        out_path = pathjoin(temp_dir, name)
1401
 
        out_file = file(out_path, "wb")
1402
 
        in_file = tree.get_file(file_id)
1403
 
        for line in in_file:
1404
 
            out_file.write(line)
1405
 
        return out_path
1406
 
 
1407
 
    def text_merge(self, file_id, trans_id):
1408
 
        import bzrlib.patch
1409
 
        temp_dir = mkdtemp(prefix="bzr-")
1410
 
        try:
1411
 
            new_file = os.path.join(temp_dir, "new")
1412
 
            this = self.dump_file(temp_dir, "this", self.this_tree, file_id)
1413
 
            base = self.dump_file(temp_dir, "base", self.base_tree, file_id)
1414
 
            other = self.dump_file(temp_dir, "other", self.other_tree, file_id)
1415
 
            status = bzrlib.patch.diff3(new_file, this, base, other)
1416
 
            if status not in (0, 1):
1417
 
                raise BzrError("Unhandled diff3 exit code")
1418
 
            self.tt.create_file(file(new_file, "rb"), trans_id)
1419
 
            if status == 1:
1420
 
                name = self.tt.final_name(trans_id)
1421
 
                parent_id = self.tt.final_parent(trans_id)
1422
 
                self._dump_conflicts(name, parent_id, file_id)
1423
 
            self._raw_conflicts.append(('text conflict', trans_id))
1424
 
        finally:
1425
 
            rmtree(temp_dir)