~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/upgrade.py

  • Committer: John Arbash Meinel
  • Date: 2005-11-30 15:43:57 UTC
  • mto: (1185.50.1 jam-integration)
  • mto: This revision was merged to the branch mainline in revision 1518.
  • Revision ID: john@arbash-meinel.com-20051130154357-614206b3a7b83cd0
Refactored bzrlib/ui.py into a module with the possibility for multiple ui forms.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2008, 2009 Canonical Ltd
 
1
# Copyright (C) 2005 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
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
"""bzr upgrade logic."""
18
 
 
19
 
 
20
 
from bzrlib.bzrdir import BzrDir, BzrDirFormat
21
 
import bzrlib.errors as errors
22
 
from bzrlib.remote import RemoteBzrDir
23
 
from bzrlib.transport import get_transport
24
 
import bzrlib.ui as ui
 
17
"""Experiment in converting existing bzr branches to weaves."""
 
18
 
 
19
# To make this properly useful
 
20
#
 
21
# 1. assign text version ids, and put those text versions into
 
22
#    the inventory as they're converted.
 
23
#
 
24
# 2. keep track of the previous version of each file, rather than
 
25
#    just using the last one imported
 
26
#
 
27
# 3. assign entry versions when files are added, renamed or moved.
 
28
#
 
29
# 4. when merged-in versions are observed, walk down through them
 
30
#    to discover everything, then commit bottom-up
 
31
#
 
32
# 5. track ancestry as things are merged in, and commit that in each
 
33
#    revision
 
34
#
 
35
# Perhaps it's best to first walk the whole graph and make a plan for
 
36
# what should be imported in what order?  Need a kind of topological
 
37
# sort of all revisions.  (Or do we, can we just before doing a revision
 
38
# see that all its parents have either been converted or abandoned?)
 
39
 
 
40
 
 
41
# Cannot import a revision until all its parents have been
 
42
# imported.  in other words, we can only import revisions whose
 
43
# parents have all been imported.  the first step must be to
 
44
# import a revision with no parents, of which there must be at
 
45
# least one.  (So perhaps it's useful to store forward pointers
 
46
# from a list of parents to their children?)
 
47
#
 
48
# Another (equivalent?) approach is to build up the ordered
 
49
# ancestry list for the last revision, and walk through that.  We
 
50
# are going to need that.
 
51
#
 
52
# We don't want to have to recurse all the way back down the list.
 
53
#
 
54
# Suppose we keep a queue of the revisions able to be processed at
 
55
# any point.  This starts out with all the revisions having no
 
56
# parents.
 
57
#
 
58
# This seems like a generally useful algorithm...
 
59
#
 
60
# The current algorithm is dumb (O(n**2)?) but will do the job, and
 
61
# takes less than a second on the bzr.dev branch.
 
62
 
 
63
# This currently does a kind of lazy conversion of file texts, where a
 
64
# new text is written in every version.  That's unnecessary but for
 
65
# the moment saves us having to worry about when files need new
 
66
# versions.
 
67
 
 
68
 
 
69
import os
 
70
import tempfile
 
71
import sys
 
72
import shutil
 
73
 
 
74
from bzrlib.branch import Branch, find_branch
 
75
from bzrlib.branch import BZR_BRANCH_FORMAT_5, BZR_BRANCH_FORMAT_6
 
76
import bzrlib.hashcache as hashcache
 
77
from bzrlib.weave import Weave
 
78
from bzrlib.weavefile import read_weave, write_weave
 
79
from bzrlib.ui import ui_factory
 
80
from bzrlib.atomicfile import AtomicFile
 
81
from bzrlib.xml4 import serializer_v4
 
82
from bzrlib.xml5 import serializer_v5
 
83
from bzrlib.trace import mutter, note, warning
 
84
from bzrlib.osutils import sha_strings, sha_string
25
85
 
26
86
 
27
87
class Convert(object):
 
88
    def __init__(self, base_dir):
 
89
        self.base = base_dir
 
90
        self.converted_revs = set()
 
91
        self.absent_revisions = set()
 
92
        self.text_count = 0
 
93
        self.revisions = {}
 
94
        self.convert()
28
95
 
29
 
    def __init__(self, url, format):
30
 
        if format is None:
