~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bzrdir.py

  • Committer: Robert Collins
  • Date: 2006-02-16 07:20:25 UTC
  • mto: (1534.1.24 integration)
  • mto: This revision was merged to the branch mainline in revision 1554.
  • Revision ID: robertc@robertcollins.net-20060216072025-8a97e0b2c3f3cc92
Start factoring out the upgrade policy logic.

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
"""
22
22
 
23
23
from copy import deepcopy
 
24
import os
24
25
from cStringIO import StringIO
25
26
from unittest import TestSuite
26
27
 
27
 
 
28
28
import bzrlib
29
29
import bzrlib.errors as errors
30
30
from bzrlib.lockable_files import LockableFiles
31
31
from bzrlib.osutils import safe_unicode
 
32
from bzrlib.osutils import (
 
33
                            abspath,
 
34
                            pathjoin,
 
35
                            safe_unicode,
 
36
                            sha_strings,
 
37
                            sha_string,
 
38
                            )
 
39
from bzrlib.store.text import TextStore
 
40
from bzrlib.store.weave import WeaveStore
 
41
from bzrlib.symbol_versioning import *
32
42
from bzrlib.trace import mutter
33
 
from bzrlib.symbol_versioning import *
 
43
from bzrlib.transactions import PassThroughTransaction
34
44
from bzrlib.transport import get_transport
35
45
from bzrlib.transport.local import LocalTransport
 
46
from bzrlib.weave import Weave
 
47
from bzrlib.weavefile import read_weave, write_weave
 
48
from bzrlib.xml4 import serializer_v4
 
49
from bzrlib.xml5 import serializer_v5
36
50
 
37
51
 
38
52
class BzrDir(object):
47
61
        a transport connected to the directory this bzr was opened from.
48
62
    """
49
63
 
 
64
    def can_update_format(self):
 
65
        """Return true if this bzrdir is one whose format we can update."""
 
66
        return True
 
67
 
50
68
    def _check_supported(self, format, allow_unsupported):
51
69
        """Check whether format is a supported format.
52
70
 
344
362
        self.transport = _transport.clone('.bzr')
345
363
        self.root_transport = _transport
346
364
 
 
365
    def needs_format_update(self):
 
366
        """Return true if this bzrdir needs update_format run on it.
 
367
        
 
368
        For instance, if the repository format is out of date but the 
 
369
        branch and working tree are not, this should return True.
 
370
        """
 
371
        # for now, if the format is not the same as the system default,
 
372
        # an upgrade is needed.
 
373
        return not isinstance(self._format, BzrDirFormat.get_default_format().__class__)
 
374
 
347
375
    @staticmethod
348
376
    def open_unsupported(base):
349
377
        """Open a branch which is not supported."""
614
642
        from bzrlib.repository import RepositoryFormat4
615
643
        return RepositoryFormat4().initialize(self, shared)
616
644
 
 
645
    def needs_format_update(self):
 
646
        """Format 4 dirs are always in need of updating."""
 
647
        return True
 
648
 
617
649
    def open_repository(self):
618
650
        """See BzrDir.open_repository."""
619
651
        from bzrlib.repository import RepositoryFormat4
1088
1120
        copytree(self.base, base, symlinks=True)
1089
1121
        return ScratchDir(
1090
1122
            transport=bzrlib.transport.local.ScratchTransport(base))
 
1123
 
 
1124
 
 
1125
class Converter(object):
 
1126
    """Converts a disk format object from one format to another."""
 
1127
 
 
1128
    def __init__(self, pb):
 
1129
        """Create a converter.
 
1130
 
 
1131
        :param pb: a progress bar to use for progress information.
 
1132
        """
 
1133
        self.pb = pb
 
1134
 
 
1135
 
 
1136
class ConvertBzrDir4To5(Converter):
 
1137
    """Converts format 4 bzr dirs to format 5."""
 
1138
 
 
1139
    def __init__(self, to_convert, pb):
 
1140
        """Create a converter.
 
