/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/upgrade.py

  • Committer: Robert Collins
  • Date: 2006-02-16 05:54:02 UTC
  • mto: (1534.1.24 integration)
  • mto: This revision was merged to the branch mainline in revision 1554.
  • Revision ID: robertc@robertcollins.net-20060216055402-bb6afc4d15c715cd
split out converter logic into per-format objects.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""bzr upgrade logic."""
 
18
 
 
19
# change upgrade from .bzr to create a '.bzr-new', then do a bait and switch.
 
20
 
 
21
 
 
22
from cStringIO import StringIO
 
23
import os
 
24
import tempfile
 
25
import sys
 
26
from stat import *
 
27
 
 
28
import bzrlib
 
29
from bzrlib.branch import Branch
 
30
import bzrlib.bzrdir as bzrdir
 
31
from bzrlib.bzrdir import BzrDirFormat, BzrDirFormat4, BzrDirFormat5, BzrDirFormat6
 
32
import bzrlib.errors as errors
 
33
from bzrlib.errors import NoSuchFile, UpgradeReadonly
 
34
from bzrlib.lockable_files import LockableFiles
 
35
from bzrlib.osutils import sha_strings, sha_string, pathjoin, abspath
 
36
from bzrlib.ui import ui_factory
 
37
from bzrlib.store.text import TextStore
 
38
from bzrlib.store.weave import WeaveStore
 
39
from bzrlib.trace import mutter, note, warning
 
40
from bzrlib.transactions import PassThroughTransaction
 
41
from bzrlib.transport import get_transport
 
42
from bzrlib.transport.local import LocalTransport
 
43
from bzrlib.weave import Weave
 
44
from bzrlib.weavefile import read_weave, write_weave
 
45
from bzrlib.xml4 import serializer_v4
 
46
from bzrlib.xml5 import serializer_v5
 
47
 
 
48
 
 
49
class Converter(object):
 
50
    """Converts a disk format object from one format to another."""
 
51
 
 
52
    def __init__(self, pb):
 
53
        """Create a converter.
 
54
 
 
55
        :param pb: a progress bar to use for progress information.
 
56
        """
 
57
        self.pb = pb
 
58
 
 
59
 
 
60
class ConvertBzrDir4To5(Converter):
 
61
    """Converts format 4 bzr dirs to format 5."""
 
62
 
 
63
    def __init__(self, to_convert, pb):
 
64
        """Create a converter.
 
65
 
 
66
        :param to_convert: The disk object to convert.
 
67
        :param pb: a progress bar to use for progress information.
 
