1
# Copyright (C) 2005-2010 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
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."""
24
24
from bzrlib import (
28
28
from bzrlib.bundle import apply_bundle
29
from bzrlib.errors import (
33
from bzrlib.inventory import (
39
from bzrlib.osutils import sha_string, pathjoin
29
from bzrlib.errors import (TestamentMismatch, BzrError,
30
MalformedHeader, MalformedPatches, NotABundle)
31
from bzrlib.inventory import (Inventory, InventoryEntry,
32
InventoryDirectory, InventoryFile,
34
from bzrlib.osutils import sha_file, sha_string, pathjoin
40
35
from bzrlib.revision import Revision, NULL_REVISION
41
36
from bzrlib.testament import StrictTestament
42
37
from bzrlib.trace import mutter, warning
38
import bzrlib.transport
43
39
from bzrlib.tree import Tree
40
import bzrlib.urlutils
44
41
from bzrlib.xml5 import serializer_v5
80
77
for property in self.properties:
81
78
key_end = property.find(': ')
83
if not property.endswith(':'):
84
raise ValueError(property)
80
assert property.endswith(':')
85
81
key = str(property[:-1])
95
def from_revision(revision):
96
revision_info = RevisionInfo(revision.revision_id)
97
date = timestamp.format_highres_date(revision.timestamp,
99
revision_info.date = date
100
revision_info.timezone = revision.timezone
101
revision_info.timestamp = revision.timestamp
102
revision_info.message = revision.message.split('\n')
103
revision_info.properties = [': '.join(p) for p in
104
revision.properties.iteritems()]
108
91
class BundleInfo(object):
109
92
"""This contains the meta information. Stuff that allows you to
110
93
recreate the revision or inventory XML.
112
def __init__(self, bundle_format=None):
113
self.bundle_format = None
114
96
self.committer = None
116
98
self.message = None
161
143
def get_base(self, revision):
162
144
revision_info = self.get_revision_info(revision.revision_id)
163
145
if revision_info.base_id is not None:
164
return revision_info.base_id
146
if revision_info.base_id == NULL_REVISION:
149
return revision_info.base_id
165
150
if len(revision.parent_ids) == 0:
166
151
# There is no base listed, and
167
152
# the lowest revision doesn't have a parent
168
153
# so this is probably against the empty tree
169
# and thus base truly is NULL_REVISION
154
# and thus base truly is None
172
157
return revision.parent_ids[-1]
194
179
raise KeyError(revision_id)
196
181
def revision_tree(self, repository, revision_id, base=None):
182
revision_id = osutils.safe_revision_id(revision_id)
197
183
revision = self.get_revision(revision_id)
198
184
base = self.get_base(revision)
199
if base == revision_id:
200
raise AssertionError()
185
assert base != revision_id
201
186
if not self._validated_revisions_against_repo:
202
187
self._validate_references_from_repository(repository)
203
188
revision_info = self.get_revision_info(revision_id)
204
189
inventory_revision_id = revision_id
205
bundle_tree = BundleTree(repository.revision_tree(base),
190
bundle_tree = BundleTree(repository.revision_tree(base),
206
191
inventory_revision_id)
207
192
self._update_tree(bundle_tree, revision_id)
209
194
inv = bundle_tree.inventory
210
195
self._validate_inventory(inv, revision_id)
211
self._validate_revision(bundle_tree, revision_id)
196
self._validate_revision(inv, revision_id)
213
198
return bundle_tree
241
226
for rev_info in self.revisions:
242
227
checked[rev_info.revision_id] = True
243
228
add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
245
230
for (rev, rev_info) in zip(self.real_revisions, self.revisions):
246
231
add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
250
235
for revision_id, sha1 in rev_to_sha.iteritems():
251
236
if repository.has_revision(revision_id):
252
testament = StrictTestament.from_revision(repository,
237
testament = StrictTestament.from_revision(repository,
254
239
local_sha1 = self._testament_sha1_from_revision(repository,
256
241
if sha1 != local_sha1:
257
raise BzrError('sha1 mismatch. For revision id {%s}'
242
raise BzrError('sha1 mismatch. For revision id {%s}'
258
243
'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
261
246
elif revision_id not in checked:
262
247
missing[revision_id] = sha1
249
for inv_id, sha1 in inv_to_sha.iteritems():
250
if repository.has_revision(inv_id):
251
# Note: branch.get_inventory_sha1() just returns the value that
252
# is stored in the revision text, and that value may be out
253
# of date. This is bogus, because that means we aren't
254
# validating the actual text, just that we wrote and read the
255
# string. But for now, what the hell.
256
local_sha1 = repository.get_inventory_sha1(inv_id)
257
if sha1 != local_sha1:
258
raise BzrError('sha1 mismatch. For inventory id {%s}'
259
'local: %s, bundle: %s' %
260
(inv_id, local_sha1, sha1))
264
264
if len(missing) > 0:
265
265
# I don't know if this is an error yet
266
266
warning('Not all revision hashes could be validated.'
272
272
"""At this point we should have generated the BundleTree,
273
273
so build up an inventory, and make sure the hashes match.
276
assert inv is not None
275
278
# Now we should have a complete inventory entry.
276
279
s = serializer_v5.write_inventory_to_string(inv)
277
280
sha1 = sha_string(s)
278
281
# Target revision is the last entry in the real_revisions list
279
282
rev = self.get_revision(revision_id)
280
if rev.revision_id != revision_id:
281
raise AssertionError()
283
assert rev.revision_id == revision_id
282
284
if sha1 != rev.inventory_sha1:
283
f = open(',,bogus-inv', 'wb')
285
open(',,bogus-inv', 'wb').write(s)
288
286
warning('Inventory sha hash mismatch for revision %s. %s'
289
287
' != %s' % (revision_id, sha1, rev.inventory_sha1))
291
def _validate_revision(self, tree, revision_id):
289
def _validate_revision(self, inventory, revision_id):
292
290
"""Make sure all revision entries match their checksum."""
294
# This is a mapping from each revision id to its sha hash
292
# This is a mapping from each revision id to it's sha hash
297
295
rev = self.get_revision(revision_id)
298
296
rev_info = self.get_revision_info(revision_id)
299
if not (rev.revision_id == rev_info.revision_id):
300
raise AssertionError()
301
if not (rev.revision_id == revision_id):
302
raise AssertionError()
303
sha1 = self._testament_sha1(rev, tree)
297
assert rev.revision_id == rev_info.revision_id
298
assert rev.revision_id == revision_id
299
sha1 = self._testament_sha1(rev, inventory)
304
300
if sha1 != rev_info.sha1:
305
301
raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
306
302
if rev.revision_id in rev_to_sha1:
334
330
name, value = info_item.split(':', 1)
335
331
except ValueError:
336
raise ValueError('Value %r has no colon' % info_item)
332
raise 'Value %r has no colon' % info_item
337
333
if name == 'last-changed':
338
334
last_changed = value
339
335
elif name == 'executable':
336
assert value in ('yes', 'no'), value
340
337
val = (value == 'yes')
341
338
bundle_tree.note_executable(new_path, val)
342
339
elif name == 'target':
346
343
return last_changed, encoding
348
345
def do_patch(path, lines, encoding):
349
if encoding == 'base64':
346
if encoding is not None:
347
assert encoding == 'base64'
350
348
patch = base64.decodestring(''.join(lines))
351
elif encoding is None:
352
350
patch = ''.join(lines)
354
raise ValueError(encoding)
355
351
bundle_tree.note_patch(path, patch)
357
353
def renamed(kind, extra, lines):
446
442
' (unrecognized action): %r' % action_line)
447
443
valid_actions[action](kind, extra, lines)
449
def install_revisions(self, target_repo, stream_input=True):
450
"""Install revisions and return the target revision
452
:param target_repo: The repository to install into
453
:param stream_input: Ignored by this implementation.
445
def install_revisions(self, target_repo):
446
"""Install revisions and return the target revision"""
455
447
apply_bundle.install_bundle(target_repo, self)
456
448
return self.target
458
def get_merge_request(self, target_repo):
459
"""Provide data for performing a merge
461
Returns suggested base, suggested target, and patch verification status
463
return None, self.target, 'inapplicable'
466
451
class BundleTree(Tree):
468
452
def __init__(self, base_tree, revision_id):
469
453
self.base_tree = base_tree
470
454
self._renamed = {} # Mapping from old_path => new_path
487
471
def note_rename(self, old_path, new_path):
488
472
"""A file/directory has been renamed from old_path => new_path"""
489
if new_path in self._renamed:
490
raise AssertionError(new_path)
491
if old_path in self._renamed_r:
492
raise AssertionError(old_path)
473
assert new_path not in self._renamed
474
assert old_path not in self._renamed_r
493
475
self._renamed[new_path] = old_path
494
476
self._renamed_r[old_path] = new_path
526
508
def old_path(self, new_path):
527
509
"""Get the old_path (path in the base_tree) for the file at new_path"""
528
if new_path[:1] in ('\\', '/'):
529
raise ValueError(new_path)
510
assert new_path[:1] not in ('\\', '/')
530
511
old_path = self._renamed.get(new_path)
531
512
if old_path is not None:
547
528
if old_path in self._renamed_r:
551
532
def new_path(self, old_path):
552
533
"""Get the new_path (path in the target_tree) for the file at old_path
553
534
in the base tree.
555
if old_path[:1] in ('\\', '/'):
556
raise ValueError(old_path)
536
assert old_path[:1] not in ('\\', '/')
557
537
new_path = self._renamed_r.get(old_path)
558
538
if new_path is not None:
613
593
new_path = self.id2path(file_id)
614
594
return self.base_tree.path2id(new_path)
616
596
def get_file(self, file_id):
617
597
"""Return a file-like object containing the new contents of the
618
598
file given by file_id.
629
609
patch_original = None
630
610
file_patch = self.patches.get(self.id2path(file_id))
631
611
if file_patch is None:
632
if (patch_original is None and
612
if (patch_original is None and
633
613
self.get_kind(file_id) == 'directory'):
634
614
return StringIO()
635
if patch_original is None:
636
raise AssertionError("None: %s" % file_id)
615
assert patch_original is not None, "None: %s" % file_id
637
616
return patch_original
639
if file_patch.startswith('\\'):
641
'Malformed patch for %s, %r' % (file_id, file_patch))
618
assert not file_patch.startswith('\\'), \
619
'Malformed patch for %s, %r' % (file_id, file_patch)
642
620
return patched_file(file_patch, patch_original)
644
def get_symlink_target(self, file_id, path=None):
646
path = self.id2path(file_id)
622
def get_symlink_target(self, file_id):
623
new_path = self.id2path(file_id)
648
return self._targets[path]
625
return self._targets[new_path]
650
627
return self.base_tree.get_symlink_target(file_id)
665
642
path = self.id2path(file_id)
666
643
if path in self._last_changed:
667
644
return self._last_changed[path]
668
return self.base_tree.get_file_revision(file_id)
645
return self.base_tree.inventory[file_id].revision
670
647
def get_size_and_sha1(self, file_id):
671
648
"""Return the size and sha1 hash of the given file id.
692
669
This need to be called before ever accessing self.inventory
694
671
from os.path import dirname, basename
673
assert self.base_tree is not None
695
674
base_inv = self.base_tree.inventory
696
675
inv = Inventory(None, self.revision_id)
716
695
ie.executable = self.is_executable(file_id)
717
696
elif kind == 'symlink':
718
697
ie = InventoryLink(file_id, name, parent_id)
719
ie.symlink_target = self.get_symlink_target(file_id, path)
698
ie.symlink_target = self.get_symlink_target(file_id)
720
699
ie.revision = revision_id
701
if kind in ('directory', 'symlink'):
702
ie.text_size, ie.text_sha1 = None, None
723
704
ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
724
if ie.text_size is None:
726
'Got a text_size of None for file_id %r' % file_id)
705
if (ie.text_size is None) and (kind == 'file'):
706
raise BzrError('Got a text_size of None for file_id %r' % file_id)
729
709
sorted_entries = self.sorted_path_id()
743
723
for path, entry in self.inventory.iter_entries():
744
724
yield entry.file_id
746
def list_files(self, include_root=False, from_dir=None, recursive=True):
747
# The only files returned by this are those from the version
752
from_dir_id = inv.path2id(from_dir)
753
if from_dir_id is None:
754
# Directory not versioned
756
entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
757
if inv.root is not None and not include_root and from_dir is None:
758
# skip the root for compatability with the current apis.
760
for path, entry in entries:
761
yield path, 'V', entry.kind, entry.file_id, entry
763
726
def sorted_path_id(self):
765
728
for result in self._new_id.iteritems():
766
729
paths.append(result)
767
for id in self.base_tree.all_file_ids():
730
for id in self.base_tree:
768
731
path = self.id2path(id)