1141
 
 
1142
        :param to_convert: The disk object to convert.
 
1143
        :param pb: a progress bar to use for progress information.
 
1144
        """
 
1145
        super(ConvertBzrDir4To5, self).__init__(pb)
 
1146
        self.bzrdir = to_convert
 
1147
        self.converted_revs = set()
 
1148
        self.absent_revisions = set()
 
1149
        self.text_count = 0
 
1150
        self.revisions = {}
 
1151
        
 
1152
    def convert(self):
 
1153
        """See Converter.convert()."""
 
1154
        self.pb.note('starting upgrade from format 4 to 5')
 
1155
        if isinstance(self.bzrdir.transport, LocalTransport):
 
1156
            self.bzrdir.get_workingtree_transport(None).delete('stat-cache')
 
1157
        self._convert_to_weaves()
 
1158
        return BzrDir.open(self.bzrdir.root_transport.base)
 
1159
 
 
1160
    def _convert_to_weaves(self):
 
1161
        self.pb.note('note: upgrade may be faster if all store files are ungzipped first')
 
1162
        try:
 
1163
            # TODO permissions
 
1164
            stat = self.bzrdir.transport.stat('weaves')
 
1165
            if not S_ISDIR(stat.st_mode):
 
1166
                self.bzrdir.transport.delete('weaves')
 
1167
                self.bzrdir.transport.mkdir('weaves')
 
1168
        except errors.NoSuchFile:
 
1169
            self.bzrdir.transport.mkdir('weaves')
 
1170
        self.inv_weave = Weave('inventory')
 
1171
        # holds in-memory weaves for all files
 
1172
        self.text_weaves = {}
 
1173
        self.bzrdir.transport.delete('branch-format')
 
1174
        self.branch = self.bzrdir.open_branch()
 
1175
        self._convert_working_inv()
 
1176
        rev_history = self.branch.revision_history()
 
1177
        # to_read is a stack holding the revisions we still need to process;
 
1178
        # appending to it adds new highest-priority revisions
 
1179
        self.known_revisions = set(rev_history)
 
1180
        self.to_read = rev_history[-1:]
 
1181
        while self.to_read:
 
1182
            rev_id = self.to_read.pop()
 
1183
            if (rev_id not in self.revisions
 
1184
                and rev_id not in self.absent_revisions):
 
1185
                self._load_one_rev(rev_id)
 
1186
        self.pb.clear()
 
1187
        to_import = self._make_order()
 
1188
        for i, rev_id in enumerate(to_import):
 
1189
            self.pb.update('converting revision', i, len(to_import))
 
1190
            self._convert_one_rev(rev_id)
 
1191
        self.pb.clear()
 
1192
        self._write_all_weaves()
 
1193
        self._write_all_revs()
 
1194
        self.pb.note('upgraded to weaves:')
 
1195
        self.pb.note('  %6d revisions and inventories', len(self.revisions))
 
1196
        self.pb.note('  %6d revisions not present', len(self.absent_revisions))
 
1197
        self.pb.note('  %6d texts', self.text_count)
 
1198
        self._cleanup_spare_files_after_format4()
 
1199
        self.branch.control_files.put_utf8('branch-format', BzrDirFormat5().get_format_string())
 
1200
 
 
1201
    def _cleanup_spare_files_after_format4(self):
 
1202
        # FIXME working tree upgrade foo.
 
1203
        for n in 'merged-patches', 'pending-merged-patches':
 
1204
            try:
 
1205
                ## assert os.path.getsize(p) == 0
 
1206
                self.bzrdir.transport.delete(n)
 
1207
            except errors.NoSuchFile:
 
1208
                pass
 
1209
        self.bzrdir.transport.delete_tree('inventory-store')
 
1210
        self.bzrdir.transport.delete_tree('text-store')
 
1211
 
 
1212
    def _convert_working_inv(self):
 
1213
        inv = serializer_v4.read_inventory(self.branch.control_files.get('inventory'))
 
1214
        new_inv_xml = serializer_v5.write_inventory_to_string(inv)
 
1215
        # FIXME inventory is a working tree change.
 
1216
        self.branch.control_files.put('inventory', new_inv_xml)
 
1217
 
 
1218
    def _write_all_weaves(self):
 
1219
        controlweaves = WeaveStore(self.bzrdir.transport, prefixed=False)
 
1220
        weave_transport = self.bzrdir.transport.clone('weaves')
 
1221
        weaves = WeaveStore(weave_transport, prefixed=False)
 
1222
        transaction = PassThroughTransaction()
 
1223
 
 
1224
        controlweaves.put_weave('inventory', self.inv_weave, transaction)
 
1225
        i = 0
 
1226
        try:
 
1227
            for file_id, file_weave in self.text_weaves.items():
 
1228
                self.pb.update('writing weave', i, len(self.text_weaves))
 
1229
                weaves.put_weave(file_id, file_weave, transaction)
 
1230
                i += 1
 
1231
        finally:
 
1232
            self.pb.clear()
 
1233
 
 
1234
    def _write_all_revs(self):
 
1235
        """Write all revisions out in new form."""
 
1236
        self.bzrdir.transport.delete_tree('revision-store')
 
1237
        self.bzrdir.transport.mkdir('revision-store')
 
1238
        revision_transport = self.bzrdir.transport.clone('revision-store')
 
1239
        # TODO permissions
 
1240
        revision_store = TextStore(revision_transport,
 
1241
                                   prefixed=False,
 
1242
                                   compressed=True)
 
1243
        try:
 
1244
            for i, rev_id in enumerate(self.converted_revs):
 
1245
                self.pb.update('write revision', i, len(self.converted_revs))
 
1246
                rev_tmp = StringIO()
 
1247
                serializer_v5.write_revision(self.revisions[rev_id], rev_tmp)
 
1248
                rev_tmp.seek(0)
 
1249
                revision_store.add(rev_tmp, rev_id)
 
1250
        finally:
 
1251
            self.pb.clear()
 
1252
 
 
1253
            
 
1254
    def _load_one_rev(self, rev_id):
 
1255
        """Load a revision object into memory.
 
