13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
17
"""Read in a bundle stream, and process it into a BundleReader object."""
28
24
import bzrlib.errors
29
from bzrlib.bundle import apply_bundle
30
from bzrlib.errors import (TestamentMismatch, BzrError,
25
from bzrlib.errors import (TestamentMismatch, BzrError,
31
26
MalformedHeader, MalformedPatches, NotABundle)
32
27
from bzrlib.inventory import (Inventory, InventoryEntry,
33
28
InventoryDirectory, InventoryFile,
29
InventoryLink, ROOT_ID)
35
30
from bzrlib.osutils import sha_file, sha_string, pathjoin
36
31
from bzrlib.revision import Revision, NULL_REVISION
37
32
from bzrlib.testament import StrictTestament
77
72
if self.properties:
78
73
for property in self.properties:
79
74
key_end = property.find(': ')
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:]
75
assert key_end is not None
76
key = property[:key_end].encode('utf-8')
77
value = property[key_end+2:].encode('utf-8')
88
78
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()]
106
83
class BundleInfo(object):
107
84
"""This contains the meta information. Stuff that allows you to
108
85
recreate the revision or inventory XML.
110
def __init__(self, bundle_format=None):
111
self.bundle_format = None
112
88
self.committer = None
114
90
self.message = None
136
109
split up, based on the assumptions that can be made
137
110
when information is missing.
139
from bzrlib.timestamp import unpack_highres_date
112
from bzrlib.bundle.serializer import unpack_highres_date
140
113
# Put in all of the guessable information.
141
114
if not self.timestamp and self.date:
142
115
self.timestamp, self.timezone = unpack_highres_date(self.date)
159
132
def get_base(self, revision):
160
133
revision_info = self.get_revision_info(revision.revision_id)
161
134
if revision_info.base_id is not None:
162
return revision_info.base_id
135
if revision_info.base_id == NULL_REVISION:
138
return revision_info.base_id
163
139
if len(revision.parent_ids) == 0:
164
140
# There is no base listed, and
165
141
# the lowest revision doesn't have a parent
166
142
# so this is probably against the empty tree
167
# and thus base truly is NULL_REVISION
143
# and thus base truly is None
170
146
return revision.parent_ids[-1]
194
170
def revision_tree(self, repository, revision_id, base=None):
195
171
revision = self.get_revision(revision_id)
196
172
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)
173
assert base != revision_id
174
self._validate_references_from_repository(repository)
201
175
revision_info = self.get_revision_info(revision_id)
202
176
inventory_revision_id = revision_id
203
bundle_tree = BundleTree(repository.revision_tree(base),
177
bundle_tree = BundleTree(repository.revision_tree(base),
204
178
inventory_revision_id)
205
179
self._update_tree(bundle_tree, revision_id)
239
213
for rev_info in self.revisions:
240
214
checked[rev_info.revision_id] = True
241
215
add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
243
217
for (rev, rev_info) in zip(self.real_revisions, self.revisions):
244
218
add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
248
222
for revision_id, sha1 in rev_to_sha.iteritems():
249
223
if repository.has_revision(revision_id):
250
testament = StrictTestament.from_revision(repository,
224
testament = StrictTestament.from_revision(repository,
252
local_sha1 = self._testament_sha1_from_revision(repository,
226
local_sha1 = testament.as_sha1()
254
227
if sha1 != local_sha1:
255
raise BzrError('sha1 mismatch. For revision id {%s}'
228
raise BzrError('sha1 mismatch. For revision id {%s}'
256
229
'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
259
232
elif revision_id not in checked:
260
233
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))
262
250
if len(missing) > 0:
263
251
# I don't know if this is an error yet
264
252
warning('Not all revision hashes could be validated.'
265
253
' Unable validate %d hashes' % len(missing))
266
254
mutter('Verified %d sha hashes for the bundle.' % count)
267
self._validated_revisions_against_repo = True
269
256
def _validate_inventory(self, inv, revision_id):
270
257
"""At this point we should have generated the BundleTree,
271
258
so build up an inventory, and make sure the hashes match.
261
assert inv is not None
273
263
# Now we should have a complete inventory entry.
274
264
s = serializer_v5.write_inventory_to_string(inv)
275
265
sha1 = sha_string(s)
276
266
# Target revision is the last entry in the real_revisions list
277
267
rev = self.get_revision(revision_id)
278
if rev.revision_id != revision_id:
279
raise AssertionError()
268
assert rev.revision_id == revision_id
280
269
if sha1 != rev.inventory_sha1:
281
270
open(',,bogus-inv', 'wb').write(s)
282
271
warning('Inventory sha hash mismatch for revision %s. %s'
288
277
# This is a mapping from each revision id to it's sha hash
291
280
rev = self.get_revision(revision_id)
292
281
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)
282
assert rev.revision_id == rev_info.revision_id
283
assert rev.revision_id == revision_id
284
sha1 = StrictTestament(rev, inventory).as_sha1()
298
285
if sha1 != rev_info.sha1:
299
286
raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
300
if rev.revision_id in rev_to_sha1:
287
if rev_to_sha1.has_key(rev.revision_id):
301
288
raise BzrError('Revision {%s} given twice in the list'
302
289
% (rev.revision_id))
303
290
rev_to_sha1[rev.revision_id] = sha1
312
299
def get_rev_id(last_changed, path, kind):
313
300
if last_changed is not None:
314
# last_changed will be a Unicode string because of how it was
315
# read. Convert it back to utf8.
316
changed_revision_id = osutils.safe_revision_id(last_changed,
301
changed_revision_id = last_changed.decode('utf-8')
319
303
changed_revision_id = revision_id
320
304
bundle_tree.note_last_changed(path, changed_revision_id)
340
325
return last_changed, encoding
342
327
def do_patch(path, lines, encoding):
343
if encoding == 'base64':
328
if encoding is not None:
329
assert encoding == 'base64'
344
330
patch = base64.decodestring(''.join(lines))
345
elif encoding is None:
346
332
patch = ''.join(lines)
348
raise ValueError(encoding)
349
333
bundle_tree.note_patch(path, patch)
351
335
def renamed(kind, extra, lines):
387
371
if not info[1].startswith('file-id:'):
388
372
raise BzrError('The file-id should follow the path for an add'
390
# This will be Unicode because of how the stream is read. Turn it
391
# back into a utf8 file_id
392
file_id = osutils.safe_file_id(info[1][8:], warn=False)
374
file_id = info[1][8:]
394
376
bundle_tree.note_id(file_id, path, kind)
395
377
# this will be overridden in extra_info if executable is specified.
440
422
' (unrecognized action): %r' % action_line)
441
423
valid_actions[action](kind, extra, lines)
443
def install_revisions(self, target_repo, stream_input=True):
444
"""Install revisions and return the target revision
446
:param target_repo: The repository to install into
447
:param stream_input: Ignored by this implementation.
449
apply_bundle.install_bundle(target_repo, self)
452
def get_merge_request(self, target_repo):
453
"""Provide data for performing a merge
455
Returns suggested base, suggested target, and patch verification status
457
return None, self.target, 'inapplicable'
460
426
class BundleTree(Tree):
461
427
def __init__(self, base_tree, revision_id):
474
440
self.revision_id = revision_id
475
441
self._inventory = None
444
def _true_path(path):
477
450
def __str__(self):
478
451
return pprint.pformat(self.__dict__)
480
453
def note_rename(self, old_path, new_path):
481
454
"""A file/directory has been renamed from old_path => new_path"""
482
if new_path in self._renamed:
483
raise AssertionError(new_path)
484
if old_path in self._renamed_r:
485
raise AssertionError(old_path)
455
new_path = self._true_path(new_path)
456
old_path = self._true_path(old_path)
457
assert not self._renamed.has_key(new_path)
458
assert not self._renamed_r.has_key(old_path)
486
459
self._renamed[new_path] = old_path
487
460
self._renamed_r[old_path] = new_path
489
462
def note_id(self, new_id, new_path, kind='file'):
490
463
"""Files that don't exist in base need a new id."""
464
new_path = self._true_path(new_path)
491
465
self._new_id[new_path] = new_id
492
466
self._new_id_r[new_id] = new_path
493
467
self._kinds[new_id] = kind
495
469
def note_last_changed(self, file_id, revision_id):
496
if (file_id in self._last_changed
470
if (self._last_changed.has_key(file_id)
497
471
and self._last_changed[file_id] != revision_id):
498
472
raise BzrError('Mismatched last-changed revision for file_id {%s}'
499
473
': %s != %s' % (file_id,
504
478
def note_patch(self, new_path, patch):
505
479
"""There is a patch for a given filename."""
506
self.patches[new_path] = patch
480
self.patches[self._true_path(new_path)] = patch
508
482
def note_target(self, new_path, target):
509
483
"""The symlink at the new path has the given target"""
510
self._targets[new_path] = target
484
self._targets[self._true_path(new_path)] = target
512
486
def note_deletion(self, old_path):
513
487
"""The file at old_path has been deleted."""
514
self.deleted.append(old_path)
488
self.deleted.append(self._true_path(old_path))
516
490
def note_executable(self, new_path, executable):
517
self._executable[new_path] = executable
491
self._executable[self._true_path(new_path)] = executable
519
493
def old_path(self, new_path):
520
494
"""Get the old_path (path in the base_tree) for the file at new_path"""
521
if new_path[:1] in ('\\', '/'):
522
raise ValueError(new_path)
495
new_path = self._true_path(new_path)
496
assert new_path[:1] not in ('\\', '/')
523
497
old_path = self._renamed.get(new_path)
524
498
if old_path is not None:
537
511
old_path = new_path
538
512
#If the new path wasn't in renamed, the old one shouldn't be in
540
if old_path in self._renamed_r:
514
if self._renamed_r.has_key(old_path):
544
518
def new_path(self, old_path):
545
519
"""Get the new_path (path in the target_tree) for the file at old_path
546
520
in the base tree.
548
if old_path[:1] in ('\\', '/'):
549
raise ValueError(old_path)
522
old_path = self._true_path(old_path)
523
assert old_path[:1] not in ('\\', '/')
550
524
new_path = self._renamed_r.get(old_path)
551
525
if new_path is not None:
553
if new_path in self._renamed:
527
if self._renamed.has_key(new_path):
555
529
dirname,basename = os.path.split(old_path)
556
530
if dirname != '':
617
591
base_id = self.old_contents_id(file_id)
618
if (base_id is not None and
619
base_id != self.base_tree.inventory.root.file_id):
592
if base_id is not None:
620
593
patch_original = self.base_tree.get_file(base_id)
622
595
patch_original = None
623
596
file_patch = self.patches.get(self.id2path(file_id))
624
597
if file_patch is None:
625
if (patch_original is None and
598
if (patch_original is None and
626
599
self.get_kind(file_id) == 'directory'):
627
600
return StringIO()
628
if patch_original is None:
629
raise AssertionError("None: %s" % file_id)
601
assert patch_original is not None, "None: %s" % file_id
630
602
return patch_original
632
if file_patch.startswith('\\'):
634
'Malformed patch for %s, %r' % (file_id, file_patch))
604
assert not file_patch.startswith('\\'), \
605
'Malformed patch for %s, %r' % (file_id, file_patch)
635
606
return patched_file(file_patch, patch_original)
637
608
def get_symlink_target(self, file_id):
684
655
This need to be called before ever accessing self.inventory
686
657
from os.path import dirname, basename
659
assert self.base_tree is not None
687
660
base_inv = self.base_tree.inventory
688
inv = Inventory(None, self.revision_id)
662
if base_inv.root is not None:
663
root_id = base_inv.root.file_id
665
root_id = self._new_id['']
669
# New inventories have a unique root_id
670
inv = Inventory(root_id, self.revision_id)
672
inv = Inventory(revision_id=self.revision_id)
673
inv.root.revision = self.get_last_changed(root_id)
690
675
def add_entry(file_id):
691
676
path = self.id2path(file_id)
679
parent_path = dirname(path)
680
if parent_path == u'':
697
parent_path = dirname(path)
698
683
parent_id = self.path2id(parent_path)
700
685
kind = self.get_kind(file_id)