1
# Copyright (C) 2005 Canonical Ltd
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.
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.
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
17
"""bzr upgrade logic."""
19
# change upgrade from .bzr to create a '.bzr-new', then do a bait and switch.
22
from cStringIO import StringIO
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
49
class Converter(object):
50
"""Converts a disk format object from one format to another."""
52
def __init__(self, pb):
53
"""Create a converter.
55
:param pb: a progress bar to use for progress information.
60
class ConvertBzrDir4To5(Converter):
61
"""Converts format 4 bzr dirs to format 5."""
63
def __init__(self, to_convert, pb):
64
"""Create a converter.
66
:param to_convert: The disk object to convert.
67
:param pb: a progress bar to use for progress information.
69
super(ConvertBzrDir4To5, self).__init__(pb)
70
self.bzrdir = to_convert
71
self.converted_revs = set()
72
self.absent_revisions = set()
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)
84
def _convert_to_weaves(self):
85
self.pb.note('note: upgrade may be faster if all store files are ungzipped first')
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')
93
self.bzrdir.transport.mkdir('weaves')
94
self.inv_weave = Weave('inventory')
95
# holds in-memory weaves for all files
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:]
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)
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)
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())
125
def _cleanup_spare_files_after_format4(self):
126
# FIXME working tree upgrade foo.
127
for n in 'merged-patches', 'pending-merged-patches':
129
## assert os.path.getsize(p) == 0
130
self.bzrdir.transport.delete(n)
133
self.bzrdir.transport.delete_tree('inventory-store')
134
self.bzrdir.transport.delete_tree('text-store')
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)
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()
148
controlweaves.put_weave('inventory', self.inv_weave, transaction)
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)
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')
164
revision_store = TextStore(revision_transport,
168
for i, rev_id in enumerate(self.converted_revs):
169
self.pb.update('write revision', i, len(self.converted_revs))
171
serializer_v5.write_revision(self.revisions[rev_id], rev_tmp)
173
revision_store.add(rev_tmp, rev_id)
178
def _load_one_rev(self, rev_id):
179
"""Load a revision object into memory.
181
Any parents not either loaded or abandoned get queued to be
183
self.pb.update('loading revision',
185
len(self.known_revisions))
186
if not self.branch.repository.revision_store.has_id(rev_id):
188
note('revision {%s} not present in branch; '
189
'will be converted as a ghost',
191
self.absent_revisions.add(rev_id)
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
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
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)
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)
230
def _store_new_weave(self, rev, inv, present_parents):
231
# the XML is now updated with text versions
235
if ie.kind == 'root_directory':
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,
244
new_inv_xml.splitlines(True),
246
rev.inventory_sha1 = new_inv_sha1
248
def _convert_revision_contents(self, rev, inv, present_parents):
249
"""Convert all the files within a revision.
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}',
255
parent_invs = map(self._load_updated_inventory, present_parents)
258
self._convert_file_version(rev, ie, parent_invs)
260
def _convert_file_version(self, rev, ie, parent_invs):
261
"""Convert one version of one file.
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.
266
if ie.kind == 'root_directory':
269
rev_id = rev.revision_id
270
w = self.text_weaves.get(file_id)
273
self.text_weaves[file_id] = w
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)
281
assert getattr(ie, 'revision', None) is not None
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
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
296
parent_indexes = map(w.lookup, previous_revisions)
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)
305
w.add(rev_id, parent_indexes, [], None)
307
##mutter('import text {%s} of {%s}',
308
## ie.text_id, file_id)
310
def _make_order(self):
311
"""Return a suitable order for importing revisions.
313
The order must be such that an revision is imported after all
314
its (present) parents.
316
todo = set(self.revisions.keys())
317
done = self.absent_revisions.copy()
320
# scan through looking for a revision whose parents
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
333
class ConvertBzrDir5To6(Converter):
334
"""Converts format 5 bzr dirs to format 6."""
336
def __init__(self, to_convert, pb):
337
"""Create a converter.
339
:param to_convert: The disk object to convert.
340
:param pb: a progress bar to use for progress information.
342
super(ConvertBzrDir5To6, self).__init__(pb)
343
self.bzrdir = to_convert
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)
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]
364
prefix_dir = hash_prefix(file_id)
365
# FIXME keep track of the dirs made RBC 20060121
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())
374
class Convert(object):
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
387
# FIXME: control files reuse
388
# self.control_files.lock_write()
392
# self.control_files.unlock()
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)
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" %
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")
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',
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')
428
t = get_transport(url)