1256
 
 
1257
        Any parents not either loaded or abandoned get queued to be
 
1258
        loaded."""
 
1259
        self.pb.update('loading revision',
 
1260
                       len(self.revisions),
 
1261
                       len(self.known_revisions))
 
1262
        if not self.branch.repository.revision_store.has_id(rev_id):
 
1263
            self.pb.clear()
 
1264
            self.pb.note('revision {%s} not present in branch; '
 
1265
                         'will be converted as a ghost',
 
1266
                         rev_id)
 
1267
            self.absent_revisions.add(rev_id)
 
1268
        else:
 
1269
            rev_xml = self.branch.repository.revision_store.get(rev_id).read()
 
1270
            rev = serializer_v4.read_revision_from_string(rev_xml)
 
1271
            for parent_id in rev.parent_ids:
 
1272
                self.known_revisions.add(parent_id)
 
1273
                self.to_read.append(parent_id)
 
1274
            self.revisions[rev_id] = rev
 
1275
 
 
1276
 
 
1277
    def _load_old_inventory(self, rev_id):
 
1278
        assert rev_id not in self.converted_revs
 
1279
        old_inv_xml = self.branch.repository.inventory_store.get(rev_id).read()
 
1280
        inv = serializer_v4.read_inventory_from_string(old_inv_xml)
 
1281
        rev = self.revisions[rev_id]
 
1282
        if rev.inventory_sha1:
 
1283
            assert rev.inventory_sha1 == sha_string(old_inv_xml), \
 
1284
                'inventory sha mismatch for {%s}' % rev_id
 
1285
        return inv
 
1286
        
 
1287
 
 
1288
    def _load_updated_inventory(self, rev_id):
 
1289
        assert rev_id in self.converted_revs
 
1290
        inv_xml = self.inv_weave.get_text(rev_id)
 
1291
        inv = serializer_v5.read_inventory_from_string(inv_xml)
 
1292
        return inv
 
1293
 
 
1294
 
 
1295
    def _convert_one_rev(self, rev_id):
 
1296
        """Convert revision and all referenced objects to new format."""
 
1297
        rev = self.revisions[rev_id]
 
1298
        inv = self._load_old_inventory(rev_id)
 
1299
        present_parents = [p for p in rev.parent_ids
 
1300
                           if p not in self.absent_revisions]
 
1301
        self._convert_revision_contents(rev, inv, present_parents)
 
1302
        self._store_new_weave(rev, inv, present_parents)
 
1303
        self.converted_revs.add(rev_id)
 
1304
 
 
1305
 
 
1306
    def _store_new_weave(self, rev, inv, present_parents):
 
1307
        # the XML is now updated with text versions
 
1308
        if __debug__:
 
1309
            for file_id in inv:
 
1310
                ie = inv[file_id]
 
1311
                if ie.kind == 'root_directory':
 
1312
                    continue
 
1313
                assert hasattr(ie, 'revision'), \
 
1314
                    'no revision on {%s} in {%s}' % \
 
1315
                    (file_id, rev.revision_id)
 
1316
        new_inv_xml = serializer_v5.write_inventory_to_string(inv)
 
1317
        new_inv_sha1 = sha_string(new_inv_xml)
 
1318
        self.inv_weave.add(rev.revision_id, 
 
1319
                           present_parents,
 
1320
                           new_inv_xml.splitlines(True),
 
1321
                           new_inv_sha1)
 
1322
        rev.inventory_sha1 = new_inv_sha1
 
1323
 
 
1324
    def _convert_revision_contents(self, rev, inv, present_parents):
 
1325
        """Convert all the files within a revision.
 
