192
169
raise KeyError(revision_id)
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)
172
class BundleReader(object):
173
"""This class reads in a bundle from a file, and returns
174
a Bundle object, which can then be applied against a tree.
176
def __init__(self, from_file):
177
"""Read in the bundle from the file.
179
:param from_file: A file-like object (must have iterator support).
181
object.__init__(self)
182
self.from_file = iter(from_file)
183
self._next_line = None
185
self.info = BundleInfo()
186
# We put the actual inventory ids in the footer, so that the patch
187
# is easier to read for humans.
188
# Unfortunately, that means we need to read everything before we
189
# can create a proper bundle.
195
while self._next_line is not None:
196
self._read_revision_header()
197
if self._next_line is None:
203
"""Make sure that the information read in makes sense
204
and passes appropriate checksums.
206
# Fill in all the missing blanks for the revisions
207
# and generate the real_revisions list.
208
self.info.complete_info()
210
def _validate_revision(self, inventory, revision_id):
211
"""Make sure all revision entries match their checksum."""
213
# This is a mapping from each revision id to it's sha hash
216
rev = self.info.get_revision(revision_id)
217
rev_info = self.info.get_revision_info(revision_id)
218
assert rev.revision_id == rev_info.revision_id
219
assert rev.revision_id == revision_id
220
sha1 = StrictTestament(rev, inventory).as_sha1()
221
if sha1 != rev_info.sha1:
222
raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
223
if rev_to_sha1.has_key(rev.revision_id):
224
raise BzrError('Revision {%s} given twice in the list'
226
rev_to_sha1[rev.revision_id] = sha1
213
228
def _validate_references_from_repository(self, repository):
214
229
"""Now that we have a repository which should have some of the
236
251
# All of the contained revisions were checked
237
252
# in _validate_revisions
239
for rev_info in self.revisions:
254
for rev_info in self.info.revisions:
240
255
checked[rev_info.revision_id] = True
241
256
add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
243
for (rev, rev_info) in zip(self.real_revisions, self.revisions):
258
for (rev, rev_info) in zip(self.info.real_revisions, self.info.revisions):
244
259
add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
248
263
for revision_id, sha1 in rev_to_sha.iteritems():
249
264
if repository.has_revision(revision_id):
250
testament = StrictTestament.from_revision(repository,
265
testament = StrictTestament.from_revision(repository,
252
local_sha1 = self._testament_sha1_from_revision(repository,
267
local_sha1 = testament.as_sha1()
254
268
if sha1 != local_sha1:
255
raise BzrError('sha1 mismatch. For revision id {%s}'
269
raise BzrError('sha1 mismatch. For revision id {%s}'
256
270
'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
259
273
elif revision_id not in checked:
260
274
missing[revision_id] = sha1
276
for inv_id, sha1 in inv_to_sha.iteritems():
277
if repository.has_revision(inv_id):
278
# Note: branch.get_inventory_sha1() just returns the value that
279
# is stored in the revision text, and that value may be out
280
# of date. This is bogus, because that means we aren't
281
# validating the actual text, just that we wrote and read the
282
# string. But for now, what the hell.
283
local_sha1 = repository.get_inventory_sha1(inv_id)
284
if sha1 != local_sha1:
285
raise BzrError('sha1 mismatch. For inventory id {%s}'
286
'local: %s, bundle: %s' %
287
(inv_id, local_sha1, sha1))
262
291
if len(missing) > 0:
263
292
# I don't know if this is an error yet
264
293
warning('Not all revision hashes could be validated.'
265
294
' Unable validate %d hashes' % len(missing))
266
295
mutter('Verified %d sha hashes for the bundle.' % count)
267
self._validated_revisions_against_repo = True
269
297
def _validate_inventory(self, inv, revision_id):
270
298
"""At this point we should have generated the BundleTree,
271
299
so build up an inventory, and make sure the hashes match.
302
assert inv is not None
273
304
# Now we should have a complete inventory entry.
274
305
s = serializer_v5.write_inventory_to_string(inv)
275
306
sha1 = sha_string(s)
276
307
# Target revision is the last entry in the real_revisions list
277
rev = self.get_revision(revision_id)
278
if rev.revision_id != revision_id:
279
raise AssertionError()
308
rev = self.info.get_revision(revision_id)
309
assert rev.revision_id == revision_id
280
310
if sha1 != rev.inventory_sha1:
281
311
open(',,bogus-inv', 'wb').write(s)
282
312
warning('Inventory sha hash mismatch for revision %s. %s'
283
313
' != %s' % (revision_id, sha1, rev.inventory_sha1))
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
315
def get_bundle(self, repository):
316
"""Return the meta information, and a Bundle tree which can
317
be used to populate the local stores and working tree, respectively.
319
return self.info, self.revision_tree(repository, self.info.target)
321
def revision_tree(self, repository, revision_id, base=None):
322
revision = self.info.get_revision(revision_id)
323
base = self.info.get_base(revision)
324
assert base != revision_id
325
self._validate_references_from_repository(repository)
326
revision_info = self.info.get_revision_info(revision_id)
327
inventory_revision_id = revision_id
328
bundle_tree = BundleTree(repository.revision_tree(base),
329
inventory_revision_id)
330
self._update_tree(bundle_tree, revision_id)
332
inv = bundle_tree.inventory
333
self._validate_inventory(inv, revision_id)
334
self._validate_revision(inv, revision_id)
339
"""yield the next line, but secretly
340
keep 1 extra line for peeking.
342
for line in self.from_file:
343
last = self._next_line
344
self._next_line = line
346
#mutter('yielding line: %r' % last)
348
last = self._next_line
349
self._next_line = None
350
#mutter('yielding line: %r' % last)
353
def _read_header(self):
354
"""Read the bzr header"""
355
header = get_header()
357
for line in self._next():
359
# not all mailers will keep trailing whitespace
362
if (not line.startswith('# ') or not line.endswith('\n')
363
or line[2:-1].decode('utf-8') != header[0]):
364
raise MalformedHeader('Found a header, but it'
365
' was improperly formatted')
366
header.pop(0) # We read this line.
368
break # We found everything.
369
elif (line.startswith('#') and line.endswith('\n')):
370
line = line[1:-1].strip().decode('utf-8')
371
if line[:len(header_str)] == header_str:
372
if line == header[0]:
375
raise MalformedHeader('Found what looks like'
376
' a header, but did not match')
379
raise NotABundle('Did not find an opening header')
381
def _read_revision_header(self):
382
self.info.revisions.append(RevisionInfo(None))
383
for line in self._next():
384
# The bzr header is terminated with a blank line
385
# which does not start with '#'
386
if line is None or line == '\n':
388
self._handle_next(line)
390
def _read_next_entry(self, line, indent=1):
391
"""Read in a key-value pair
393
if not line.startswith('#'):
394
raise MalformedHeader('Bzr header did not start with #')
395
line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
396
if line[:indent] == ' '*indent:
399
return None, None# Ignore blank lines
401
loc = line.find(': ')
406
value = self._read_many(indent=indent+2)
407
elif line[-1:] == ':':
409
value = self._read_many(indent=indent+2)
411
raise MalformedHeader('While looking for key: value pairs,'
412
' did not find the colon %r' % (line))
414
key = key.replace(' ', '_')
415
#mutter('found %s: %s' % (key, value))
418
def _handle_next(self, line):
421
key, value = self._read_next_entry(line, indent=1)
422
mutter('_handle_next %r => %r' % (key, value))
426
revision_info = self.info.revisions[-1]
427
if hasattr(revision_info, key):
428
if getattr(revision_info, key) is None:
429
setattr(revision_info, key, value)
431
raise MalformedHeader('Duplicated Key: %s' % key)
433
# What do we do with a key we don't recognize
434
raise MalformedHeader('Unknown Key: "%s"' % key)
436
def _read_many(self, indent):
437
"""If a line ends with no entry, that means that it should be
438
followed with multiple lines of values.
440
This detects the end of the list, because it will be a line that
441
does not start properly indented.
444
start = '#' + (' '*indent)
446
if self._next_line is None or self._next_line[:len(start)] != start:
449
for line in self._next():
450
values.append(line[len(start):-1].decode('utf-8'))
451
if self._next_line is None or self._next_line[:len(start)] != start:
455
def _read_one_patch(self):
456
"""Read in one patch, return the complete patch, along with
459
:return: action, lines, do_continue
461
#mutter('_read_one_patch: %r' % self._next_line)
462
# Peek and see if there are no patches
463
if self._next_line is None or self._next_line.startswith('#'):
464
return None, [], False
468
for line in self._next():
470
if not line.startswith('==='):
471
raise MalformedPatches('The first line of all patches'
472
' should be a bzr meta line "==="'
474
action = line[4:-1].decode('utf-8')
475
elif line.startswith('... '):
476
action += line[len('... '):-1].decode('utf-8')
478
if (self._next_line is not None and
479
self._next_line.startswith('===')):
480
return action, lines, True
481
elif self._next_line is None or self._next_line.startswith('#'):
482
return action, lines, False
486
elif not line.startswith('... '):
489
return action, lines, False
491
def _read_patches(self):
493
revision_actions = []
495
action, lines, do_continue = self._read_one_patch()
496
if action is not None:
497
revision_actions.append((action, lines))
498
assert self.info.revisions[-1].tree_actions is None
499
self.info.revisions[-1].tree_actions = revision_actions
501
def _read_footer(self):
502
"""Read the rest of the meta information.
504
:param first_line: The previous step iterates past what it
505
can handle. That extra line is given here.
507
for line in self._next():
508
self._handle_next(line)
509
if not self._next_line.startswith('#'):
512
if self._next_line is None:
305
515
def _update_tree(self, bundle_tree, revision_id):
306
516
"""This fills out a BundleTree based on the information