68
        """
 
69
        super(ConvertBzrDir4To5, self).__init__(pb)
 
70
        self.bzrdir = to_convert
 
71
        self.converted_revs = set()
 
72
        self.absent_revisions = set()
 
73
        self.text_count = 0
 
74
        self.revisions = {}
 
75
        
 
76
    def convert(self):
 
77
        """See Converter.convert()."""
 
78
        self.pb.note('starting upgrade from format 4 to 5')
 
79
        if isinstance(self.bzrdir.transport, LocalTransport):
 
80
            self.bzrdir.get_workingtree_transport(None).delete('stat-cache')
 
81
        self._convert_to_weaves()
 
82
        return bzrdir.BzrDir.open(self.bzrdir.root_transport.base)
 
83
 
 
84
    def _convert_to_weaves(self):
 
85
        self.pb.note('note: upgrade may be faster if all store files are ungzipped first')
 
86
        try:
 
87
            # TODO permissions
 
88
            stat = self.bzrdir.transport.stat('weaves')
 
89
            if not S_ISDIR(stat.st_mode):
 
90
                self.bzrdir.transport.delete('weaves')
 
91
                self.bzrdir.transport.mkdir('weaves')
 
92
        except NoSuchFile:
 
93
            self.bzrdir.transport.mkdir('weaves')
 
94
        self.inv_weave = Weave('inventory')
 
95
        # holds in-memory weaves for all files
 
96
        self.text_weaves = {}
 
97
        self.bzrdir.transport.delete('branch-format')
 
98
        self.branch = self.bzrdir.open_branch()
 
99
        self._convert_working_inv()
 
100
        rev_history = self.branch.revision_history()
 
101
        # to_read is a stack holding the revisions we still need to process;
 
102
        # appending to it adds new highest-priority revisions
 
103
        self.known_revisions = set(rev_history)
 
104
        self.to_read = rev_history[-1:]
 
105
        while self.to_read:
 
106
            rev_id = self.to_read.pop()
 
107
            if (rev_id not in self.revisions
 
108
                and rev_id not in self.absent_revisions):
 
109
                self._load_one_rev(rev_id)
 
110
        self.pb.clear()
 
111
        to_import = self._make_order()
 
112
        for i, rev_id in enumerate(to_import):
 
113
            self.pb.update('converting revision', i, len(to_import))
 
114
            self._convert_one_rev(rev_id)
 
115
        self.pb.clear()
 
116
        self._write_all_weaves()
 
117
        self._write_all_revs()
 
118
        self.pb.note('upgraded to weaves:')
 
119
        self.pb.note('  %6d revisions and inventories', len(self.revisions))
 
120
        self.pb.note('  %6d revisions not present', len(self.absent_revisions))
 
121
        self.pb.note('  %6d texts', self.text_count)
 
122
        self._cleanup_spare_files_after_format4()
 
123
        self.branch.control_files.put_utf8('branch-format', BzrDirFormat5().get_format_string())
 
124
 
 
125
    def _cleanup_spare_files_after_format4(self):
 
126
        # FIXME working tree upgrade foo.
 
127
        for n in 'merged-patches', 'pending-merged-patches':
 
128
            try:
 
129
                ## assert os.path.getsize(p) == 0
 
130
                self.bzrdir.transport.delete(n)
 
131
            except NoSuchFile:
 
132
                pass
 
133
        self.bzrdir.transport.delete_tree('inventory-store')
 
134
        self.bzrdir.transport.delete_tree('text-store')
 
135
 
 
136
    def _convert_working_inv(self):
 
137
        inv = serializer_v4.read_inventory(self.branch.control_files.get('inventory'))
 
138
        new_inv_xml = serializer_v5.write_inventory_to_string(inv)
 
139
        # FIXME inventory is a working tree change.
 
140
        self.branch.control_files.put('inventory', new_inv_xml)
 
141
 
 
142
    def _write_all_weaves(self):
 
143
        controlweaves = WeaveStore(self.bzrdir.transport, prefixed=False)
 
144
        weave_transport = self.bzrdir.transport.clone('weaves')
 
145
        weaves = WeaveStore(weave_transport, prefixed=False)
 
146
        transaction = PassThroughTransaction()
 
147
 
 
148
        controlweaves.put_weave('inventory', self.inv_weave, transaction)
 
149
        i = 0
 
150
        try:
 
151
            for file_id, file_weave in self.text_weaves.items():
 
152
                self.pb.update('writing weave', i, len(self.text_weaves))
 
153
                weaves.put_weave(file_id, file_weave, transaction)
 
154
                i += 1
 
155
        finally:
 
156
            self.pb.clear()
 
157
 
 
158
    def _write_all_revs(self):
 
159
        """Write all revisions out in new form."""
 
160
        self.bzrdir.transport.delete_tree('revision-store')
 
161
        self.bzrdir.transport.mkdir('revision-store')
 
162
        revision_transport = self.bzrdir.transport.clone('revision-store')
 
163
        # TODO permissions
 
164
        revision_store = TextStore(revision_transport,
 
165
                                   prefixed=False,
 
166
                                   compressed=True)
 
167
        try:
 
168
            for i, rev_id in enumerate(self.converted_revs):
 
169
                self.pb.update('write revision', i, len(self.converted_revs))
 
170
                rev_tmp = StringIO()
 
171
                serializer_v5.write_revision(self.revisions[rev_id], rev_tmp)
 
172
                rev_tmp.seek(0)
 
173
                revision_store.add(rev_tmp, rev_id)
 
174
        finally:
 
175
            self.pb.clear()
 
176
 
 
177
            
 
178
    def _load_one_rev(self, rev_id):
 
179
        """Load a revision object into memory.
 