1326
 
 
1327
        Also upgrade the inventory to refer to the text revision ids."""
 
1328
        rev_id = rev.revision_id
 
1329
        mutter('converting texts of revision {%s}',
 
1330
               rev_id)
 
1331
        parent_invs = map(self._load_updated_inventory, present_parents)
 
1332
        for file_id in inv:
 
1333
            ie = inv[file_id]
 
1334
            self._convert_file_version(rev, ie, parent_invs)
 
1335
 
 
1336
    def _convert_file_version(self, rev, ie, parent_invs):
 
1337
        """Convert one version of one file.
 
1338
 
 
1339
        The file needs to be added into the weave if it is a merge
 
1340
        of >=2 parents or if it's changed from its parent.
 
1341
        """
 
1342
        if ie.kind == 'root_directory':
 
1343
            return
 
1344
        file_id = ie.file_id
 
1345
        rev_id = rev.revision_id
 
1346
        w = self.text_weaves.get(file_id)
 
1347
        if w is None:
 
1348
            w = Weave(file_id)
 
1349
            self.text_weaves[file_id] = w
 
1350
        text_changed = False
 
1351
        previous_entries = ie.find_previous_heads(parent_invs, w)
 
1352
        for old_revision in previous_entries:
 
1353
                # if this fails, its a ghost ?
 
1354
                assert old_revision in self.converted_revs 
 
1355
        self.snapshot_ie(previous_entries, ie, w, rev_id)
 
1356
        del ie.text_id
 
1357
        assert getattr(ie, 'revision', None) is not None
 
1358
 
 
1359
    def snapshot_ie(self, previous_revisions, ie, w, rev_id):
 
1360
        # TODO: convert this logic, which is ~= snapshot to
 
1361
        # a call to:. This needs the path figured out. rather than a work_tree
 
1362
        # a v4 revision_tree can be given, or something that looks enough like
 
1363
        # one to give the file content to the entry if it needs it.
 
1364
        # and we need something that looks like a weave store for snapshot to 
 
1365
        # save against.
 
1366
        #ie.snapshot(rev, PATH, previous_revisions, REVISION_TREE, InMemoryWeaveStore(self.text_weaves))
 
1367
        if len(previous_revisions) == 1:
 
1368
            previous_ie = previous_revisions.values()[0]
 
1369
            if ie._unchanged(previous_ie):
 
1370
                ie.revision = previous_ie.revision
 
1371
                return
 
1372
        parent_indexes = map(w.lookup, previous_revisions)
 
1373
        if ie.has_text():
 
1374
            text = self.branch.repository.text_store.get(ie.text_id)
 
1375
            file_lines = text.readlines()
 
1376
            assert sha_strings(file_lines) == ie.text_sha1
 
1377
            assert sum(map(len, file_lines)) == ie.text_size
 
1378
            w.add(rev_id, parent_indexes, file_lines, ie.text_sha1)
 
1379
            self.text_count += 1
 
1380
        else:
 
1381
            w.add(rev_id, parent_indexes, [], None)
 
1382
        ie.revision = rev_id
 
1383
        ##mutter('import text {%s} of {%s}',
 
1384
        ##       ie.text_id, file_id)
 
1385
 
 
1386
    def _make_order(self):
 
1387
        """Return a suitable order for importing revisions.
 
