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."""
20
from cStringIO import StringIO
29
from bzrlib.bundle import apply_bundle
30
from bzrlib.errors import (TestamentMismatch, BzrError,
31
MalformedHeader, MalformedPatches, NotABundle)
32
from bzrlib.inventory import (Inventory, InventoryEntry,
33
InventoryDirectory, InventoryFile,
35
from bzrlib.osutils import sha_file, sha_string, pathjoin
36
from bzrlib.revision import Revision, NULL_REVISION
37
from bzrlib.testament import StrictTestament
38
from bzrlib.trace import mutter, warning
39
import bzrlib.transport
40
from bzrlib.tree import Tree
41
import bzrlib.urlutils
42
from bzrlib.xml5 import serializer_v5
45
class RevisionInfo(object):
46
"""Gets filled out for each revision object that is read.
48
def __init__(self, revision_id):
49
self.revision_id = revision_id
55
self.inventory_sha1 = None
57
self.parent_ids = None
60
self.properties = None
61
self.tree_actions = None
64
return pprint.pformat(self.__dict__)
66
def as_revision(self):
67
rev = Revision(revision_id=self.revision_id,
68
committer=self.committer,
69
timestamp=float(self.timestamp),
70
timezone=int(self.timezone),
71
inventory_sha1=self.inventory_sha1,
72
message='\n'.join(self.message))
75
rev.parent_ids.extend(self.parent_ids)
78
for property in self.properties:
79
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:]
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()]
106
class BundleInfo(object):
107
"""This contains the meta information. Stuff that allows you to
108
recreate the revision or inventory XML.
110
def __init__(self, bundle_format=None):
111
self.bundle_format = None
112
self.committer = None
116
# A list of RevisionInfo objects
119
# The next entries are created during complete_info() and
120
# other post-read functions.
122
# A list of real Revision objects
123
self.real_revisions = []
125
self.timestamp = None
128
# Have we checked the repository yet?
129
self._validated_revisions_against_repo = False
132
return pprint.pformat(self.__dict__)
134
def complete_info(self):
135
"""This makes sure that all information is properly
136
split up, based on the assumptions that can be made
137
when information is missing.
139
from bzrlib.timestamp import unpack_highres_date
140
# Put in all of the guessable information.
141
if not self.timestamp and self.date:
142
self.timestamp, self.timezone = unpack_highres_date(self.date)
144
self.real_revisions = []
145
for rev in self.revisions:
146
if rev.timestamp is None:
147
if rev.date is not None:
148
rev.timestamp, rev.timezone = \
149
unpack_highres_date(rev.date)
151
rev.timestamp = self.timestamp
152
rev.timezone = self.timezone
153
if rev.message is None and self.message:
154
rev.message = self.message
155
if rev.committer is None and self.committer:
156
rev.committer = self.committer
157
self.real_revisions.append(rev.as_revision())
159
def get_base(self, revision):
160
revision_info = self.get_revision_info(revision.revision_id)
161
if revision_info.base_id is not None:
162
return revision_info.base_id
163
if len(revision.parent_ids) == 0:
164
# There is no base listed, and
165
# the lowest revision doesn't have a parent
166
# so this is probably against the empty tree
167
# and thus base truly is NULL_REVISION
170
return revision.parent_ids[-1]
172
def _get_target(self):
173
"""Return the target revision."""
174
if len(self.real_revisions) > 0:
175
return self.real_revisions[0].revision_id
176
elif len(self.revisions) > 0:
177
return self.revisions[0].revision_id
180
target = property(_get_target, doc='The target revision id')
182
def get_revision(self, revision_id):
183
for r in self.real_revisions:
184
if r.revision_id == revision_id:
186
raise KeyError(revision_id)
188
def get_revision_info(self, revision_id):
189
for r in self.revisions:
190
if r.revision_id == revision_id:
192
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)
213
def _validate_references_from_repository(self, repository):
214
"""Now that we have a repository which should have some of the
215
revisions we care about, go through and validate all of them
220
def add_sha(d, revision_id, sha1):
221
if revision_id is None:
223
raise BzrError('A Null revision should always'
224
'have a null sha1 hash')
227
# This really should have been validated as part
228
# of _validate_revisions but lets do it again
229
if sha1 != d[revision_id]:
230
raise BzrError('** Revision %r referenced with 2 different'
231
' sha hashes %s != %s' % (revision_id,
232
sha1, d[revision_id]))
234
d[revision_id] = sha1
236
# All of the contained revisions were checked
237
# in _validate_revisions
239
for rev_info in self.revisions:
240
checked[rev_info.revision_id] = True
241
add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
243
for (rev, rev_info) in zip(self.real_revisions, self.revisions):
244
add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
248
for revision_id, sha1 in rev_to_sha.iteritems():
249
if repository.has_revision(revision_id):
250
testament = StrictTestament.from_revision(repository,
252
local_sha1 = self._testament_sha1_from_revision(repository,
254
if sha1 != local_sha1:
255
raise BzrError('sha1 mismatch. For revision id {%s}'
256
'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
259
elif revision_id not in checked:
260
missing[revision_id] = sha1
263
# I don't know if this is an error yet
264
warning('Not all revision hashes could be validated.'
265
' Unable validate %d hashes' % len(missing))
266
mutter('Verified %d sha hashes for the bundle.' % count)
267
self._validated_revisions_against_repo = True
269
def _validate_inventory(self, inv, revision_id):
270
"""At this point we should have generated the BundleTree,
271
so build up an inventory, and make sure the hashes match.
273
# Now we should have a complete inventory entry.
274
s = serializer_v5.write_inventory_to_string(inv)
276
# 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()
280
if sha1 != rev.inventory_sha1:
281
open(',,bogus-inv', 'wb').write(s)
282
warning('Inventory sha hash mismatch for revision %s. %s'
283
' != %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
305
def _update_tree(self, bundle_tree, revision_id):
306
"""This fills out a BundleTree based on the information
309
:param bundle_tree: A BundleTree to update with the new information.
312
def get_rev_id(last_changed, path, kind):
313
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,
319
changed_revision_id = revision_id
320
bundle_tree.note_last_changed(path, changed_revision_id)
321
return changed_revision_id
323
def extra_info(info, new_path):
326
for info_item in info:
328
name, value = info_item.split(':', 1)
330
raise 'Value %r has no colon' % info_item
331
if name == 'last-changed':
333
elif name == 'executable':
334
val = (value == 'yes')
335
bundle_tree.note_executable(new_path, val)
336
elif name == 'target':
337
bundle_tree.note_target(new_path, value)
338
elif name == 'encoding':
340
return last_changed, encoding
342
def do_patch(path, lines, encoding):
343
if encoding == 'base64':
344
patch = base64.decodestring(''.join(lines))
345
elif encoding is None:
346
patch = ''.join(lines)
348
raise ValueError(encoding)
349
bundle_tree.note_patch(path, patch)
351
def renamed(kind, extra, lines):
352
info = extra.split(' // ')
354
raise BzrError('renamed action lines need both a from and to'
357
if info[1].startswith('=> '):
358
new_path = info[1][3:]
362
bundle_tree.note_rename(old_path, new_path)
363
last_modified, encoding = extra_info(info[2:], new_path)
364
revision = get_rev_id(last_modified, new_path, kind)
366
do_patch(new_path, lines, encoding)
368
def removed(kind, extra, lines):
369
info = extra.split(' // ')
371
# TODO: in the future we might allow file ids to be
372
# given for removed entries
373
raise BzrError('removed action lines should only have the path'
376
bundle_tree.note_deletion(path)
378
def added(kind, extra, lines):
379
info = extra.split(' // ')
381
raise BzrError('add action lines require the path and file id'
384
raise BzrError('add action lines have fewer than 5 entries.'
387
if not info[1].startswith('file-id:'):
388
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)
394
bundle_tree.note_id(file_id, path, kind)
395
# this will be overridden in extra_info if executable is specified.
396
bundle_tree.note_executable(path, False)
397
last_changed, encoding = extra_info(info[2:], path)
398
revision = get_rev_id(last_changed, path, kind)
399
if kind == 'directory':
401
do_patch(path, lines, encoding)
403
def modified(kind, extra, lines):
404
info = extra.split(' // ')
406
raise BzrError('modified action lines have at least'
407
'the path in them: %r' % extra)
410
last_modified, encoding = extra_info(info[1:], path)
411
revision = get_rev_id(last_modified, path, kind)
413
do_patch(path, lines, encoding)
421
for action_line, lines in \
422
self.get_revision_info(revision_id).tree_actions:
423
first = action_line.find(' ')
425
raise BzrError('Bogus action line'
426
' (no opening space): %r' % action_line)
427
second = action_line.find(' ', first+1)
429
raise BzrError('Bogus action line'
430
' (missing second space): %r' % action_line)
431
action = action_line[:first]
432
kind = action_line[first+1:second]
433
if kind not in ('file', 'directory', 'symlink'):
434
raise BzrError('Bogus action line'
435
' (invalid object kind %r): %r' % (kind, action_line))
436
extra = action_line[second+1:]
438
if action not in valid_actions:
439
raise BzrError('Bogus action line'
440
' (unrecognized action): %r' % action_line)
441
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
class BundleTree(Tree):
461
def __init__(self, base_tree, revision_id):
462
self.base_tree = base_tree
463
self._renamed = {} # Mapping from old_path => new_path
464
self._renamed_r = {} # new_path => old_path
465
self._new_id = {} # new_path => new_id
466
self._new_id_r = {} # new_id => new_path
467
self._kinds = {} # new_id => kind
468
self._last_changed = {} # new_id => revision_id
469
self._executable = {} # new_id => executable value
471
self._targets = {} # new path => new symlink target
473
self.contents_by_id = True
474
self.revision_id = revision_id
475
self._inventory = None
478
return pprint.pformat(self.__dict__)
480
def note_rename(self, old_path, new_path):
481
"""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)
486
self._renamed[new_path] = old_path
487
self._renamed_r[old_path] = new_path
489
def note_id(self, new_id, new_path, kind='file'):
490
"""Files that don't exist in base need a new id."""
491
self._new_id[new_path] = new_id
492
self._new_id_r[new_id] = new_path
493
self._kinds[new_id] = kind
495
def note_last_changed(self, file_id, revision_id):
496
if (file_id in self._last_changed
497
and self._last_changed[file_id] != revision_id):
498
raise BzrError('Mismatched last-changed revision for file_id {%s}'
499
': %s != %s' % (file_id,
500
self._last_changed[file_id],
502
self._last_changed[file_id] = revision_id
504
def note_patch(self, new_path, patch):
505
"""There is a patch for a given filename."""
506
self.patches[new_path] = patch
508
def note_target(self, new_path, target):
509
"""The symlink at the new path has the given target"""
510
self._targets[new_path] = target
512
def note_deletion(self, old_path):
513
"""The file at old_path has been deleted."""
514
self.deleted.append(old_path)
516
def note_executable(self, new_path, executable):
517
self._executable[new_path] = executable
519
def old_path(self, new_path):
520
"""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)
523
old_path = self._renamed.get(new_path)
524
if old_path is not None:
526
dirname,basename = os.path.split(new_path)
527
# dirname is not '' doesn't work, because
528
# dirname may be a unicode entry, and is
529
# requires the objects to be identical
531
old_dir = self.old_path(dirname)
535
old_path = pathjoin(old_dir, basename)
538
#If the new path wasn't in renamed, the old one shouldn't be in
540
if old_path in self._renamed_r:
544
def new_path(self, old_path):
545
"""Get the new_path (path in the target_tree) for the file at old_path
548
if old_path[:1] in ('\\', '/'):
549
raise ValueError(old_path)
550
new_path = self._renamed_r.get(old_path)
551
if new_path is not None:
553
if new_path in self._renamed:
555
dirname,basename = os.path.split(old_path)
557
new_dir = self.new_path(dirname)
561
new_path = pathjoin(new_dir, basename)
564
#If the old path wasn't in renamed, the new one shouldn't be in
566
if new_path in self._renamed:
570
def path2id(self, path):
571
"""Return the id of the file present at path in the target tree."""
572
file_id = self._new_id.get(path)
573
if file_id is not None:
575
old_path = self.old_path(path)
578
if old_path in self.deleted:
580
if getattr(self.base_tree, 'path2id', None) is not None:
581
return self.base_tree.path2id(old_path)
583
return self.base_tree.inventory.path2id(old_path)
585
def id2path(self, file_id):
586
"""Return the new path in the target tree of the file with id file_id"""
587
path = self._new_id_r.get(file_id)
590
old_path = self.base_tree.id2path(file_id)
593
if old_path in self.deleted:
595
return self.new_path(old_path)
597
def old_contents_id(self, file_id):
598
"""Return the id in the base_tree for the given file_id.
599
Return None if the file did not exist in base.
601
if self.contents_by_id:
602
if self.base_tree.has_id(file_id):
606
new_path = self.id2path(file_id)
607
return self.base_tree.path2id(new_path)
609
def get_file(self, file_id):
610
"""Return a file-like object containing the new contents of the
611
file given by file_id.
613
TODO: It might be nice if this actually generated an entry
614
in the text-store, so that the file contents would
617
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):
620
patch_original = self.base_tree.get_file(base_id)
622
patch_original = None
623
file_patch = self.patches.get(self.id2path(file_id))
624
if file_patch is None:
625
if (patch_original is None and
626
self.get_kind(file_id) == 'directory'):
628
if patch_original is None:
629
raise AssertionError("None: %s" % file_id)
630
return patch_original
632
if file_patch.startswith('\\'):
634
'Malformed patch for %s, %r' % (file_id, file_patch))
635
return patched_file(file_patch, patch_original)
637
def get_symlink_target(self, file_id):
638
new_path = self.id2path(file_id)
640
return self._targets[new_path]
642
return self.base_tree.get_symlink_target(file_id)
644
def get_kind(self, file_id):
645
if file_id in self._kinds:
646
return self._kinds[file_id]
647
return self.base_tree.inventory[file_id].kind
649
def is_executable(self, file_id):
650
path = self.id2path(file_id)
651
if path in self._executable:
652
return self._executable[path]
654
return self.base_tree.inventory[file_id].executable
656
def get_last_changed(self, file_id):
657
path = self.id2path(file_id)
658
if path in self._last_changed:
659
return self._last_changed[path]
660
return self.base_tree.inventory[file_id].revision
662
def get_size_and_sha1(self, file_id):
663
"""Return the size and sha1 hash of the given file id.
664
If the file was not locally modified, this is extracted
665
from the base_tree. Rather than re-reading the file.
667
new_path = self.id2path(file_id)
670
if new_path not in self.patches:
671
# If the entry does not have a patch, then the
672
# contents must be the same as in the base_tree
673
ie = self.base_tree.inventory[file_id]
674
if ie.text_size is None:
675
return ie.text_size, ie.text_sha1
676
return int(ie.text_size), ie.text_sha1
677
fileobj = self.get_file(file_id)
678
content = fileobj.read()
679
return len(content), sha_string(content)
681
def _get_inventory(self):
682
"""Build up the inventory entry for the BundleTree.
684
This need to be called before ever accessing self.inventory
686
from os.path import dirname, basename
687
base_inv = self.base_tree.inventory
688
inv = Inventory(None, self.revision_id)
690
def add_entry(file_id):
691
path = self.id2path(file_id)
697
parent_path = dirname(path)
698
parent_id = self.path2id(parent_path)
700
kind = self.get_kind(file_id)
701
revision_id = self.get_last_changed(file_id)
703
name = basename(path)
704
if kind == 'directory':
705
ie = InventoryDirectory(file_id, name, parent_id)
707
ie = InventoryFile(file_id, name, parent_id)
708
ie.executable = self.is_executable(file_id)
709
elif kind == 'symlink':
710
ie = InventoryLink(file_id, name, parent_id)
711
ie.symlink_target = self.get_symlink_target(file_id)
712
ie.revision = revision_id
714
if kind in ('directory', 'symlink'):
715
ie.text_size, ie.text_sha1 = None, None
717
ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
718
if (ie.text_size is None) and (kind == 'file'):
719
raise BzrError('Got a text_size of None for file_id %r' % file_id)
722
sorted_entries = self.sorted_path_id()
723
for path, file_id in sorted_entries:
728
# Have to overload the inherited inventory property
729
# because _get_inventory is only called in the parent.
730
# Reading the docs, property() objects do not use
731
# overloading, they use the function as it was defined
733
inventory = property(_get_inventory)
736
for path, entry in self.inventory.iter_entries():
739
def sorted_path_id(self):
741
for result in self._new_id.iteritems():
743
for id in self.base_tree:
744
path = self.id2path(id)
747
paths.append((path, id))
752
def patched_file(file_patch, original):
753
"""Produce a file-like object with the patched version of a text"""
754
from bzrlib.patches import iter_patched
755
from bzrlib.iterablefile import IterableFile
757
return IterableFile(())
758
# string.splitlines(True) also splits on '\r', but the iter_patched code
759
# only expects to iterate over '\n' style lines
760
return IterableFile(iter_patched(original,
761
StringIO(file_patch).readlines()))