180
 
 
181
        Any parents not either loaded or abandoned get queued to be
 
182
        loaded."""
 
183
        self.pb.update('loading revision',
 
184
                       len(self.revisions),
 
185
                       len(self.known_revisions))
 
186
        if not self.branch.repository.revision_store.has_id(rev_id):
 
187
            self.pb.clear()
 
188
            note('revision {%s} not present in branch; '
 
189
                 'will be converted as a ghost',
 
190
                 rev_id)
 
191
            self.absent_revisions.add(rev_id)
 
192
        else:
 
193
            rev_xml = self.branch.repository.revision_store.get(rev_id).read()
 
194
            rev = serializer_v4.read_revision_from_string(rev_xml)
 
195
            for parent_id in rev.parent_ids:
 
196
                self.known_revisions.add(parent_id)
 
197
                self.to_read.append(parent_id)
 
198
            self.revisions[rev_id] = rev
 
199
 
 
200
 
 
201
    def _load_old_inventory(self, rev_id):
 
202
        assert rev_id not in self.converted_revs
 
203
        old_inv_xml = self.branch.repository.inventory_store.get(rev_id).read()
 
204
        inv = serializer_v4.read_inventory_from_string(old_inv_xml)
 
205
        rev = self.revisions[rev_id]
 
206
        if rev.inventory_sha1:
 
207
            assert rev.inventory_sha1 == sha_string(old_inv_xml), \
 
208
                'inventory sha mismatch for {%s}' % rev_id
 
209
        return inv
 
210
        
 
211
 
 
212
    def _load_updated_inventory(self, rev_id):
 
213
        assert rev_id in self.converted_revs
 
214
        inv_xml = self.inv_weave.get_text(rev_id)
 
215
        inv = serializer_v5.read_inventory_from_string(inv_xml)
 
216
        return inv
 
217
 
 
218
 
 
219
    def _convert_one_rev(self, rev_id):
 
220
        """Convert revision and all referenced objects to new format."""
 
221
        rev = self.revisions[rev_id]
 
222
        inv = self._load_old_inventory(rev_id)
 
223
        present_parents = [p for p in rev.parent_ids
 
224
                           if p not in self.absent_revisions]
 
225
        self._convert_revision_contents(rev, inv, present_parents)
 
226
        self._store_new_weave(rev, inv, present_parents)
 
227
        self.converted_revs.add(rev_id)
 
228
 
 
229
 
 
230
    def _store_new_weave(self, rev, inv, present_parents):
 
231
        # the XML is now updated with text versions
 
232
        if __debug__:
 
233
            for file_id in inv:
 
234
                ie = inv[file_id]
 
235
                if ie.kind == 'root_directory':
 
236
                    continue
 
237
                assert hasattr(ie, 'revision'), \
 
238
                    'no revision on {%s} in {%s}' % \
 
239
                    (file_id, rev.revision_id)
 
240
        new_inv_xml = serializer_v5.write_inventory_to_string(inv)
 
241
        new_inv_sha1 = sha_string(new_inv_xml)
 
242
        self.inv_weave.add(rev.revision_id, 
 
243
                           present_parents,
 
244
                           new_inv_xml.splitlines(True),
 
245
                           new_inv_sha1)
 
246
        rev.inventory_sha1 = new_inv_sha1
 
247
 
 
248
    def _convert_revision_contents(self, rev, inv, present_parents):
 
249
        """Convert all the files within a revision.
 
250
 
 
251
        Also upgrade the inventory to refer to the text revision ids."""
 
252
        rev_id = rev.revision_id
 
253
        mutter('converting texts of revision {%s}',
 
254
               rev_id)
 
255
        parent_invs = map(self._load_updated_inventory, present_parents)
 
256
        for file_id in inv:
 
257
            ie = inv[file_id]
 
258
            self._convert_file_version(rev, ie, parent_invs)
 
259
 
 
260
    def _convert_file_version(self, rev, ie, parent_invs):
 
261
        """Convert one version of one file.
 
262
 
 
263
        The file needs to be added into the weave if it is a merge
 
264
        of >=2 parents or if it's changed from its parent.
 