1388
 
 
1389
        The order must be such that an revision is imported after all
 
1390
        its (present) parents.
 
1391
        """
 
1392
        todo = set(self.revisions.keys())
 
1393
        done = self.absent_revisions.copy()
 
1394
        o = []
 
1395
        while todo:
 
1396
            # scan through looking for a revision whose parents
 
1397
            # are all done
 
1398
            for rev_id in sorted(list(todo)):
 
1399
                rev = self.revisions[rev_id]
 
1400
                parent_ids = set(rev.parent_ids)
 
1401
                if parent_ids.issubset(done):
 
1402
                    # can take this one now
 
1403
                    o.append(rev_id)
 
1404
                    todo.remove(rev_id)
 
1405
                    done.add(rev_id)
 
1406
        return o
 
1407
 
 
1408
 
 
1409
class ConvertBzrDir5To6(Converter):
 
1410
    """Converts format 5 bzr dirs to format 6."""
 
1411
 
 
1412
    def __init__(self, to_convert, pb):
 
1413
        """Create a converter.
 
1414
 
 
1415
        :param to_convert: The disk object to convert.
 
1416
        :param pb: a progress bar to use for progress information.
 
1417
        """
 
1418
        super(ConvertBzrDir5To6, self).__init__(pb)
 
1419
        self.bzrdir = to_convert
 
1420
        
 
1421
    def convert(self):
 
1422
        """See Converter.convert()."""
 
1423
        self.pb.note('starting upgrade from format 5 to 6')
 
1424
        self._convert_to_prefixed()
 
1425
        return BzrDir.open(self.bzrdir.root_transport.base)
 
1426
 
 
1427
    def _convert_to_prefixed(self):
 
1428
        from bzrlib.store import hash_prefix
 
1429
        self.bzrdir.transport.delete('branch-format')
 
1430
        for store_name in ["weaves", "revision-store"]:
 
1431
            self.pb.note("adding prefixes to %s" % store_name) 
 
1432
            store_transport = self.bzrdir.transport.clone(store_name)
 
1433
            for filename in store_transport.list_dir('.'):
 
1434
                if (filename.endswith(".weave") or
 
1435
                    filename.endswith(".gz") or
 
1436
                    filename.endswith(".sig")):
 
1437
                    file_id = os.path.splitext(filename)[0]
 
1438
                else:
 
1439
                    file_id = filename
 
1440
                prefix_dir = hash_prefix(file_id)
 
1441
                # FIXME keep track of the dirs made RBC 20060121
 
1442
                try:
 
1443
                    store_transport.move(filename, prefix_dir + '/' + filename)
 
1444
                except errors.NoSuchFile: # catches missing dirs strangely enough
 
1445
                    store_transport.mkdir(prefix_dir)
 
1446
                    store_transport.move(filename, prefix_dir + '/' + filename)
 
1447
        self.bzrdir._control_files.put_utf8('branch-format', BzrDirFormat6().get_format_string())
 
1448
 
 
1449