/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

 * The internal storage of history, and logical branch identity have now
   been split into Branch, and Repository. The common locking and file 
   management routines are now in bzrlib.lockablefiles. 
   (Aaron Bentley, Robert Collins, Martin Pool)

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
"""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
 
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, pathjoin, abspath
 
85
 
 
86
 
 
87
# TODO: jam 20060108 Create a new branch format, and as part of upgrade
 
88
#       make sure that ancestry.weave is deleted (it is never used, but
 
89
#       used to be created)
 
90
 
 
91
 
 
92
class Convert(object):
 
93
    def __init__(self, base_dir):
 
94
        self.base = base_dir
 
95
        self.converted_revs = set()
 
96
        self.absent_revisions = set()
 
97
        self.text_count = 0
 
98
        self.revisions = {}
 
99
        self.convert()
 
100
 
 
101
 
 
102
    def convert(self):
 
103
        if not self._open_branch():
 
104
            return
 
105
        note('starting upgrade of %s', os.path.abspath(self.base))
 
106
        self._backup_control_dir()
 
107
        self.pb = ui_factory.progress_bar()
 
108
        if self.old_format == 4:
 
109
            note('starting upgrade from format 4 to 5')
 
110
            self.branch.lock_write()
 
111
            try:
 
112
                self._convert_to_weaves()
 
113
            finally:
 
114
                self.branch.unlock()
 
115
            self._open_branch()
 
116
        if self.old_format == 5:
 
117
            note('starting upgrade from format 5 to 6')
 
118
            self.branch.lock_write()
 
119
            try:
 
120
                self._convert_to_prefixed()
 
121
            finally:
 
122
                self.branch.unlock()
 
123
            self._open_branch()
 
124
        cache = hashcache.HashCache(abspath(self.base))
 
125
        cache.clear()
 
126
        cache.write()
 
127
        note("finished")
 
128
 
 
129
 
 
130
    def _convert_to_prefixed(self):
 
131
        from bzrlib.store import hash_prefix
 
132
        for store_name in ["weaves", "revision-store"]:
 
133
            note("adding prefixes to %s" % store_name) 
 
134
            store_dir = pathjoin(self.base, ".bzr", store_name)
 
135
            for filename in os.listdir(store_dir):
 
136
                if filename.endswith(".weave") or filename.endswith(".gz"):
 
137
                    file_id = os.path.splitext(filename)[0]
 
138
                else:
 
139
                    file_id = filename
 
140
                prefix_dir = pathjoin(store_dir, hash_prefix(file_id))
 
141
                if not os.path.isdir(prefix_dir):
 
142
                    os.mkdir(prefix_dir)
 
143
                os.rename(pathjoin(store_dir, filename),
 
144
                          pathjoin(prefix_dir, filename))
 
145
        self._set_new_format(BZR_BRANCH_FORMAT_6)
 
146
 
 
147
 
 
148
    def _convert_to_weaves(self):
 
149
        note('note: upgrade may be faster if all store files are ungzipped first')
 
150
        if not os.path.isdir(self.base + '/.bzr/weaves'):
 
151
            os.mkdir(self.base + '/.bzr/weaves')
 
152
        self.inv_weave = Weave('inventory')
 
153
        # holds in-memory weaves for all files
 
154
        self.text_weaves = {}
 
155
        os.remove(self.branch.control_files.controlfilename('branch-format'))
 
156
        self._convert_working_inv()
 
157
        rev_history = self.branch.revision_history()
 
158
        # to_read is a stack holding the revisions we still need to process;
 
159
        # appending to it adds new highest-priority revisions
 
160
        self.known_revisions = set(rev_history)
 
161
        self.to_read = rev_history[-1:]
 
162
        while self.to_read:
 
163
            rev_id = self.to_read.pop()
 
164
            if (rev_id not in self.revisions
 
165
                and rev_id not in self.absent_revisions):
 
166
                self._load_one_rev(rev_id)
 
167
        self.pb.clear()
 
168
        to_import = self._make_order()
 
169
        for i, rev_id in enumerate(to_import):
 
170
            self.pb.update('converting revision', i, len(to_import))
 
171
            self._convert_one_rev(rev_id)
 
172
        self.pb.clear()
 
173
        note('upgraded to weaves:')
 
174
        note('  %6d revisions and inventories' % len(self.revisions))
 
175
        note('  %6d revisions not present' % len(self.absent_revisions))
 
176
        note('  %6d texts' % self.text_count)
 
177
        self._write_all_weaves()
 
178
        self._write_all_revs()
 
179
        self._cleanup_spare_files()
 
180
        self._set_new_format(BZR_BRANCH_FORMAT_5)
 
181
 
 
182
 
 
183
    def _open_branch(self):
 
184
        self.branch = Branch.open_downlevel(self.base)
 
185
        self.old_format = self.branch._branch_format
 
186
        if self.old_format == 6:
 
187
            note('this branch is in the most current format')
 
188
            return False
 
189
        if self.old_format not in (4, 5):
 
190
            raise BzrError("cannot upgrade from branch format %r" %
 
191
                           self.branch._branch_format)
 
192
        return True
 
193
 
 
194
    def _set_new_format(self, format):
 
195
        self.branch.control_files.put_utf8('branch-format', format)
 
196
 
 
197
    def _cleanup_spare_files(self):
 
198
        for n in 'merged-patches', 'pending-merged-patches':
 
199
            p = self.branch.control_files.controlfilename(n)
 
200
            if not os.path.exists(p):
 
201
                continue
 
202
            ## assert os.path.getsize(p) == 0
 
203
            os.remove(p)
 
204
        shutil.rmtree(self.base + '/.bzr/inventory-store')
 
205
        shutil.rmtree(self.base + '/.bzr/text-store')
 
206
 
 
207
    def _backup_control_dir(self):
 
208
        orig = self.base + '/.bzr'
 
209
        backup = orig + '.backup'
 
210
        note('making backup of tree history')
 
211
        shutil.copytree(orig, backup)
 
212
        note('%s has been backed up to %s', orig, backup)
 
213
        note('if conversion fails, you can move this directory back to .bzr')
 
214
        note('if it succeeds, you can remove this directory if you wish')
 
215
 
 
216
    def _convert_working_inv(self):
 
217
        branch = self.branch
 
218
        inv = serializer_v4.read_inventory(branch.control_files.get('inventory'))
 
219
        new_inv_xml = serializer_v5.write_inventory_to_string(inv)
 
220
        branch.control_files.put('inventory', new_inv_xml)
 
221
 
 
222
    def _write_all_weaves(self):
 
223
        write_a_weave(self.inv_weave, self.base + '/.bzr/inventory.weave')
 
224
        i = 0
 
225
        try:
 
226
            for file_id, file_weave in self.text_weaves.items():
 
227
                self.pb.update('writing weave', i, len(self.text_weaves))
 
228
                write_a_weave(file_weave, self.base + '/.bzr/weaves/%s.weave' % file_id)
 
229
                i += 1
 
230
        finally:
 
231
            self.pb.clear()
 
232
 
 
233
 
 
234
    def _write_all_revs(self):
 
235
        """Write all revisions out in new form."""
 
236
        shutil.rmtree(self.base + '/.bzr/revision-store')
 
237
        os.mkdir(self.base + '/.bzr/revision-store')
 
238
        try:
 
239
            for i, rev_id in enumerate(self.converted_revs):
 
240
                self.pb.update('write revision', i, len(self.converted_revs))
 
241
                f = file(self.base + '/.bzr/revision-store/%s' % rev_id, 'wb')
 
242
                try:
 
243
                    serializer_v5.write_revision(self.revisions[rev_id], f)
 
244
                finally:
 
245
                    f.close()
 
246
        finally:
 
247
            self.pb.clear()
 
248
 
 
249
            
 
250
    def _load_one_rev(self, rev_id):
 
251
        """Load a revision object into memory.
 
