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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
"""Read in a bundle stream, and process it into a BundleReader object."""
24
28
import bzrlib.errors
25
from bzrlib.errors import (TestamentMismatch, BzrError,
29
from bzrlib.bundle import apply_bundle
30
from bzrlib.errors import (TestamentMismatch, BzrError,
26
31
MalformedHeader, MalformedPatches, NotABundle)
27
32
from bzrlib.inventory import (Inventory, InventoryEntry,
28
33
InventoryDirectory, InventoryFile,
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)
132
159
def get_base(self, revision):
133
160
revision_info = self.get_revision_info(revision.revision_id)
134
161
if revision_info.base_id is not None:
135
if revision_info.base_id == NULL_REVISION:
138
return revision_info.base_id
162
return revision_info.base_id
139
163
if len(revision.parent_ids) == 0:
140
164
# There is no base listed, and
141
165
# the lowest revision doesn't have a parent
142
166
# so this is probably against the empty tree
143
# and thus base truly is None
167
# and thus base truly is NULL_REVISION
146
170
return revision.parent_ids[-1]
170
194
def revision_tree(self, repository, revision_id, base=None):
171
195
revision = self.get_revision(revision_id)
172
196
base = self.get_base(revision)
173
assert base != revision_id
174
self._validate_references_from_repository(repository)
197
if base == revision_id:
198
raise AssertionError()
199
if not self._validated_revisions_against_repo:
200
self._validate_references_from_repository(repository)
175
201
revision_info = self.get_revision_info(revision_id)
176
202
inventory_revision_id = revision_id
177
bundle_tree = BundleTree(repository.revision_tree(base),
203
bundle_tree = BundleTree(repository.revision_tree(base),
178
204
inventory_revision_id)
179
205
self._update_tree(bundle_tree, revision_id)
213
239
for rev_info in self.revisions:
214
240
checked[rev_info.revision_id] = True
215
241
add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
217
243
for (rev, rev_info) in zip(self.real_revisions, self.revisions):
218
244
add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
222
248
for revision_id, sha1 in rev_to_sha.iteritems():
223
249
if repository.has_revision(revision_id):
224
testament = StrictTestament.from_revision(repository,
250
testament = StrictTestament.from_revision(repository,
226
local_sha1 = testament.as_sha1()
252
local_sha1 = self._testament_sha1_from_revision(repository,
227
254
if sha1 != local_sha1:
228
raise BzrError('sha1 mismatch. For revision id {%s}'
255
raise BzrError('sha1 mismatch. For revision id {%s}'
229
256
'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
232
259
elif revision_id not in checked:
233
260
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
262
if len(missing) > 0:
251
263
# I don't know if this is an error yet
252
264
warning('Not all revision hashes could be validated.'
253
265
' Unable validate %d hashes' % len(missing))
254
266
mutter('Verified %d sha hashes for the bundle.' % count)
267
self._validated_revisions_against_repo = True
256
269
def _validate_inventory(self, inv, revision_id):
257
270
"""At this point we should have generated the BundleTree,
258
271
so build up an inventory, and make sure the hashes match.
261
assert inv is not None
263
273
# Now we should have a complete inventory entry.
264
274
s = serializer_v5.write_inventory_to_string(inv)
265
275
sha1 = sha_string(s)
266
276
# Target revision is the last entry in the real_revisions list
267
277
rev = self.get_revision(revision_id)
268
assert rev.revision_id == revision_id
278
if rev.revision_id != revision_id:
279
raise AssertionError()
269
280
if sha1 != rev.inventory_sha1:
270
281
open(',,bogus-inv', 'wb').write(s)
271
282
warning('Inventory sha hash mismatch for revision %s. %s'
277
288
# This is a mapping from each revision id to it's sha hash
280
291
rev = self.get_revision(revision_id)
281
292
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()
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)
285
298
if sha1 != rev_info.sha1:
286
299
raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
287
if rev_to_sha1.has_key(rev.revision_id):
300
if rev.revision_id in rev_to_sha1:
288
301
raise BzrError('Revision {%s} given twice in the list'
289
302
% (rev.revision_id))
290
303
rev_to_sha1[rev.revision_id] = sha1
299
312
def get_rev_id(last_changed, path, kind):
300
313
if last_changed is not None:
301
changed_revision_id = last_changed.decode('utf-8')
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,
303
319
changed_revision_id = revision_id
304
320
bundle_tree.note_last_changed(path, changed_revision_id)
325
340
return last_changed, encoding
327
342
def do_patch(path, lines, encoding):
328
if encoding is not None:
329
assert encoding == 'base64'
343
if encoding == 'base64':
330
344
patch = base64.decodestring(''.join(lines))
345
elif encoding is None:
332
346
patch = ''.join(lines)
348
raise ValueError(encoding)
333
349
bundle_tree.note_patch(path, patch)
335
351
def renamed(kind, extra, lines):
371
387
if not info[1].startswith('file-id:'):
372
388
raise BzrError('The file-id should follow the path for an add'
374
file_id = info[1][8:]
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)
376
394
bundle_tree.note_id(file_id, path, kind)
377
395
# this will be overridden in extra_info if executable is specified.
422
440
' (unrecognized action): %r' % action_line)
423
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'
426
460
class BundleTree(Tree):
427
461
def __init__(self, base_tree, revision_id):
446
480
def note_rename(self, old_path, new_path):
447
481
"""A file/directory has been renamed from old_path => new_path"""
448
assert not self._renamed.has_key(new_path)
449
assert not self._renamed_r.has_key(old_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)
450
486
self._renamed[new_path] = old_path
451
487
self._renamed_r[old_path] = new_path
457
493
self._kinds[new_id] = kind
459
495
def note_last_changed(self, file_id, revision_id):
460
if (self._last_changed.has_key(file_id)
496
if (file_id in self._last_changed
461
497
and self._last_changed[file_id] != revision_id):
462
498
raise BzrError('Mismatched last-changed revision for file_id {%s}'
463
499
': %s != %s' % (file_id,
483
519
def old_path(self, new_path):
484
520
"""Get the old_path (path in the base_tree) for the file at new_path"""
485
assert new_path[:1] not in ('\\', '/')
521
if new_path[:1] in ('\\', '/'):
522
raise ValueError(new_path)
486
523
old_path = self._renamed.get(new_path)
487
524
if old_path is not None:
500
537
old_path = new_path
501
538
#If the new path wasn't in renamed, the old one shouldn't be in
503
if self._renamed_r.has_key(old_path):
540
if old_path in self._renamed_r:
507
544
def new_path(self, old_path):
508
545
"""Get the new_path (path in the target_tree) for the file at old_path
509
546
in the base tree.
511
assert old_path[:1] not in ('\\', '/')
548
if old_path[:1] in ('\\', '/'):
549
raise ValueError(old_path)
512
550
new_path = self._renamed_r.get(old_path)
513
551
if new_path is not None:
515
if self._renamed.has_key(new_path):
553
if new_path in self._renamed:
517
555
dirname,basename = os.path.split(old_path)
518
556
if dirname != '':
579
617
base_id = self.old_contents_id(file_id)
580
if base_id is not None:
618
if (base_id is not None and
619
base_id != self.base_tree.inventory.root.file_id):
581
620
patch_original = self.base_tree.get_file(base_id)
583
622
patch_original = None
584
623
file_patch = self.patches.get(self.id2path(file_id))
585
624
if file_patch is None:
586
if (patch_original is None and
625
if (patch_original is None and
587
626
self.get_kind(file_id) == 'directory'):
588
627
return StringIO()
589
assert patch_original is not None, "None: %s" % file_id
628
if patch_original is None:
629
raise AssertionError("None: %s" % file_id)
590
630
return patch_original
592
assert not file_patch.startswith('\\'), \
593
'Malformed patch for %s, %r' % (file_id, file_patch)
632
if file_patch.startswith('\\'):
634
'Malformed patch for %s, %r' % (file_id, file_patch))
594
635
return patched_file(file_patch, patch_original)
596
637
def get_symlink_target(self, file_id):
643
684
This need to be called before ever accessing self.inventory
645
686
from os.path import dirname, basename
647
assert self.base_tree is not None
648
687
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)
688
inv = Inventory(None, self.revision_id)
656
690
def add_entry(file_id):
657
691
path = self.id2path(file_id)
660
parent_path = dirname(path)
661
if parent_path == u'':
697
parent_path = dirname(path)
664
698
parent_id = self.path2id(parent_path)
666
700
kind = self.get_kind(file_id)