31
 
            format = BzrDirFormat.get_default_format()
32
 
        self.format = format
33
 
        self.bzrdir = BzrDir.open_unsupported(url)
34
 
        if isinstance(self.bzrdir, RemoteBzrDir):
35
 
            self.bzrdir._ensure_real()
36
 
            self.bzrdir = self.bzrdir._real_bzrdir
37
 
        if self.bzrdir.root_transport.is_readonly():
38
 
            raise errors.UpgradeReadonly
39
 
        self.transport = self.bzrdir.root_transport
40
 
        self.pb = ui.ui_factory.nested_progress_bar()
41
 
        try:
42
 
            self.convert()
43
 
        finally:
44
 
            self.pb.finished()
45
96
 
46
97
    def convert(self):
47
 
        try:
48
 
            branch = self.bzrdir.open_branch()
49
 
            if branch.bzrdir.root_transport.base != \
50
 
                self.bzrdir.root_transport.base:
51
 
                self.pb.note("This is a checkout. The branch (%s) needs to be "
52
 
                             "upgraded separately.",
53
 
                             branch.bzrdir.root_transport.base)
54
 
            del branch
55
 
        except (errors.NotBranchError, errors.IncompatibleRepositories):
56
 
            # might not be a format we can open without upgrading; see e.g. 
57
 
            # https://bugs.launchpad.net/bzr/+bug/253891
58
 
            pass
59
 
        if not self.bzrdir.needs_format_conversion(self.format):
60
 
            raise errors.UpToDateFormat(self.bzrdir._format)
61
 
        if not self.bzrdir.can_convert_format():
62
 
            raise errors.BzrError("cannot upgrade from bzrdir format %s" %
63
 
                           self.bzrdir._format)
64
 
        self.bzrdir.check_conversion_target(self.format)
65
 
        self.pb.note('starting upgrade of %s', self.transport.base)
66
 
        self.bzrdir.backup_bzrdir()
67
 
        while self.bzrdir.needs_format_conversion(self.format):
68
 
            converter = self.bzrdir._format.get_converter(self.format)
69
 
            self.bzrdir = converter.convert(self.bzrdir, self.pb)
70
 
        self.pb.note("finished")
71
 
 
72
 
 
73
 
def upgrade(url, format=None):
74
 
    """Upgrade to format, or the default bzrdir format if not supplied."""
75
 
    Convert(url, format)
 
98
        if not self._open_branch():
 
99
            return
 
100
        note('starting upgrade of %s', os.path.abspath(self.base))
 
101
        self._backup_control_dir()
 
102
        self.pb = ui_factory.progress_bar()
 
103
        if self.old_format == 4:
 
104
            note('starting upgrade from format 4 to 5')
 
105
            self._convert_to_weaves()
 
106
            self._open_branch()
 
107
        if self.old_format == 5:
 
108
            note('starting upgrade from format 5 to 6')
 
109
            self._convert_to_prefixed()
 
110
            self._open_branch()
 
111
        cache = hashcache.HashCache(os.path.abspath(self.base))
 
112
        cache.clear()
 
113
        cache.write()
 
114
        note("finished")
 
115
 
 
116
 
 
117
    def _convert_to_prefixed(self):
 
118
        from bzrlib.store import hash_prefix
 
119
        for store_name in ["weaves", "revision-store"]:
 
120
            note("adding prefixes to %s" % store_name) 
 
121
            store_dir = os.path.join(self.base, ".bzr", store_name)
 
122
            for filename in os.listdir(store_dir):
 
123
                if filename.endswith(".weave") or filename.endswith(".gz"):
 
124
                    file_id = os.path.splitext(filename)[0]
 
125
                else:
 
126
                    file_id = filename
 
127
                prefix_dir = os.path.join(store_dir, hash_prefix(file_id))
 
128
                if not os.path.isdir(prefix_dir):
 
129
                    os.mkdir(prefix_dir)
 
130
                os.rename(os.path.join(store_dir, filename),
 
131
                          os.path.join(prefix_dir, filename))
 
132
        self._set_new_format(BZR_BRANCH_FORMAT_6)
 
133
 
 
134
 
 
135
    def _convert_to_weaves(self):
 
136
        note('note: upgrade may be faster if all store files are ungzipped first')
 