265
        """
 
266
        if ie.kind == 'root_directory':
 
267
            return
 
268
        file_id = ie.file_id
 
269
        rev_id = rev.revision_id
 
270
        w = self.text_weaves.get(file_id)
 
271
        if w is None:
 
272
            w = Weave(file_id)
 
273
            self.text_weaves[file_id] = w
 
274
        text_changed = False
 
275
        previous_entries = ie.find_previous_heads(parent_invs, w)
 
276
        for old_revision in previous_entries:
 
277
                # if this fails, its a ghost ?
 
278
                assert old_revision in self.converted_revs 
 
279
        self.snapshot_ie(previous_entries, ie, w, rev_id)
 
280
        del ie.text_id
 
281
        assert getattr(ie, 'revision', None) is not None
 
282
 
 
283
    def snapshot_ie(self, previous_revisions, ie, w, rev_id):
 
284
        # TODO: convert this logic, which is ~= snapshot to
 
285
        # a call to:. This needs the path figured out. rather than a work_tree
 
286
        # a v4 revision_tree can be given, or something that looks enough like
 
287
        # one to give the file content to the entry if it needs it.
 
288
        # and we need something that looks like a weave store for snapshot to 
 
289
        # save against.
 
290
        #ie.snapshot(rev, PATH, previous_revisions, REVISION_TREE, InMemoryWeaveStore(self.text_weaves))
 
291
        if len(previous_revisions) == 1:
 
292
            previous_ie = previous_revisions.values()[0]
 
293
            if ie._unchanged(previous_ie):
 
294
                ie.revision = previous_ie.revision
 
295
                return
 
296
        parent_indexes = map(w.lookup, previous_revisions)
 
297
        if ie.has_text():
 
298
            text = self.branch.repository.text_store.get(ie.text_id)
 
299
            file_lines = text.readlines()
 
300
            assert sha_strings(file_lines) == ie.text_sha1
 
301
            assert sum(map(len, file_lines)) == ie.text_size
 
302
            w.add(rev_id, parent_indexes, file_lines, ie.text_sha1)
 
303
            self.text_count += 1
 
304
        else:
 
305
            w.add(rev_id, parent_indexes, [], None)
 
306
        ie.revision = rev_id
 
307
        ##mutter('import text {%s} of {%s}',
 
308
        ##       ie.text_id, file_id)
 
309
 
 
310
    def _make_order(self):
 
311
        """Return a suitable order for importing revisions.
 
312
 
 
313
        The order must be such that an revision is imported after all
 
314
        its (present) parents.
 
315
        """
 
316
        todo = set(self.revisions.keys())
 
317
        done = self.absent_revisions.copy()
 
318
        o = []
 
319
        while todo:
 
320
            # scan through looking for a revision whose parents
 
321
            # are all done
 
322
            for rev_id in sorted(list(todo)):
 
323
                rev = self.revisions[rev_id]
 
324
                parent_ids = set(rev.parent_ids)
 
325
                if parent_ids.issubset(done):
 
326
                    # can take this one now
 
327
                    o.append(rev_id)
 
328
                    todo.remove(rev_id)
 
329
                    done.add(rev_id)
 
330
        return o
 
331
 
 
332
 
 
333
class ConvertBzrDir5To6(Converter):
 
334
    """Converts format 5 bzr dirs to format 6."""
 
335
 
 
336
    def __init__(self, to_convert, pb):
 
337
        """Create a converter.
 
338
 
 
339
        :param to_convert: The disk object to convert.
 
340
        :param pb: a progress bar to use for progress information.
 
