1
# Copyright (C) 2006 by 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
24
from bzrlib.bundle.common import get_header, header_str
26
from bzrlib.errors import (TestamentMismatch, BzrError,
27
MalformedHeader, MalformedPatches, NotABundle)
28
from bzrlib.inventory import (Inventory, InventoryEntry,
29
InventoryDirectory, InventoryFile,
31
from bzrlib.osutils import sha_file, sha_string
32
from bzrlib.revision import Revision, NULL_REVISION
33
from bzrlib.testament import StrictTestament
34
from bzrlib.trace import mutter, warning
35
import bzrlib.transport
36
from bzrlib.tree import Tree
37
import bzrlib.urlutils
38
from bzrlib.xml5 import serializer_v5
41
class RevisionInfo(object):
42
"""Gets filled out for each revision object that is read.
44
def __init__(self, revision_id):
45
self.revision_id = revision_id
51
self.inventory_sha1 = None
53
self.parent_ids = None
56
self.properties = None
57
self.tree_actions = None
60
return pprint.pformat(self.__dict__)
62
def as_revision(self):
63
rev = Revision(revision_id=self.revision_id,
64
committer=self.committer,
65
timestamp=float(self.timestamp),
66
timezone=int(self.timezone),
67
inventory_sha1=self.inventory_sha1,
68
message='\n'.join(self.message))
71
rev.parent_ids.extend(self.parent_ids)
74
for property in self.properties:
75
key_end = property.find(': ')
76
assert key_end is not None
77
key = property[:key_end].encode('utf-8')
78
value = property[key_end+2:].encode('utf-8')
79
rev.properties[key] = value
84
class BundleInfo(object):
85
"""This contains the meta information. Stuff that allows you to
86
recreate the revision or inventory XML.
93
# A list of RevisionInfo objects
96
# The next entries are created during complete_info() and
97
# other post-read functions.
99
# A list of real Revision objects
100
self.real_revisions = []
102
self.timestamp = None
106
return pprint.pformat(self.__dict__)
108
def complete_info(self):
109
"""This makes sure that all information is properly
110
split up, based on the assumptions that can be made
111
when information is missing.
113
from bzrlib.bundle.common import unpack_highres_date
114
# Put in all of the guessable information.
115
if not self.timestamp and self.date:
116
self.timestamp, self.timezone = unpack_highres_date(self.date)
118
self.real_revisions = []
119
for rev in self.revisions:
120
if rev.timestamp is None:
121
if rev.date is not None:
122
rev.timestamp, rev.timezone = \
123
unpack_highres_date(rev.date)
125
rev.timestamp = self.timestamp
126
rev.timezone = self.timezone
127
if rev.message is None and self.message:
128
rev.message = self.message
129
if rev.committer is None and self.committer:
130
rev.committer = self.committer
131
self.real_revisions.append(rev.as_revision())
133
def get_base(self, revision):
134
revision_info = self.get_revision_info(revision.revision_id)
135
if revision_info.base_id is not None:
136
if revision_info.base_id == NULL_REVISION:
139
return revision_info.base_id
140
if len(revision.parent_ids) == 0:
141
# There is no base listed, and
142
# the lowest revision doesn't have a parent
143
# so this is probably against the empty tree
144
# and thus base truly is None
147
return revision.parent_ids[-1]
149
def _get_target(self):
150
"""Return the target revision."""
151
if len(self.real_revisions) > 0:
152
return self.real_revisions[0].revision_id
153
elif len(self.revisions) > 0:
154
return self.revisions[0].revision_id
157
target = property(_get_target, doc='The target revision id')
159
def get_revision(self, revision_id):
160
for r in self.real_revisions:
161
if r.revision_id == revision_id:
163
raise KeyError(revision_id)
165
def get_revision_info(self, revision_id):
166
for r in self.revisions:
167
if r.revision_id == revision_id:
169
raise KeyError(revision_id)
172
class BundleReader(object):
173
"""This class reads in a bundle from a file, and returns
174
a Bundle object, which can then be applied against a tree.
176
def __init__(self, from_file):
177
"""Read in the bundle from the file.
179
:param from_file: A file-like object (must have iterator support).
181
object.__init__(self)
182
self.from_file = iter(from_file)
183
self._next_line = None
185
self.info = BundleInfo()
186
# We put the actual inventory ids in the footer, so that the patch
187
# is easier to read for humans.
188
# Unfortunately, that means we need to read everything before we
189
# can create a proper bundle.
195
while self._next_line is not None:
196
self._read_revision_header()
197
if self._next_line is None:
203
"""Make sure that the information read in makes sense
204
and passes appropriate checksums.
206
# Fill in all the missing blanks for the revisions
207
# and generate the real_revisions list.
208
self.info.complete_info()
210
def _validate_revision(self, inventory, revision_id):
211
"""Make sure all revision entries match their checksum."""
213
# This is a mapping from each revision id to it's sha hash
216
rev = self.info.get_revision(revision_id)
217
rev_info = self.info.get_revision_info(revision_id)
218
assert rev.revision_id == rev_info.revision_id
219
assert rev.revision_id == revision_id
220
sha1 = StrictTestament(rev, inventory).as_sha1()
221
if sha1 != rev_info.sha1:
222
raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
223
if rev_to_sha1.has_key(rev.revision_id):
224
raise BzrError('Revision {%s} given twice in the list'
226
rev_to_sha1[rev.revision_id] = sha1
228
def _validate_references_from_repository(self, repository):
229
"""Now that we have a repository which should have some of the
230
revisions we care about, go through and validate all of them
235
def add_sha(d, revision_id, sha1):
236
if revision_id is None:
238
raise BzrError('A Null revision should always'
239
'have a null sha1 hash')
242
# This really should have been validated as part
243
# of _validate_revisions but lets do it again
244
if sha1 != d[revision_id]:
245
raise BzrError('** Revision %r referenced with 2 different'
246
' sha hashes %s != %s' % (revision_id,
247
sha1, d[revision_id]))
249
d[revision_id] = sha1
251
# All of the contained revisions were checked
252
# in _validate_revisions
254
for rev_info in self.info.revisions:
255
checked[rev_info.revision_id] = True
256
add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
258
for (rev, rev_info) in zip(self.info.real_revisions, self.info.revisions):
259
add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
263
for revision_id, sha1 in rev_to_sha.iteritems():
264
if repository.has_revision(revision_id):
265
testament = StrictTestament.from_revision(repository,
267
local_sha1 = testament.as_sha1()
268
if sha1 != local_sha1:
269
raise BzrError('sha1 mismatch. For revision id {%s}'
270
'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
273
elif revision_id not in checked:
274
missing[revision_id] = sha1
276
for inv_id, sha1 in inv_to_sha.iteritems():
277
if repository.has_revision(inv_id):
278
# Note: branch.get_inventory_sha1() just returns the value that
279
# is stored in the revision text, and that value may be out
280
# of date. This is bogus, because that means we aren't
281
# validating the actual text, just that we wrote and read the
282
# string. But for now, what the hell.
283
local_sha1 = repository.get_inventory_sha1(inv_id)
284
if sha1 != local_sha1:
285
raise BzrError('sha1 mismatch. For inventory id {%s}'
286
'local: %s, bundle: %s' %
287
(inv_id, local_sha1, sha1))
292
# I don't know if this is an error yet
293
warning('Not all revision hashes could be validated.'
294
' Unable validate %d hashes' % len(missing))
295
mutter('Verified %d sha hashes for the bundle.' % count)
297
def _validate_inventory(self, inv, revision_id):
298
"""At this point we should have generated the BundleTree,
299
so build up an inventory, and make sure the hashes match.
302
assert inv is not None
304
# Now we should have a complete inventory entry.
305
s = serializer_v5.write_inventory_to_string(inv)
307
# Target revision is the last entry in the real_revisions list
308
rev = self.info.get_revision(revision_id)
309
assert rev.revision_id == revision_id
310
if sha1 != rev.inventory_sha1:
311
open(',,bogus-inv', 'wb').write(s)
312
warning('Inventory sha hash mismatch for revision %s. %s'
313
' != %s' % (revision_id, sha1, rev.inventory_sha1))
315
def get_bundle(self, repository):
316
"""Return the meta information, and a Bundle tree which can
317
be used to populate the local stores and working tree, respectively.
319
return self.info, self.revision_tree(repository, self.info.target)
321
def revision_tree(self, repository, revision_id, base=None):
322
revision = self.info.get_revision(revision_id)
323
base = self.info.get_base(revision)
324
assert base != revision_id
325
self._validate_references_from_repository(repository)
326
revision_info = self.info.get_revision_info(revision_id)
327
inventory_revision_id = revision_id
328
bundle_tree = BundleTree(repository.revision_tree(base),
329
inventory_revision_id)
330
self._update_tree(bundle_tree, revision_id)
332
inv = bundle_tree.inventory
333
self._validate_inventory(inv, revision_id)
334
self._validate_revision(inv, revision_id)
339
"""yield the next line, but secretly
340
keep 1 extra line for peeking.
342
for line in self.from_file:
343
last = self._next_line
344
self._next_line = line
346
#mutter('yielding line: %r' % last)
348
last = self._next_line
349
self._next_line = None
350
#mutter('yielding line: %r' % last)
353
def _read_header(self):
354
"""Read the bzr header"""
355
header = get_header()
357
for line in self._next():
359
# not all mailers will keep trailing whitespace
362
if (not line.startswith('# ') or not line.endswith('\n')
363
or line[2:-1].decode('utf-8') != header[0]):
364
raise MalformedHeader('Found a header, but it'
365
' was improperly formatted')
366
header.pop(0) # We read this line.
368
break # We found everything.
369
elif (line.startswith('#') and line.endswith('\n')):
370
line = line[1:-1].strip().decode('utf-8')
371
if line[:len(header_str)] == header_str:
372
if line == header[0]:
375
raise MalformedHeader('Found what looks like'
376
' a header, but did not match')
379
raise NotABundle('Did not find an opening header')
381
def _read_revision_header(self):
382
self.info.revisions.append(RevisionInfo(None))
383
for line in self._next():
384
# The bzr header is terminated with a blank line
385
# which does not start with '#'
386
if line is None or line == '\n':
388
self._handle_next(line)
390
def _read_next_entry(self, line, indent=1):
391
"""Read in a key-value pair
393
if not line.startswith('#'):
394
raise MalformedHeader('Bzr header did not start with #')
395
line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
396
if line[:indent] == ' '*indent:
399
return None, None# Ignore blank lines
401
loc = line.find(': ')
406
value = self._read_many(indent=indent+2)
407
elif line[-1:] == ':':
409
value = self._read_many(indent=indent+2)
411
raise MalformedHeader('While looking for key: value pairs,'
412
' did not find the colon %r' % (line))
414
key = key.replace(' ', '_')
415
#mutter('found %s: %s' % (key, value))
418
def _handle_next(self, line):
421
key, value = self._read_next_entry(line, indent=1)
422
mutter('_handle_next %r => %r' % (key, value))
426
revision_info = self.info.revisions[-1]
427
if hasattr(revision_info, key):
428
if getattr(revision_info, key) is None:
429
setattr(revision_info, key, value)
431
raise MalformedHeader('Duplicated Key: %s' % key)
433
# What do we do with a key we don't recognize
434
raise MalformedHeader('Unknown Key: "%s"' % key)
436
def _read_many(self, indent):
437
"""If a line ends with no entry, that means that it should be
438
followed with multiple lines of values.
440
This detects the end of the list, because it will be a line that
441
does not start properly indented.
444
start = '#' + (' '*indent)
446
if self._next_line is None or self._next_line[:len(start)] != start:
449
for line in self._next():
450
values.append(line[len(start):-1].decode('utf-8'))
451
if self._next_line is None or self._next_line[:len(start)] != start:
455
def _read_one_patch(self):
456
"""Read in one patch, return the complete patch, along with
459
:return: action, lines, do_continue
461
#mutter('_read_one_patch: %r' % self._next_line)
462
# Peek and see if there are no patches
463
if self._next_line is None or self._next_line.startswith('#'):
464
return None, [], False
468
for line in self._next():
470
if not line.startswith('==='):
471
raise MalformedPatches('The first line of all patches'
472
' should be a bzr meta line "==="'
474
action = line[4:-1].decode('utf-8')
475
elif line.startswith('... '):
476
action += line[len('... '):-1].decode('utf-8')
478
if (self._next_line is not None and
479
self._next_line.startswith('===')):
480
return action, lines, True
481
elif self._next_line is None or self._next_line.startswith('#'):
482
return action, lines, False
486
elif not line.startswith('... '):
489
return action, lines, False
491
def _read_patches(self):
493
revision_actions = []
495
action, lines, do_continue = self._read_one_patch()
496
if action is not None:
497
revision_actions.append((action, lines))
498
assert self.info.revisions[-1].tree_actions is None
499
self.info.revisions[-1].tree_actions = revision_actions
501
def _read_footer(self):
502
"""Read the rest of the meta information.
504
:param first_line: The previous step iterates past what it
505
can handle. That extra line is given here.
507
for line in self._next():
508
self._handle_next(line)
509
if not self._next_line.startswith('#'):
512
if self._next_line is None:
515
def _update_tree(self, bundle_tree, revision_id):
516
"""This fills out a BundleTree based on the information
519
:param bundle_tree: A BundleTree to update with the new information.
522
def get_rev_id(last_changed, path, kind):
523
if last_changed is not None:
524
changed_revision_id = last_changed.decode('utf-8')
526
changed_revision_id = revision_id
527
bundle_tree.note_last_changed(path, changed_revision_id)
528
return changed_revision_id
530
def extra_info(info, new_path):
533
for info_item in info:
535
name, value = info_item.split(':', 1)
537
raise 'Value %r has no colon' % info_item
538
if name == 'last-changed':
540
elif name == 'executable':
541
assert value in ('yes', 'no'), value
542
val = (value == 'yes')
543
bundle_tree.note_executable(new_path, val)
544
elif name == 'target':
545
bundle_tree.note_target(new_path, value)
546
elif name == 'encoding':
548
return last_changed, encoding
550
def do_patch(path, lines, encoding):
551
if encoding is not None:
552
assert encoding == 'base64'
553
patch = base64.decodestring(''.join(lines))
555
patch = ''.join(lines)
556
bundle_tree.note_patch(path, patch)
558
def renamed(kind, extra, lines):
559
info = extra.split(' // ')
561
raise BzrError('renamed action lines need both a from and to'
564
if info[1].startswith('=> '):
565
new_path = info[1][3:]
569
bundle_tree.note_rename(old_path, new_path)
570
last_modified, encoding = extra_info(info[2:], new_path)
571
revision = get_rev_id(last_modified, new_path, kind)
573
do_patch(new_path, lines, encoding)
575
def removed(kind, extra, lines):
576
info = extra.split(' // ')
578
# TODO: in the future we might allow file ids to be
579
# given for removed entries
580
raise BzrError('removed action lines should only have the path'
583
bundle_tree.note_deletion(path)
585
def added(kind, extra, lines):
586
info = extra.split(' // ')
588
raise BzrError('add action lines require the path and file id'
591
raise BzrError('add action lines have fewer than 5 entries.'
594
if not info[1].startswith('file-id:'):
595
raise BzrError('The file-id should follow the path for an add'
597
file_id = info[1][8:]
599
bundle_tree.note_id(file_id, path, kind)
600
# this will be overridden in extra_info if executable is specified.
601
bundle_tree.note_executable(path, False)
602
last_changed, encoding = extra_info(info[2:], path)
603
revision = get_rev_id(last_changed, path, kind)
604
if kind == 'directory':
606
do_patch(path, lines, encoding)
608
def modified(kind, extra, lines):
609
info = extra.split(' // ')
611
raise BzrError('modified action lines have at least'
612
'the path in them: %r' % extra)
615
last_modified, encoding = extra_info(info[1:], path)
616
revision = get_rev_id(last_modified, path, kind)
618
do_patch(path, lines, encoding)
626
for action_line, lines in \
627
self.info.get_revision_info(revision_id).tree_actions:
628
first = action_line.find(' ')
630
raise BzrError('Bogus action line'
631
' (no opening space): %r' % action_line)
632
second = action_line.find(' ', first+1)
634
raise BzrError('Bogus action line'
635
' (missing second space): %r' % action_line)
636
action = action_line[:first]
637
kind = action_line[first+1:second]
638
if kind not in ('file', 'directory', 'symlink'):
639
raise BzrError('Bogus action line'
640
' (invalid object kind %r): %r' % (kind, action_line))
641
extra = action_line[second+1:]
643
if action not in valid_actions:
644
raise BzrError('Bogus action line'
645
' (unrecognized action): %r' % action_line)
646
valid_actions[action](kind, extra, lines)
649
class BundleTree(Tree):
650
def __init__(self, base_tree, revision_id):
651
self.base_tree = base_tree
652
self._renamed = {} # Mapping from old_path => new_path
653
self._renamed_r = {} # new_path => old_path
654
self._new_id = {} # new_path => new_id
655
self._new_id_r = {} # new_id => new_path
656
self._kinds = {} # new_id => kind
657
self._last_changed = {} # new_id => revision_id
658
self._executable = {} # new_id => executable value
660
self._targets = {} # new path => new symlink target
662
self.contents_by_id = True
663
self.revision_id = revision_id
664
self._inventory = None
667
return pprint.pformat(self.__dict__)
669
def note_rename(self, old_path, new_path):
670
"""A file/directory has been renamed from old_path => new_path"""
671
assert not self._renamed.has_key(new_path)
672
assert not self._renamed_r.has_key(old_path)
673
self._renamed[new_path] = old_path
674
self._renamed_r[old_path] = new_path
676
def note_id(self, new_id, new_path, kind='file'):
677
"""Files that don't exist in base need a new id."""
678
self._new_id[new_path] = new_id
679
self._new_id_r[new_id] = new_path
680
self._kinds[new_id] = kind
682
def note_last_changed(self, file_id, revision_id):
683
if (self._last_changed.has_key(file_id)
684
and self._last_changed[file_id] != revision_id):
685
raise BzrError('Mismatched last-changed revision for file_id {%s}'
686
': %s != %s' % (file_id,
687
self._last_changed[file_id],
689
self._last_changed[file_id] = revision_id
691
def note_patch(self, new_path, patch):
692
"""There is a patch for a given filename."""
693
self.patches[new_path] = patch
695
def note_target(self, new_path, target):
696
"""The symlink at the new path has the given target"""
697
self._targets[new_path] = target
699
def note_deletion(self, old_path):
700
"""The file at old_path has been deleted."""
701
self.deleted.append(old_path)
703
def note_executable(self, new_path, executable):
704
self._executable[new_path] = executable
706
def old_path(self, new_path):
707
"""Get the old_path (path in the base_tree) for the file at new_path"""
708
assert new_path[:1] not in ('\\', '/')
709
old_path = self._renamed.get(new_path)
710
if old_path is not None:
712
dirname,basename = os.path.split(new_path)
713
# dirname is not '' doesn't work, because
714
# dirname may be a unicode entry, and is
715
# requires the objects to be identical
717
old_dir = self.old_path(dirname)
721
old_path = os.path.join(old_dir, basename)
724
#If the new path wasn't in renamed, the old one shouldn't be in
726
if self._renamed_r.has_key(old_path):
730
def new_path(self, old_path):
731
"""Get the new_path (path in the target_tree) for the file at old_path
734
assert old_path[:1] not in ('\\', '/')
735
new_path = self._renamed_r.get(old_path)
736
if new_path is not None:
738
if self._renamed.has_key(new_path):
740
dirname,basename = os.path.split(old_path)
742
new_dir = self.new_path(dirname)
746
new_path = os.path.join(new_dir, basename)
749
#If the old path wasn't in renamed, the new one shouldn't be in
751
if self._renamed.has_key(new_path):
755
def path2id(self, path):
756
"""Return the id of the file present at path in the target tree."""
757
file_id = self._new_id.get(path)
758
if file_id is not None:
760
old_path = self.old_path(path)
763
if old_path in self.deleted:
765
if hasattr(self.base_tree, 'path2id'):
766
return self.base_tree.path2id(old_path)
768
return self.base_tree.inventory.path2id(old_path)
770
def id2path(self, file_id):
771
"""Return the new path in the target tree of the file with id file_id"""
772
path = self._new_id_r.get(file_id)
775
old_path = self.base_tree.id2path(file_id)
778
if old_path in self.deleted:
780
return self.new_path(old_path)
782
def old_contents_id(self, file_id):
783
"""Return the id in the base_tree for the given file_id.
784
Return None if the file did not exist in base.
786
if self.contents_by_id:
787
if self.base_tree.has_id(file_id):
791
new_path = self.id2path(file_id)
792
return self.base_tree.path2id(new_path)
794
def get_file(self, file_id):
795
"""Return a file-like object containing the new contents of the
796
file given by file_id.
798
TODO: It might be nice if this actually generated an entry
799
in the text-store, so that the file contents would
802
base_id = self.old_contents_id(file_id)
803
if base_id is not None:
804
patch_original = self.base_tree.get_file(base_id)
806
patch_original = None
807
file_patch = self.patches.get(self.id2path(file_id))
808
if file_patch is None:
809
if (patch_original is None and
810
self.get_kind(file_id) == 'directory'):
812
assert patch_original is not None, "None: %s" % file_id
813
return patch_original
815
assert not file_patch.startswith('\\'), \
816
'Malformed patch for %s, %r' % (file_id, file_patch)
817
return patched_file(file_patch, patch_original)
819
def get_symlink_target(self, file_id):
820
new_path = self.id2path(file_id)
822
return self._targets[new_path]
824
return self.base_tree.get_symlink_target(file_id)
826
def get_kind(self, file_id):
827
if file_id in self._kinds:
828
return self._kinds[file_id]
829
return self.base_tree.inventory[file_id].kind
831
def is_executable(self, file_id):
832
path = self.id2path(file_id)
833
if path in self._executable:
834
return self._executable[path]
836
return self.base_tree.inventory[file_id].executable
838
def get_last_changed(self, file_id):
839
path = self.id2path(file_id)
840
if path in self._last_changed:
841
return self._last_changed[path]
842
return self.base_tree.inventory[file_id].revision
844
def get_size_and_sha1(self, file_id):
845
"""Return the size and sha1 hash of the given file id.
846
If the file was not locally modified, this is extracted
847
from the base_tree. Rather than re-reading the file.
849
new_path = self.id2path(file_id)
852
if new_path not in self.patches:
853
# If the entry does not have a patch, then the
854
# contents must be the same as in the base_tree
855
ie = self.base_tree.inventory[file_id]
856
if ie.text_size is None:
857
return ie.text_size, ie.text_sha1
858
return int(ie.text_size), ie.text_sha1
859
fileobj = self.get_file(file_id)
860
content = fileobj.read()
861
return len(content), sha_string(content)
863
def _get_inventory(self):
864
"""Build up the inventory entry for the BundleTree.
866
This need to be called before ever accessing self.inventory
868
from os.path import dirname, basename
870
assert self.base_tree is not None
871
base_inv = self.base_tree.inventory
872
root_id = base_inv.root.file_id
874
# New inventories have a unique root_id
875
inv = Inventory(root_id, self.revision_id)
877
inv = Inventory(revision_id=self.revision_id)
879
def add_entry(file_id):
880
path = self.id2path(file_id)
883
parent_path = dirname(path)
884
if parent_path == u'':
887
parent_id = self.path2id(parent_path)
889
kind = self.get_kind(file_id)
890
revision_id = self.get_last_changed(file_id)
892
name = basename(path)
893
if kind == 'directory':
894
ie = InventoryDirectory(file_id, name, parent_id)
896
ie = InventoryFile(file_id, name, parent_id)
897
ie.executable = self.is_executable(file_id)
898
elif kind == 'symlink':
899
ie = InventoryLink(file_id, name, parent_id)
900
ie.symlink_target = self.get_symlink_target(file_id)
901
ie.revision = revision_id
903
if kind in ('directory', 'symlink'):
904
ie.text_size, ie.text_sha1 = None, None
906
ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
907
if (ie.text_size is None) and (kind == 'file'):
908
raise BzrError('Got a text_size of None for file_id %r' % file_id)
911
sorted_entries = self.sorted_path_id()
912
for path, file_id in sorted_entries:
913
if file_id == inv.root.file_id:
919
# Have to overload the inherited inventory property
920
# because _get_inventory is only called in the parent.
921
# Reading the docs, property() objects do not use
922
# overloading, they use the function as it was defined
924
inventory = property(_get_inventory)
927
for path, entry in self.inventory.iter_entries():
930
def sorted_path_id(self):
932
for result in self._new_id.iteritems():
934
for id in self.base_tree:
935
path = self.id2path(id)
938
paths.append((path, id))
943
def patched_file(file_patch, original):
944
"""Produce a file-like object with the patched version of a text"""
945
from bzrlib.patches import iter_patched
946
from bzrlib.iterablefile import IterableFile
948
return IterableFile(())
949
return IterableFile(iter_patched(original, file_patch.splitlines(True)))