252
 
 
253
        Any parents not either loaded or abandoned get queued to be
 
254
        loaded."""
 
255
        self.pb.update('loading revision',
 
256
                       len(self.revisions),
 
257
                       len(self.known_revisions))
 
258
        if not self.branch.repository.revision_store.has_id(rev_id):
 
259
            self.pb.clear()
 
260
            note('revision {%s} not present in branch; '
 
261
                 'will be converted as a ghost',
 
262
                 rev_id)
 
263
            self.absent_revisions.add(rev_id)
 
264
        else:
 
265
            rev_xml = self.branch.repository.revision_store.get(rev_id).read()
 
266
            rev = serializer_v4.read_revision_from_string(rev_xml)
 
267
            for parent_id in rev.parent_ids:
 
268
                self.known_revisions.add(parent_id)
 
269
                self.to_read.append(parent_id)
 
270
            self.revisions[rev_id] = rev
 
271
 
 
272
 
 
273
    def _load_old_inventory(self, rev_id):
 
274
        assert rev_id not in self.converted_revs
 
275
        old_inv_xml = self.branch.repository.inventory_store.get(rev_id).read()
 
276
        inv = serializer_v4.read_inventory_from_string(old_inv_xml)
 
277
        rev = self.revisions[rev_id]
 
278
        if rev.inventory_sha1:
 
279
            assert rev.inventory_sha1 == sha_string(old_inv_xml), \
 
280
                'inventory sha mismatch for {%s}' % rev_id
 
281
        return inv
 
282
        
 
283
 
 
284
    def _load_updated_inventory(self, rev_id):
 
285
        assert rev_id in self.converted_revs
 
286
        inv_xml = self.inv_weave.get_text(rev_id)
 
287
        inv = serializer_v5.read_inventory_from_string(inv_xml)
 
288
        return inv
 
289
 
 
290
 
 
291
    def _convert_one_rev(self, rev_id):
 
292
        """Convert revision and all referenced objects to new format."""
 
293
        rev = self.revisions[rev_id]
 
294
        inv = self._load_old_inventory(rev_id)
 
295
        present_parents = [p for p in rev.parent_ids
 
296
                           if p not in self.absent_revisions]
 
297
        self._convert_revision_contents(rev, inv, present_parents)
 
298
        self._store_new_weave(rev, inv, present_parents)
 
299
        self.converted_revs.add(rev_id)
 
300
 
 
301
 
 
302
    def _store_new_weave(self, rev, inv, present_parents):
 
303
        # the XML is now updated with text versions
 
304
        if __debug__:
 
305
            for file_id in inv:
 
306
                ie = inv[file_id]
 
307
                if ie.kind == 'root_directory':
 
308
                    continue
 
309
                assert hasattr(ie, 'revision'), \
 
310
                    'no revision on {%s} in {%s}' % \
 
311
                    (file_id, rev.revision_id)
 
312
        new_inv_xml = serializer_v5.write_inventory_to_string(inv)
 
313
        new_inv_sha1 = sha_string(new_inv_xml)
 
314
        self.inv_weave.add(rev.revision_id, 
 
315
                           present_parents,
 
316
                           new_inv_xml.splitlines(True),
 
317
                           new_inv_sha1)
 
318
        rev.inventory_sha1 = new_inv_sha1
 
319
 
 
320
    def _convert_revision_contents(self, rev, inv, present_parents):
 
321
        """Convert all the files within a revision.
 