137
        if not os.path.isdir(self.base + '/.bzr/weaves'):
 
138
            os.mkdir(self.base + '/.bzr/weaves')
 
139
        self.inv_weave = Weave('inventory')
 
140
        # holds in-memory weaves for all files
 
141
        self.text_weaves = {}
 
142
        os.remove(self.branch.controlfilename('branch-format'))
 
143
        self._convert_working_inv()
 
144
        rev_history = self.branch.revision_history()
 
145
        # to_read is a stack holding the revisions we still need to process;
 
146
        # appending to it adds new highest-priority revisions
 
147
        self.known_revisions = set(rev_history)
 
148
        self.to_read = rev_history[-1:]
 
149
        while self.to_read:
 
150
            rev_id = self.to_read.pop()
 
151
            if (rev_id not in self.revisions
 
152
                and rev_id not in self.absent_revisions):
 
153
                self._load_one_rev(rev_id)
 
154
        self.pb.clear()
 
155
        to_import = self._make_order()
 
156
        for i, rev_id in enumerate(to_import):
 
157
            self.pb.update('converting revision', i, len(to_import))
 
158
            self._convert_one_rev(rev_id)
 
159
        self.pb.clear()
 
160
        note('upgraded to weaves:')
 
161
        note('  %6d revisions and inventories' % len(self.revisions))
 
162
        note('  %6d revisions not present' % len(self.absent_revisions))
 
163
        note('  %6d texts' % self.text_count)
 
164
        self._write_all_weaves()
 
165
        self._write_all_revs()
 
166
        self._cleanup_spare_files()
 
167
        self._set_new_format(BZR_BRANCH_FORMAT_5)
 
168
 
 
169
 
 
170
    def _open_branch(self):
 
171
        self.branch = Branch.open_downlevel(self.base)
 
172
        self.old_format = self.branch._branch_format
 
173
        if self.old_format == 6:
 
174
            note('this branch is in the most current format')
 
175
            return False
 
176
        if self.old_format not in (4, 5):
 
177
            raise BzrError("cannot upgrade from branch format %r" %
 
178
                           self.branch._branch_format)
 
179
        return True
 
180
 
 
181
 
 
182
    def _set_new_format(self, format):
 
183
        self.branch.put_controlfile('branch-format', format)
 
184
 
 
185
 
 
186
    def _cleanup_spare_files(self):
 
187
        for n in 'merged-patches', 'pending-merged-patches':
 
188
            p = self.branch.controlfilename(n)
 
189
            if not os.path.exists(p):
 
190
                continue
 
191
            ## assert os.path.getsize(p) == 0
 
192
            os.remove(p)
 
193
        shutil.rmtree(self.base + '/.bzr/inventory-store')
 
194
        shutil.rmtree(self.base + '/.bzr/text-store')
 
195
 
 
196
 
 
197
    def _backup_control_dir(self):
 
198
        orig = self.base + '/.bzr'
 
199
        backup = orig + '.backup'
 
200
        note('making backup of tree history')
 
201
        shutil.copytree(orig, backup)
 
202
        note('%s has been backed up to %s', orig, backup)
 
203
        note('if conversion fails, you can move this directory back to .bzr')
 
204
        note('if it succeeds, you can remove this directory if you wish')
 
205
 
 
206
 
 
207
    def _convert_working_inv(self):
 
208
        branch = self.branch
 
209
        inv = serializer_v4.read_inventory(branch.controlfile('inventory', 'rb'))
 
210
        new_inv_xml = serializer_v5.write_inventory_to_string(inv)
 
211
        branch.put_controlfile('inventory', new_inv_xml)
 
212
 
 
213
 
 
214
 
 
215
    def _write_all_weaves(self):
 
216
        write_a_weave(self.inv_weave, self.base + '/.bzr/inventory.weave')
 
217
        i = 0
 
218
        try:
 
219
            for file_id, file_weave in self.text_weaves.items():
 
220
                self.pb.update('writing weave', i, len(self.text_weaves))
 
221
                write_a_weave(file_weave, self.base + '/.bzr/weaves/%s.weave' % file_id)
 
222
                i += 1
 
223
        finally:
 
224
            self.pb.clear()
 
