3
Read in a bundle stream, and process it into a BundleReader object.
1
# Copyright (C) 2006 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
"""Read in a bundle stream, and process it into a BundleReader object."""
7
20
from cStringIO import StringIO
29
from bzrlib.bundle import apply_bundle
11
30
from bzrlib.errors import (TestamentMismatch, BzrError,
12
31
MalformedHeader, MalformedPatches, NotABundle)
13
from bzrlib.bundle.common import get_header, header_str
14
32
from bzrlib.inventory import (Inventory, InventoryEntry,
15
33
InventoryDirectory, InventoryFile,
17
from bzrlib.osutils import sha_file, sha_string
35
from bzrlib.osutils import sha_file, sha_string, pathjoin
18
36
from bzrlib.revision import Revision, NULL_REVISION
19
37
from bzrlib.testament import StrictTestament
20
38
from bzrlib.trace import mutter, warning
39
import bzrlib.transport
21
40
from bzrlib.tree import Tree
41
import bzrlib.urlutils
22
42
from bzrlib.xml5 import serializer_v5
153
192
raise KeyError(revision_id)
156
class BundleReader(object):
157
"""This class reads in a bundle from a file, and returns
158
a Bundle object, which can then be applied against a tree.
160
def __init__(self, from_file):
161
"""Read in the bundle from the file.
163
:param from_file: A file-like object (must have iterator support).
165
object.__init__(self)
166
self.from_file = iter(from_file)
167
self._next_line = None
169
self.info = BundleInfo()
170
# We put the actual inventory ids in the footer, so that the patch
171
# is easier to read for humans.
172
# Unfortunately, that means we need to read everything before we
173
# can create a proper bundle.
179
while self._next_line is not None:
180
self._read_revision_header()
181
if self._next_line is None:
187
"""Make sure that the information read in makes sense
188
and passes appropriate checksums.
190
# Fill in all the missing blanks for the revisions
191
# and generate the real_revisions list.
192
self.info.complete_info()
194
def _validate_revision(self, inventory, revision_id):
195
"""Make sure all revision entries match their checksum."""
197
# This is a mapping from each revision id to it's sha hash
200
rev = self.info.get_revision(revision_id)
201
rev_info = self.info.get_revision_info(revision_id)
202
assert rev.revision_id == rev_info.revision_id
203
assert rev.revision_id == revision_id
204
sha1 = StrictTestament(rev, inventory).as_sha1()
205
if sha1 != rev_info.sha1:
206
raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
207
if rev_to_sha1.has_key(rev.revision_id):
208
raise BzrError('Revision {%s} given twice in the list'
210
rev_to_sha1[rev.revision_id] = sha1
194
def revision_tree(self, repository, revision_id, base=None):
195
revision = self.get_revision(revision_id)
196
base = self.get_base(revision)
197
if base == revision_id:
198
raise AssertionError()
199
if not self._validated_revisions_against_repo:
200
self._validate_references_from_repository(repository)
201
revision_info = self.get_revision_info(revision_id)
202
inventory_revision_id = revision_id
203
bundle_tree = BundleTree(repository.revision_tree(base),
204
inventory_revision_id)
205
self._update_tree(bundle_tree, revision_id)
207
inv = bundle_tree.inventory
208
self._validate_inventory(inv, revision_id)
209
self._validate_revision(inv, revision_id)
212
213
def _validate_references_from_repository(self, repository):
213
214
"""Now that we have a repository which should have some of the
257
259
elif revision_id not in checked:
258
260
missing[revision_id] = sha1
260
for inv_id, sha1 in inv_to_sha.iteritems():
261
if repository.has_revision(inv_id):
262
# Note: branch.get_inventory_sha1() just returns the value that
263
# is stored in the revision text, and that value may be out
264
# of date. This is bogus, because that means we aren't
265
# validating the actual text, just that we wrote and read the
266
# string. But for now, what the hell.
267
local_sha1 = repository.get_inventory_sha1(inv_id)
268
if sha1 != local_sha1:
269
raise BzrError('sha1 mismatch. For inventory id {%s}'
270
'local: %s, bundle: %s' %
271
(inv_id, local_sha1, sha1))
275
262
if len(missing) > 0:
276
263
# I don't know if this is an error yet
277
264
warning('Not all revision hashes could be validated.'
278
265
' Unable validate %d hashes' % len(missing))
279
266
mutter('Verified %d sha hashes for the bundle.' % count)
267
self._validated_revisions_against_repo = True
281
269
def _validate_inventory(self, inv, revision_id):
282
270
"""At this point we should have generated the BundleTree,
283
271
so build up an inventory, and make sure the hashes match.
286
assert inv is not None
288
273
# Now we should have a complete inventory entry.
289
274
s = serializer_v5.write_inventory_to_string(inv)
290
275
sha1 = sha_string(s)
291
276
# Target revision is the last entry in the real_revisions list
292
rev = self.info.get_revision(revision_id)
293
assert rev.revision_id == revision_id
277
rev = self.get_revision(revision_id)
278
if rev.revision_id != revision_id:
279
raise AssertionError()
294
280
if sha1 != rev.inventory_sha1:
295
281
open(',,bogus-inv', 'wb').write(s)
296
282
warning('Inventory sha hash mismatch for revision %s. %s'
297
283
' != %s' % (revision_id, sha1, rev.inventory_sha1))
299
def get_bundle(self, repository):
300
"""Return the meta information, and a Bundle tree which can
301
be used to populate the local stores and working tree, respectively.
303
return self.info, self.revision_tree(repository, self.info.target)
305
def revision_tree(self, repository, revision_id, base=None):
306
revision = self.info.get_revision(revision_id)
307
base = self.info.get_base(revision)
308
assert base != revision_id
309
self._validate_references_from_repository(repository)
310
revision_info = self.info.get_revision_info(revision_id)
311
inventory_revision_id = revision_id
312
bundle_tree = BundleTree(repository.revision_tree(base),
313
inventory_revision_id)
314
self._update_tree(bundle_tree, revision_id)
316
inv = bundle_tree.inventory
317
self._validate_inventory(inv, revision_id)
318
self._validate_revision(inv, revision_id)
323
"""yield the next line, but secretly
324
keep 1 extra line for peeking.
326
for line in self.from_file:
327
last = self._next_line
328
self._next_line = line
330
#mutter('yielding line: %r' % last)
332
last = self._next_line
333
self._next_line = None
334
#mutter('yielding line: %r' % last)
337
def _read_header(self):
338
"""Read the bzr header"""
339
header = get_header()
341
for line in self._next():
343
# not all mailers will keep trailing whitespace
346
if (not line.startswith('# ') or not line.endswith('\n')
347
or line[2:-1].decode('utf-8') != header[0]):
348
raise MalformedHeader('Found a header, but it'
349
' was improperly formatted')
350
header.pop(0) # We read this line.
352
break # We found everything.
353
elif (line.startswith('#') and line.endswith('\n')):
354
line = line[1:-1].strip().decode('utf-8')
355
if line[:len(header_str)] == header_str:
356
if line == header[0]:
359
raise MalformedHeader('Found what looks like'
360
' a header, but did not match')
363
raise NotABundle('Did not find an opening header')
365
def _read_revision_header(self):
366
self.info.revisions.append(RevisionInfo(None))
367
for line in self._next():
368
# The bzr header is terminated with a blank line
369
# which does not start with '#'
370
if line is None or line == '\n':
372
self._handle_next(line)
374
def _read_next_entry(self, line, indent=1):
375
"""Read in a key-value pair
377
if not line.startswith('#'):
378
raise MalformedHeader('Bzr header did not start with #')
379
line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
380
if line[:indent] == ' '*indent:
383
return None, None# Ignore blank lines
385
loc = line.find(': ')
390
value = self._read_many(indent=indent+2)
391
elif line[-1:] == ':':
393
value = self._read_many(indent=indent+2)
395
raise MalformedHeader('While looking for key: value pairs,'
396
' did not find the colon %r' % (line))
398
key = key.replace(' ', '_')
399
#mutter('found %s: %s' % (key, value))
402
def _handle_next(self, line):
405
key, value = self._read_next_entry(line, indent=1)
406
mutter('_handle_next %r => %r' % (key, value))
410
revision_info = self.info.revisions[-1]
411
if hasattr(revision_info, key):
412
if getattr(revision_info, key) is None:
413
setattr(revision_info, key, value)
415
raise MalformedHeader('Duplicated Key: %s' % key)
417
# What do we do with a key we don't recognize
418
raise MalformedHeader('Unknown Key: "%s"' % key)
420
def _read_many(self, indent):
421
"""If a line ends with no entry, that means that it should be
422
followed with multiple lines of values.
424
This detects the end of the list, because it will be a line that
425
does not start properly indented.
428
start = '#' + (' '*indent)
430
if self._next_line is None or self._next_line[:len(start)] != start:
433
for line in self._next():
434
values.append(line[len(start):-1].decode('utf-8'))
435
if self._next_line is None or self._next_line[:len(start)] != start:
439
def _read_one_patch(self):
440
"""Read in one patch, return the complete patch, along with
443
:return: action, lines, do_continue
445
#mutter('_read_one_patch: %r' % self._next_line)
446
# Peek and see if there are no patches
447
if self._next_line is None or self._next_line.startswith('#'):
448
return None, [], False
452
for line in self._next():
454
if not line.startswith('==='):
455
raise MalformedPatches('The first line of all patches'
456
' should be a bzr meta line "==="'
458
action = line[4:-1].decode('utf-8')
459
elif line.startswith('... '):
460
action += line[len('... '):-1].decode('utf-8')
462
if (self._next_line is not None and
463
self._next_line.startswith('===')):
464
return action, lines, True
465
elif self._next_line is None or self._next_line.startswith('#'):
466
return action, lines, False
470
elif not line.startswith('... '):
473
return action, lines, False
475
def _read_patches(self):
477
revision_actions = []
479
action, lines, do_continue = self._read_one_patch()
480
if action is not None:
481
revision_actions.append((action, lines))
482
assert self.info.revisions[-1].tree_actions is None
483
self.info.revisions[-1].tree_actions = revision_actions
485
def _read_footer(self):
486
"""Read the rest of the meta information.
488
:param first_line: The previous step iterates past what it
489
can handle. That extra line is given here.
491
for line in self._next():
492
self._handle_next(line)
493
if not self._next_line.startswith('#'):
496
if self._next_line is None:
285
def _validate_revision(self, inventory, revision_id):
286
"""Make sure all revision entries match their checksum."""
288
# This is a mapping from each revision id to it's sha hash
291
rev = self.get_revision(revision_id)
292
rev_info = self.get_revision_info(revision_id)
293
if not (rev.revision_id == rev_info.revision_id):
294
raise AssertionError()
295
if not (rev.revision_id == revision_id):
296
raise AssertionError()
297
sha1 = self._testament_sha1(rev, inventory)
298
if sha1 != rev_info.sha1:
299
raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
300
if rev.revision_id in rev_to_sha1:
301
raise BzrError('Revision {%s} given twice in the list'
303
rev_to_sha1[rev.revision_id] = sha1
499
305
def _update_tree(self, bundle_tree, revision_id):
500
306
"""This fills out a BundleTree based on the information