322
 
 
323
        Also upgrade the inventory to refer to the text revision ids."""
 
324
        rev_id = rev.revision_id
 
325
        mutter('converting texts of revision {%s}',
 
326
               rev_id)
 
327
        parent_invs = map(self._load_updated_inventory, present_parents)
 
328
        for file_id in inv:
 
329
            ie = inv[file_id]
 
330
            self._convert_file_version(rev, ie, parent_invs)
 
331
 
 
332
    def _convert_file_version(self, rev, ie, parent_invs):
 
333
        """Convert one version of one file.
 
334
 
 
335
        The file needs to be added into the weave if it is a merge
 
336
        of >=2 parents or if it's changed from its parent.
 
337
        """
 
338
        if ie.kind == 'root_directory':
 
339
            return
 
340
        file_id = ie.file_id
 
341
        rev_id = rev.revision_id
 
342
        w = self.text_weaves.get(file_id)
 
343
        if w is None:
 
344
            w = Weave(file_id)
 
345
            self.text_weaves[file_id] = w
 
346
        text_changed = False
 
347
        previous_entries = ie.find_previous_heads(parent_invs, w)
 
348
        for old_revision in previous_entries:
 
349
                # if this fails, its a ghost ?
 
350
                assert old_revision in self.converted_revs 
 
351
        self.snapshot_ie(previous_entries, ie, w, rev_id)
 
352
        del ie.text_id
 
353
        assert getattr(ie, 'revision', None) is not None
 
354
 
 
355
    def snapshot_ie(self, previous_revisions, ie, w, rev_id):
 
356
        # TODO: convert this logic, which is ~= snapshot to
 
357
        # a call to:. This needs the path figured out. rather than a work_tree
 
358
        # a v4 revision_tree can be given, or something that looks enough like
 
359
        # one to give the file content to the entry if it needs it.
 
360
        # and we need something that looks like a weave store for snapshot to 
 
361
        # save against.
 
362
        #ie.snapshot(rev, PATH, previous_revisions, REVISION_TREE, InMemoryWeaveStore(self.text_weaves))
 
363
        if len(previous_revisions) == 1:
 
364
            previous_ie = previous_revisions.values()[0]
 
365
            if ie._unchanged(previous_ie):
 
366
                ie.revision = previous_ie.revision
 
367
                return
 
368
        parent_indexes = map(w.lookup, previous_revisions)
 
369
        if ie.has_text():
 
370
            text = self.branch.repository.text_store.get(ie.text_id)
 
371
            file_lines = text.readlines()
 
372
            assert sha_strings(file_lines) == ie.text_sha1
 
373
            assert sum(map(len, file_lines)) == ie.text_size
 
374
            w.add(rev_id, parent_indexes, file_lines, ie.text_sha1)
 
375
            self.text_count += 1
 
376
        else:
 
377
            w.add(rev_id, parent_indexes, [], None)
 
378
        ie.revision = rev_id
 
379
        ##mutter('import text {%s} of {%s}',
 
380
        ##       ie.text_id, file_id)
 
381
 
 
382
    def _make_order(self):
 
383
        """Return a suitable order for importing revisions.
 
384
 
 
385
        The order must be such that an revision is imported after all
 
386
        its (present) parents.
 
387
        """
 
388
        todo = set(self.revisions.keys())
 
389
        done = self.absent_revisions.copy()
 
390
        o = []
 
391
        while todo:
 
392
            # scan through looking for a revision whose parents
 
393
            # are all done
 
394
            for rev_id in sorted(list(todo)):
 
395
                rev = self.revisions[rev_id]
 
396
                parent_ids = set(rev.parent_ids)
 
397
                if parent_ids.issubset(done):
 
398
                    # can take this one now
 
399
                    o.append(rev_id)
 
400
                    todo.remove(rev_id)
 
401
                    done.add(rev_id)
 
402
        return o
 
403
 
 
404
 
 
405
def write_a_weave(weave, filename):
 
406
    inv_wf = file(filename, 'wb')
 
407
    try:
 
408
        write_weave(weave, inv_wf)
 
409
    finally:
 
410
        inv_wf.close()
 
411
 
 
412
 
 
413
def upgrade(base_dir):
 
414
    Convert(base_dir)