225
 
 
226
 
 
227
    def _write_all_revs(self):
 
228
        """Write all revisions out in new form."""
 
229
        shutil.rmtree(self.base + '/.bzr/revision-store')
 
230
        os.mkdir(self.base + '/.bzr/revision-store')
 
231
        try:
 
232
            for i, rev_id in enumerate(self.converted_revs):
 
233
                self.pb.update('write revision', i, len(self.converted_revs))
 
234
                f = file(self.base + '/.bzr/revision-store/%s' % rev_id, 'wb')
 
235
                try:
 
236
                    serializer_v5.write_revision(self.revisions[rev_id], f)
 
237
                finally:
 
238
                    f.close()
 
239
        finally:
 
240
            self.pb.clear()
 
241
 
 
242
            
 
243
    def _load_one_rev(self, rev_id):
 
244
        """Load a revision object into memory.
 
245
 
 
246
        Any parents not either loaded or abandoned get queued to be
 
247
        loaded."""
 
248
        self.pb.update('loading revision',
 
249
                       len(self.revisions),
 
250
                       len(self.known_revisions))
 
251
        if not self.branch.revision_store.has_id(rev_id):
 
252
            self.pb.clear()
 
253
            note('revision {%s} not present in branch; '
 
254
                 'will be converted as a ghost',
 
255
                 rev_id)
 
256
            self.absent_revisions.add(rev_id)
 
257
        else:
 
258
            rev_xml = self.branch.revision_store.get(rev_id).read()
 
259
            rev = serializer_v4.read_revision_from_string(rev_xml)
 
260
            for parent_id in rev.parent_ids:
 
261
                self.known_revisions.add(parent_id)
 
262
                self.to_read.append(parent_id)
 
263
            self.revisions[rev_id] = rev
 
264
 
 
265
 
 
266
    def _load_old_inventory(self, rev_id):
 
267
        assert rev_id not in self.converted_revs
 
268
        old_inv_xml = self.branch.inventory_store.get(rev_id).read()
 
269
        inv = serializer_v4.read_inventory_from_string(old_inv_xml)
 
270
        rev = self.revisions[rev_id]
 
271
        if rev.inventory_sha1:
 
272
            assert rev.inventory_sha1 == sha_string(old_inv_xml), \
 
273
                'inventory sha mismatch for {%s}' % rev_id
 
274
        return inv
 
275
        
 
276
 
 
277
    def _load_updated_inventory(self, rev_id):
 
278
        assert rev_id in self.converted_revs
 
279
        inv_xml = self.inv_weave.get_text(rev_id)
 
280
        inv = serializer_v5.read_inventory_from_string(inv_xml)
 
281
        return inv
 
282
 
 
283
 
 
284
    def _convert_one_rev(self, rev_id):
 
285
        """Convert revision and all referenced objects to new format."""
 
286
        rev = self.revisions[rev_id]
 
287
        inv = self._load_old_inventory(rev_id)
 
288
        present_parents = [p for p in rev.parent_ids
 
289
                           if p not in self.absent_revisions]
 
290
        self._convert_revision_contents(rev, inv, present_parents)
 
291
        self._store_new_weave(rev, inv, present_parents)
 
292
        self.converted_revs.add(rev_id)
 
293
 
 
294
 
 
295
    def _store_new_weave(self, rev, inv, present_parents):
 
296
        # the XML is now updated with text versions
 
297
        if __debug__:
 
298
            for file_id in inv:
 
299
                ie = inv[file_id]
 
300
                if ie.kind == 'root_directory':
 
301
                    continue
 
302
                assert hasattr(ie, 'revision'), \
 
303
                    'no revision on {%s} in {%s}' % \
 
304
                    (file_id, rev.revision_id)
 
305
        new_inv_xml = serializer_v5.write_inventory_to_string(inv)
 
306
        new_inv_sha1 = sha_string(new_inv_xml)
 
307
        self.inv_weave.add(rev.revision_id, 
 
308
                           present_parents,
 
309
                           new_inv_xml.splitlines(True),
 
310
                           new_inv_sha1)
 
311
        rev.inventory_sha1 = new_inv_sha1
 
312
 
 
313
    def _convert_revision_contents(self, rev, inv, present_parents):
 