341
        """
 
342
        super(ConvertBzrDir5To6, self).__init__(pb)
 
343
        self.bzrdir = to_convert
 
344
        
 
345
    def convert(self):
 
346
        """See Converter.convert()."""
 
347
        self.pb.note('starting upgrade from format 5 to 6')
 
348
        self._convert_to_prefixed()
 
349
        return bzrdir.BzrDir.open(self.bzrdir.root_transport.base)
 
350
 
 
351
    def _convert_to_prefixed(self):
 
352
        from bzrlib.store import hash_prefix
 
353
        self.bzrdir.transport.delete('branch-format')
 
354
        for store_name in ["weaves", "revision-store"]:
 
355
            note("adding prefixes to %s" % store_name) 
 
356
            store_transport = self.bzrdir.transport.clone(store_name)
 
357
            for filename in store_transport.list_dir('.'):
 
358
                if (filename.endswith(".weave") or
 
359
                    filename.endswith(".gz") or
 
360
                    filename.endswith(".sig")):
 
361
                    file_id = os.path.splitext(filename)[0]
 
362
                else:
 
363
                    file_id = filename
 
364
                prefix_dir = hash_prefix(file_id)
 
365
                # FIXME keep track of the dirs made RBC 20060121
 
366
                try:
 
367
                    store_transport.move(filename, prefix_dir + '/' + filename)
 
368
                except NoSuchFile: # catches missing dirs strangely enough
 
369
                    store_transport.mkdir(prefix_dir)
 
370
                    store_transport.move(filename, prefix_dir + '/' + filename)
 
371
        self.bzrdir._control_files.put_utf8('branch-format', BzrDirFormat6().get_format_string())
 
372
 
 
373
 
 
374
class Convert(object):
 
375
 
 
376
    def __init__(self, transport):
 
377
        self.base = transport.base
 
378
        self.transport = transport
 
379
        if self.transport.is_readonly():
 
380
            raise UpgradeReadonly
 
381
        #  self.control_files = LockableFiles(transport.clone(bzrlib.BZRDIR), 'branch-lock')
 
382
        # Lock the branch (soon to be meta dir) to prevent anyone racing with us
 
383
        # This is currently windows incompatible, it will deadlock. When the upgrade
 
384
        # logic becomes format specific, then we can have the format know how to pass this
 
385
        # on. Also note that we probably have an 'upgrade meta' which upgrades the constituent
 
386
        # parts.
 
387
        # FIXME: control files reuse
 
388
        # self.control_files.lock_write()
 
389
        #try:
 
390
        self.convert()
 
391
        #finally:
 
392
        #     self.control_files.unlock()
 
393
 
 
394
    def convert(self):
 
395
        self.old_format = BzrDirFormat.find_format(self.transport)
 
396
        self.bzrdir = self.old_format.open(self.transport)
 
397
        self.branch = self.bzrdir.open_branch()
 
398
        self.pb = ui_factory.progress_bar()
 
399
        if isinstance(self.old_format, BzrDirFormat6):
 
400
            self.pb.note('this branch is in the most current format (%s)', self.old_format)
 
401
            return
 
402
        if (not isinstance(self.old_format, BzrDirFormat4) and
 
403
            not isinstance(self.old_format, BzrDirFormat5) and
 
404
            not isinstance(self.old_format, bzrdir.BzrDirMetaFormat1)):
 
405
            raise errors.BzrError("cannot upgrade from branch format %s" %
 
406
                           self.bzrdir._format)
 
407
        # return self.bzrdir.upgrade(pb)
 
408
        self.pb.note('starting upgrade of %s', self.base)
 
409
        self._backup_control_dir()
 
410
        if isinstance(self.bzrdir._format, BzrDirFormat4):
 
411
            converter = ConvertBzrDir4To5(self.bzrdir, self.pb)
 
412
            self.bzrdir = converter.convert()
 
413
        if isinstance(self.bzrdir._format, BzrDirFormat5):
 
414
            converter = ConvertBzrDir5To6(self.bzrdir, self.pb)
 
415
            self.bzrdir = converter.convert()
 
416
        self.pb.note("finished")
 
417
 
 
418
    def _backup_control_dir(self):
 
419
        note('making backup of tree history')
 
420
        self.transport.copy_tree('.bzr', '.bzr.backup')
 
421
        note('%s.bzr has been backed up to %s.bzr.backup',
 
422
             self.transport.base,
 
423
             self.transport.base)
 
424
        note('if conversion fails, you can move this directory back to .bzr')
 
425
        note('if it succeeds, you can remove this directory if you wish')
 
426
 
 
427
def upgrade(url):
 
428
    t = get_transport(url)
 
429
    Convert(t)