1
# Copyright (C) 2006 by Canonical Ltd
1
# Copyright (C) 2006 Canonical Ltd
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
24
28
import bzrlib.errors
29
from bzrlib.bundle import apply_bundle
25
30
from bzrlib.errors import (TestamentMismatch, BzrError,
26
31
MalformedHeader, MalformedPatches, NotABundle)
27
32
from bzrlib.inventory import (Inventory, InventoryEntry,
72
77
if self.properties:
73
78
for property in self.properties:
74
79
key_end = property.find(': ')
75
assert key_end is not None
76
key = property[:key_end].encode('utf-8')
77
value = property[key_end+2:].encode('utf-8')
81
if not property.endswith(':'):
82
raise ValueError(property)
83
key = str(property[:-1])
86
key = str(property[:key_end])
87
value = property[key_end+2:]
78
88
rev.properties[key] = value
93
def from_revision(revision):
94
revision_info = RevisionInfo(revision.revision_id)
95
date = timestamp.format_highres_date(revision.timestamp,
97
revision_info.date = date
98
revision_info.timezone = revision.timezone
99
revision_info.timestamp = revision.timestamp
100
revision_info.message = revision.message.split('\n')
101
revision_info.properties = [': '.join(p) for p in
102
revision.properties.iteritems()]
83
106
class BundleInfo(object):
84
107
"""This contains the meta information. Stuff that allows you to
85
108
recreate the revision or inventory XML.
110
def __init__(self, bundle_format=None):
111
self.bundle_format = None
88
112
self.committer = None
90
114
self.message = None
109
136
split up, based on the assumptions that can be made
110
137
when information is missing.
112
from bzrlib.bundle.serializer import unpack_highres_date
139
from bzrlib.timestamp import unpack_highres_date
113
140
# Put in all of the guessable information.
114
141
if not self.timestamp and self.date:
115
142
self.timestamp, self.timezone = unpack_highres_date(self.date)
170
197
def revision_tree(self, repository, revision_id, base=None):
171
198
revision = self.get_revision(revision_id)
172
199
base = self.get_base(revision)
173
assert base != revision_id
174
self._validate_references_from_repository(repository)
200
if base == revision_id:
201
raise AssertionError()
202
if not self._validated_revisions_against_repo:
203
self._validate_references_from_repository(repository)
175
204
revision_info = self.get_revision_info(revision_id)
176
205
inventory_revision_id = revision_id
177
206
bundle_tree = BundleTree(repository.revision_tree(base),
223
252
if repository.has_revision(revision_id):
224
253
testament = StrictTestament.from_revision(repository,
226
local_sha1 = testament.as_sha1()
255
local_sha1 = self._testament_sha1_from_revision(repository,
227
257
if sha1 != local_sha1:
228
258
raise BzrError('sha1 mismatch. For revision id {%s}'
229
259
'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
232
262
elif revision_id not in checked:
233
263
missing[revision_id] = sha1
235
for inv_id, sha1 in inv_to_sha.iteritems():
236
if repository.has_revision(inv_id):
237
# Note: branch.get_inventory_sha1() just returns the value that
238
# is stored in the revision text, and that value may be out
239
# of date. This is bogus, because that means we aren't
240
# validating the actual text, just that we wrote and read the
241
# string. But for now, what the hell.
242
local_sha1 = repository.get_inventory_sha1(inv_id)
243
if sha1 != local_sha1:
244
raise BzrError('sha1 mismatch. For inventory id {%s}'
245
'local: %s, bundle: %s' %
246
(inv_id, local_sha1, sha1))
250
265
if len(missing) > 0:
251
266
# I don't know if this is an error yet
252
267
warning('Not all revision hashes could be validated.'
253
268
' Unable validate %d hashes' % len(missing))
254
269
mutter('Verified %d sha hashes for the bundle.' % count)
270
self._validated_revisions_against_repo = True
256
272
def _validate_inventory(self, inv, revision_id):
257
273
"""At this point we should have generated the BundleTree,
258
274
so build up an inventory, and make sure the hashes match.
261
assert inv is not None
263
276
# Now we should have a complete inventory entry.
264
277
s = serializer_v5.write_inventory_to_string(inv)
265
278
sha1 = sha_string(s)
266
279
# Target revision is the last entry in the real_revisions list
267
280
rev = self.get_revision(revision_id)
268
assert rev.revision_id == revision_id
281
if rev.revision_id != revision_id:
282
raise AssertionError()
269
283
if sha1 != rev.inventory_sha1:
270
284
open(',,bogus-inv', 'wb').write(s)
271
285
warning('Inventory sha hash mismatch for revision %s. %s'
280
294
rev = self.get_revision(revision_id)
281
295
rev_info = self.get_revision_info(revision_id)
282
assert rev.revision_id == rev_info.revision_id
283
assert rev.revision_id == revision_id
284
sha1 = StrictTestament(rev, inventory).as_sha1()
296
if not (rev.revision_id == rev_info.revision_id):
297
raise AssertionError()
298
if not (rev.revision_id == revision_id):
299
raise AssertionError()
300
sha1 = self._testament_sha1(rev, inventory)
285
301
if sha1 != rev_info.sha1:
286
302
raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
287
303
if rev.revision_id in rev_to_sha1:
299
315
def get_rev_id(last_changed, path, kind):
300
316
if last_changed is not None:
301
changed_revision_id = last_changed.decode('utf-8')
317
# last_changed will be a Unicode string because of how it was
318
# read. Convert it back to utf8.
319
changed_revision_id = osutils.safe_revision_id(last_changed,
303
322
changed_revision_id = revision_id
304
323
bundle_tree.note_last_changed(path, changed_revision_id)
315
334
if name == 'last-changed':
316
335
last_changed = value
317
336
elif name == 'executable':
318
assert value in ('yes', 'no'), value
319
337
val = (value == 'yes')
320
338
bundle_tree.note_executable(new_path, val)
321
339
elif name == 'target':
325
343
return last_changed, encoding
327
345
def do_patch(path, lines, encoding):
328
if encoding is not None:
329
assert encoding == 'base64'
346
if encoding == 'base64':
330
347
patch = base64.decodestring(''.join(lines))
348
elif encoding is None:
332
349
patch = ''.join(lines)
351
raise ValueError(encoding)
333
352
bundle_tree.note_patch(path, patch)
335
354
def renamed(kind, extra, lines):
371
390
if not info[1].startswith('file-id:'):
372
391
raise BzrError('The file-id should follow the path for an add'
374
file_id = info[1][8:]
393
# This will be Unicode because of how the stream is read. Turn it
394
# back into a utf8 file_id
395
file_id = osutils.safe_file_id(info[1][8:], warn=False)
376
397
bundle_tree.note_id(file_id, path, kind)
377
398
# this will be overridden in extra_info if executable is specified.
422
443
' (unrecognized action): %r' % action_line)
423
444
valid_actions[action](kind, extra, lines)
446
def install_revisions(self, target_repo, stream_input=True):
447
"""Install revisions and return the target revision
449
:param target_repo: The repository to install into
450
:param stream_input: Ignored by this implementation.
452
apply_bundle.install_bundle(target_repo, self)
455
def get_merge_request(self, target_repo):
456
"""Provide data for performing a merge
458
Returns suggested base, suggested target, and patch verification status
460
return None, self.target, 'inapplicable'
426
463
class BundleTree(Tree):
427
464
def __init__(self, base_tree, revision_id):
446
483
def note_rename(self, old_path, new_path):
447
484
"""A file/directory has been renamed from old_path => new_path"""
448
assert new_path not in self._renamed
449
assert old_path not in self._renamed_r
485
if new_path in self._renamed:
486
raise AssertionError(new_path)
487
if old_path in self._renamed_r:
488
raise AssertionError(old_path)
450
489
self._renamed[new_path] = old_path
451
490
self._renamed_r[old_path] = new_path
483
522
def old_path(self, new_path):
484
523
"""Get the old_path (path in the base_tree) for the file at new_path"""
485
assert new_path[:1] not in ('\\', '/')
524
if new_path[:1] in ('\\', '/'):
525
raise ValueError(new_path)
486
526
old_path = self._renamed.get(new_path)
487
527
if old_path is not None:
508
548
"""Get the new_path (path in the target_tree) for the file at old_path
509
549
in the base tree.
511
assert old_path[:1] not in ('\\', '/')
551
if old_path[:1] in ('\\', '/'):
552
raise ValueError(old_path)
512
553
new_path = self._renamed_r.get(old_path)
513
554
if new_path is not None:
586
628
if (patch_original is None and
587
629
self.get_kind(file_id) == 'directory'):
588
630
return StringIO()
589
assert patch_original is not None, "None: %s" % file_id
631
if patch_original is None:
632
raise AssertionError("None: %s" % file_id)
590
633
return patch_original
592
assert not file_patch.startswith('\\'), \
593
'Malformed patch for %s, %r' % (file_id, file_patch)
635
if file_patch.startswith('\\'):
637
'Malformed patch for %s, %r' % (file_id, file_patch))
594
638
return patched_file(file_patch, patch_original)
596
640
def get_symlink_target(self, file_id):
643
687
This need to be called before ever accessing self.inventory
645
689
from os.path import dirname, basename
647
assert self.base_tree is not None
648
690
base_inv = self.base_tree.inventory
649
root_id = base_inv.root.file_id
651
# New inventories have a unique root_id
652
inv = Inventory(root_id, self.revision_id)
654
inv = Inventory(revision_id=self.revision_id)
655
inv.root.revision = self.get_last_changed(root_id)
691
inv = Inventory(None, self.revision_id)
657
693
def add_entry(file_id):
658
694
path = self.id2path(file_id)
661
parent_path = dirname(path)
662
if parent_path == u'':
700
parent_path = dirname(path)
665
701
parent_id = self.path2id(parent_path)
667
703
kind = self.get_kind(file_id)