314
        """Convert all the files within a revision.
 
315
 
 
316
        Also upgrade the inventory to refer to the text revision ids."""
 
317
        rev_id = rev.revision_id
 
318
        mutter('converting texts of revision {%s}',
 
319
               rev_id)
 
320
        parent_invs = map(self._load_updated_inventory, present_parents)
 
321
        for file_id in inv:
 
322
            ie = inv[file_id]
 
323
            self._convert_file_version(rev, ie, parent_invs)
 
324
 
 
325
    def _convert_file_version(self, rev, ie, parent_invs):
 
326
        """Convert one version of one file.
 
327
 
 
328
        The file needs to be added into the weave if it is a merge
 
329
        of >=2 parents or if it's changed from its parent.
 
330
        """
 
331
        if ie.kind == 'root_directory':
 
332
            return
 
333
        file_id = ie.file_id
 
334
        rev_id = rev.revision_id
 
335
        w = self.text_weaves.get(file_id)
 
336
        if w is None:
 
337
            w = Weave(file_id)
 
338
            self.text_weaves[file_id] = w
 
339
        text_changed = False
 
340
        previous_entries = ie.find_previous_heads(parent_invs, w)
 
341
        for old_revision in previous_entries:
 
342
                # if this fails, its a ghost ?
 
343
                assert old_revision in self.converted_revs 
 
344
        self.snapshot_ie(previous_entries, ie, w, rev_id)
 
345
        del ie.text_id
 
346
        assert getattr(ie, 'revision', None) is not None
 
347
 
 
348
    def snapshot_ie(self, previous_revisions, ie, w, rev_id):
 
349
        # TODO: convert this logic, which is ~= snapshot to
 
350
        # a call to:. This needs the path figured out. rather than a work_tree
 
351
        # a v4 revision_tree can be given, or something that looks enough like
 
352
        # one to give the file content to the entry if it needs it.
 
353
        # and we need something that looks like a weave store for snapshot to 
 
354
        # save against.
 
355
        #ie.snapshot(rev, PATH, previous_revisions, REVISION_TREE, InMemoryWeaveStore(self.text_weaves))
 
356
        if len(previous_revisions) == 1:
 
357
            previous_ie = previous_revisions.values()[0]
 
358
            if ie._unchanged(previous_ie):
 
359
                ie.revision = previous_ie.revision
 
360
                return
 
361
        parent_indexes = map(w.lookup, previous_revisions)
 
362
        if ie.has_text():
 
363
            file_lines = self.branch.text_store.get(ie.text_id).readlines()
 
364
            assert sha_strings(file_lines) == ie.text_sha1
 
365
            assert sum(map(len, file_lines)) == ie.text_size
 
366
            w.add(rev_id, parent_indexes, file_lines, ie.text_sha1)
 
367
            self.text_count += 1
 
368
        else:
 
369
            w.add(rev_id, parent_indexes, [], None)
 
370
        ie.revision = rev_id
 
371
        ##mutter('import text {%s} of {%s}',
 
372
        ##       ie.text_id, file_id)
 
373
 
 
374
    def _make_order(self):
 
375
        """Return a suitable order for importing revisions.
 
376
 
 
377
        The order must be such that an revision is imported after all
 
378
        its (present) parents.
 
379
        """
 
380
        todo = set(self.revisions.keys())
 
381
        done = self.absent_revisions.copy()
 
382
        o = []
 
383
        while todo:
 
384
            # scan through looking for a revision whose parents
 
385
            # are all done
 
386
            for rev_id in sorted(list(todo)):
 
387
                rev = self.revisions[rev_id]
 
388
                parent_ids = set(rev.parent_ids)
 
389
                if parent_ids.issubset(done):
 
390
                    # can take this one now
 
391
                    o.append(rev_id)
 
392
                    todo.remove(rev_id)
 
393
                    done.add(rev_id)
 
394
        return o
 
395
 
 
396
 
 
397
def write_a_weave(weave, filename):
 
398
    inv_wf = file(filename, 'wb')
 
399
    try:
 
400
        write_weave(weave, inv_wf)
 
401
    finally:
 
402
        inv_wf.close()
 
403
 
 
404
 
 
405
def upgrade(base_dir):
 
406
    Convert(base_dir)