3
Read in a bundle stream, and process it into a BundleReader object.
7
from cStringIO import StringIO
11
from bzrlib.errors import (TestamentMismatch, BzrError,
12
MalformedHeader, MalformedPatches, NotABundle)
13
from bzrlib.bundle.common import get_header, header_str
14
from bzrlib.inventory import (Inventory, InventoryEntry,
15
InventoryDirectory, InventoryFile,
17
from bzrlib.osutils import sha_file, sha_string
18
from bzrlib.revision import Revision, NULL_REVISION
19
from bzrlib.testament import StrictTestament
20
from bzrlib.trace import mutter, warning
21
from bzrlib.tree import Tree
22
from bzrlib.xml5 import serializer_v5
25
class RevisionInfo(object):
26
"""Gets filled out for each revision object that is read.
28
def __init__(self, revision_id):
29
self.revision_id = revision_id
35
self.inventory_sha1 = None
37
self.parent_ids = None
40
self.properties = None
41
self.tree_actions = None
44
return pprint.pformat(self.__dict__)
46
def as_revision(self):
47
rev = Revision(revision_id=self.revision_id,
48
committer=self.committer,
49
timestamp=float(self.timestamp),
50
timezone=int(self.timezone),
51
inventory_sha1=self.inventory_sha1,
52
message='\n'.join(self.message))
55
rev.parent_ids.extend(self.parent_ids)
58
for property in self.properties:
59
key_end = property.find(': ')
60
assert key_end is not None
61
key = property[:key_end].encode('utf-8')
62
value = property[key_end+2:].encode('utf-8')
63
rev.properties[key] = value
68
class BundleInfo(object):
69
"""This contains the meta information. Stuff that allows you to
70
recreate the revision or inventory XML.
77
# A list of RevisionInfo objects
80
# The next entries are created during complete_info() and
81
# other post-read functions.
83
# A list of real Revision objects
84
self.real_revisions = []
90
return pprint.pformat(self.__dict__)
92
def complete_info(self):
93
"""This makes sure that all information is properly
94
split up, based on the assumptions that can be made
95
when information is missing.
97
from bzrlib.bundle.common import unpack_highres_date
98
# Put in all of the guessable information.
99
if not self.timestamp and self.date:
100
self.timestamp, self.timezone = unpack_highres_date(self.date)
102
self.real_revisions = []
103
for rev in self.revisions:
104
if rev.timestamp is None:
105
if rev.date is not None:
106
rev.timestamp, rev.timezone = \
107
unpack_highres_date(rev.date)
109
rev.timestamp = self.timestamp
110
rev.timezone = self.timezone
111
if rev.message is None and self.message:
112
rev.message = self.message
113
if rev.committer is None and self.committer:
114
rev.committer = self.committer
115
self.real_revisions.append(rev.as_revision())
117
def get_base(self, revision):
118
revision_info = self.get_revision_info(revision.revision_id)
119
if revision_info.base_id is not None:
120
if revision_info.base_id == NULL_REVISION:
123
return revision_info.base_id
124
if len(revision.parent_ids) == 0:
125
# There is no base listed, and
126
# the lowest revision doesn't have a parent
127
# so this is probably against the empty tree
128
# and thus base truly is None
131
return revision.parent_ids[-1]
133
def _get_target(self):
134
"""Return the target revision."""
135
if len(self.real_revisions) > 0:
136
return self.real_revisions[0].revision_id
137
elif len(self.revisions) > 0:
138
return self.revisions[0].revision_id
141
target = property(_get_target, doc='The target revision id')
143
def get_revision(self, revision_id):
144
for r in self.real_revisions:
145
if r.revision_id == revision_id:
147
raise KeyError(revision_id)
149
def get_revision_info(self, revision_id):
150
for r in self.revisions:
151
if r.revision_id == revision_id:
153
raise KeyError(revision_id)
156
class BundleReader(object):
157
"""This class reads in a bundle from a file, and returns
158
a Bundle object, which can then be applied against a tree.
160
def __init__(self, from_file):
161
"""Read in the bundle from the file.
163
:param from_file: A file-like object (must have iterator support).
165
object.__init__(self)
166
self.from_file = iter(from_file)
167
self._next_line = None
169
self.info = BundleInfo()
170
# We put the actual inventory ids in the footer, so that the patch
171
# is easier to read for humans.
172
# Unfortunately, that means we need to read everything before we
173
# can create a proper bundle.
179
while self._next_line is not None:
180
self._read_revision_header()
181
if self._next_line is None:
187
"""Make sure that the information read in makes sense
188
and passes appropriate checksums.
190
# Fill in all the missing blanks for the revisions
191
# and generate the real_revisions list.
192
self.info.complete_info()
194
def _validate_revision(self, inventory, revision_id):
195
"""Make sure all revision entries match their checksum."""
197
# This is a mapping from each revision id to it's sha hash
200
rev = self.info.get_revision(revision_id)
201
rev_info = self.info.get_revision_info(revision_id)
202
assert rev.revision_id == rev_info.revision_id
203
assert rev.revision_id == revision_id
204
sha1 = StrictTestament(rev, inventory).as_sha1()
205
if sha1 != rev_info.sha1:
206
raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
207
if rev_to_sha1.has_key(rev.revision_id):
208
raise BzrError('Revision {%s} given twice in the list'
210
rev_to_sha1[rev.revision_id] = sha1
212
def _validate_references_from_repository(self, repository):
213
"""Now that we have a repository which should have some of the
214
revisions we care about, go through and validate all of them
219
def add_sha(d, revision_id, sha1):
220
if revision_id is None:
222
raise BzrError('A Null revision should always'
223
'have a null sha1 hash')
226
# This really should have been validated as part
227
# of _validate_revisions but lets do it again
228
if sha1 != d[revision_id]:
229
raise BzrError('** Revision %r referenced with 2 different'
230
' sha hashes %s != %s' % (revision_id,
231
sha1, d[revision_id]))
233
d[revision_id] = sha1
235
# All of the contained revisions were checked
236
# in _validate_revisions
238
for rev_info in self.info.revisions:
239
checked[rev_info.revision_id] = True
240
add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
242
for (rev, rev_info) in zip(self.info.real_revisions, self.info.revisions):
243
add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
247
for revision_id, sha1 in rev_to_sha.iteritems():
248
if repository.has_revision(revision_id):
249
testament = StrictTestament.from_revision(repository,
251
local_sha1 = testament.as_sha1()
252
if sha1 != local_sha1:
253
raise BzrError('sha1 mismatch. For revision id {%s}'
254
'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
257
elif revision_id not in checked:
258
missing[revision_id] = sha1
260
for inv_id, sha1 in inv_to_sha.iteritems():
261
if repository.has_revision(inv_id):
262
# Note: branch.get_inventory_sha1() just returns the value that
263
# is stored in the revision text, and that value may be out
264
# of date. This is bogus, because that means we aren't
265
# validating the actual text, just that we wrote and read the
266
# string. But for now, what the hell.
267
local_sha1 = repository.get_inventory_sha1(inv_id)
268
if sha1 != local_sha1:
269
raise BzrError('sha1 mismatch. For inventory id {%s}'
270
'local: %s, bundle: %s' %
271
(inv_id, local_sha1, sha1))
276
# I don't know if this is an error yet
277
warning('Not all revision hashes could be validated.'
278
' Unable validate %d hashes' % len(missing))
279
mutter('Verified %d sha hashes for the bundle.' % count)
281
def _validate_inventory(self, inv, revision_id):
282
"""At this point we should have generated the BundleTree,
283
so build up an inventory, and make sure the hashes match.
286
assert inv is not None
288
# Now we should have a complete inventory entry.
289
s = serializer_v5.write_inventory_to_string(inv)
291
# Target revision is the last entry in the real_revisions list
292
rev = self.info.get_revision(revision_id)
293
assert rev.revision_id == revision_id
294
if sha1 != rev.inventory_sha1:
295
open(',,bogus-inv', 'wb').write(s)
296
warning('Inventory sha hash mismatch for revision %s. %s'
297
' != %s' % (revision_id, sha1, rev.inventory_sha1))
299
def get_bundle(self, repository):
300
"""Return the meta information, and a Bundle tree which can
301
be used to populate the local stores and working tree, respectively.
303
return self.info, self.revision_tree(repository, self.info.target)
305
def revision_tree(self, repository, revision_id, base=None):
306
revision = self.info.get_revision(revision_id)
307
base = self.info.get_base(revision)
308
assert base != revision_id
309
self._validate_references_from_repository(repository)
310
revision_info = self.info.get_revision_info(revision_id)
311
inventory_revision_id = revision_id
312
bundle_tree = BundleTree(repository.revision_tree(base),
313
inventory_revision_id)
314
self._update_tree(bundle_tree, revision_id)
316
inv = bundle_tree.inventory
317
self._validate_inventory(inv, revision_id)
318
self._validate_revision(inv, revision_id)
323
"""yield the next line, but secretly
324
keep 1 extra line for peeking.
326
for line in self.from_file:
327
last = self._next_line
328
self._next_line = line
330
#mutter('yielding line: %r' % last)
332
last = self._next_line
333
self._next_line = None
334
#mutter('yielding line: %r' % last)
337
def _read_header(self):
338
"""Read the bzr header"""
339
header = get_header()
341
for line in self._next():
343
# not all mailers will keep trailing whitespace
346
if (not line.startswith('# ') or not line.endswith('\n')
347
or line[2:-1].decode('utf-8') != header[0]):
348
raise MalformedHeader('Found a header, but it'
349
' was improperly formatted')
350
header.pop(0) # We read this line.
352
break # We found everything.
353
elif (line.startswith('#') and line.endswith('\n')):
354
line = line[1:-1].strip().decode('utf-8')
355
if line[:len(header_str)] == header_str:
356
if line == header[0]:
359
raise MalformedHeader('Found what looks like'
360
' a header, but did not match')
363
raise NotABundle('Did not find an opening header')
365
def _read_revision_header(self):
366
self.info.revisions.append(RevisionInfo(None))
367
for line in self._next():
368
# The bzr header is terminated with a blank line
369
# which does not start with '#'
370
if line is None or line == '\n':
372
self._handle_next(line)
374
def _read_next_entry(self, line, indent=1):
375
"""Read in a key-value pair
377
if not line.startswith('#'):
378
raise MalformedHeader('Bzr header did not start with #')
379
line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
380
if line[:indent] == ' '*indent:
383
return None, None# Ignore blank lines
385
loc = line.find(': ')
390
value = self._read_many(indent=indent+2)
391
elif line[-1:] == ':':
393
value = self._read_many(indent=indent+2)
395
raise MalformedHeader('While looking for key: value pairs,'
396
' did not find the colon %r' % (line))
398
key = key.replace(' ', '_')
399
#mutter('found %s: %s' % (key, value))
402
def _handle_next(self, line):
405
key, value = self._read_next_entry(line, indent=1)
406
mutter('_handle_next %r => %r' % (key, value))
410
revision_info = self.info.revisions[-1]
411
if hasattr(revision_info, key):
412
if getattr(revision_info, key) is None:
413
setattr(revision_info, key, value)
415
raise MalformedHeader('Duplicated Key: %s' % key)
417
# What do we do with a key we don't recognize
418
raise MalformedHeader('Unknown Key: "%s"' % key)
420
def _read_many(self, indent):
421
"""If a line ends with no entry, that means that it should be
422
followed with multiple lines of values.
424
This detects the end of the list, because it will be a line that
425
does not start properly indented.
428
start = '#' + (' '*indent)
430
if self._next_line is None or self._next_line[:len(start)] != start:
433
for line in self._next():
434
values.append(line[len(start):-1].decode('utf-8'))
435
if self._next_line is None or self._next_line[:len(start)] != start:
439
def _read_one_patch(self):
440
"""Read in one patch, return the complete patch, along with
443
:return: action, lines, do_continue
445
#mutter('_read_one_patch: %r' % self._next_line)
446
# Peek and see if there are no patches
447
if self._next_line is None or self._next_line.startswith('#'):
448
return None, [], False
452
for line in self._next():
454
if not line.startswith('==='):
455
raise MalformedPatches('The first line of all patches'
456
' should be a bzr meta line "==="'
458
action = line[4:-1].decode('utf-8')
459
elif line.startswith('... '):
460
action += line[len('... '):-1].decode('utf-8')
462
if (self._next_line is not None and
463
self._next_line.startswith('===')):
464
return action, lines, True
465
elif self._next_line is None or self._next_line.startswith('#'):
466
return action, lines, False
470
elif not line.startswith('... '):
473
return action, lines, False
475
def _read_patches(self):
477
revision_actions = []
479
action, lines, do_continue = self._read_one_patch()
480
if action is not None:
481
revision_actions.append((action, lines))
482
assert self.info.revisions[-1].tree_actions is None
483
self.info.revisions[-1].tree_actions = revision_actions
485
def _read_footer(self):
486
"""Read the rest of the meta information.
488
:param first_line: The previous step iterates past what it
489
can handle. That extra line is given here.
491
for line in self._next():
492
self._handle_next(line)
493
if not self._next_line.startswith('#'):
496
if self._next_line is None:
499
def _update_tree(self, bundle_tree, revision_id):
500
"""This fills out a BundleTree based on the information
503
:param bundle_tree: A BundleTree to update with the new information.
506
def get_rev_id(last_changed, path, kind):
507
if last_changed is not None:
508
changed_revision_id = last_changed.decode('utf-8')
510
changed_revision_id = revision_id
511
bundle_tree.note_last_changed(path, changed_revision_id)
512
return changed_revision_id
514
def extra_info(info, new_path):
517
for info_item in info:
519
name, value = info_item.split(':', 1)
521
raise 'Value %r has no colon' % info_item
522
if name == 'last-changed':
524
elif name == 'executable':
525
assert value in ('yes', 'no'), value
526
val = (value == 'yes')
527
bundle_tree.note_executable(new_path, val)
528
elif name == 'target':
529
bundle_tree.note_target(new_path, value)
530
elif name == 'encoding':
532
return last_changed, encoding
534
def do_patch(path, lines, encoding):
535
if encoding is not None:
536
assert encoding == 'base64'
537
patch = base64.decodestring(''.join(lines))
539
patch = ''.join(lines)
540
bundle_tree.note_patch(path, patch)
542
def renamed(kind, extra, lines):
543
info = extra.split(' // ')
545
raise BzrError('renamed action lines need both a from and to'
548
if info[1].startswith('=> '):
549
new_path = info[1][3:]
553
bundle_tree.note_rename(old_path, new_path)
554
last_modified, encoding = extra_info(info[2:], new_path)
555
revision = get_rev_id(last_modified, new_path, kind)
557
do_patch(new_path, lines, encoding)
559
def removed(kind, extra, lines):
560
info = extra.split(' // ')
562
# TODO: in the future we might allow file ids to be
563
# given for removed entries
564
raise BzrError('removed action lines should only have the path'
567
bundle_tree.note_deletion(path)
569
def added(kind, extra, lines):
570
info = extra.split(' // ')
572
raise BzrError('add action lines require the path and file id'
575
raise BzrError('add action lines have fewer than 5 entries.'
578
if not info[1].startswith('file-id:'):
579
raise BzrError('The file-id should follow the path for an add'
581
file_id = info[1][8:]
583
bundle_tree.note_id(file_id, path, kind)
584
# this will be overridden in extra_info if executable is specified.
585
bundle_tree.note_executable(path, False)
586
last_changed, encoding = extra_info(info[2:], path)
587
revision = get_rev_id(last_changed, path, kind)
588
if kind == 'directory':
590
do_patch(path, lines, encoding)
592
def modified(kind, extra, lines):
593
info = extra.split(' // ')
595
raise BzrError('modified action lines have at least'
596
'the path in them: %r' % extra)
599
last_modified, encoding = extra_info(info[1:], path)
600
revision = get_rev_id(last_modified, path, kind)
602
do_patch(path, lines, encoding)
610
for action_line, lines in \
611
self.info.get_revision_info(revision_id).tree_actions:
612
first = action_line.find(' ')
614
raise BzrError('Bogus action line'
615
' (no opening space): %r' % action_line)
616
second = action_line.find(' ', first+1)
618
raise BzrError('Bogus action line'
619
' (missing second space): %r' % action_line)
620
action = action_line[:first]
621
kind = action_line[first+1:second]
622
if kind not in ('file', 'directory', 'symlink'):
623
raise BzrError('Bogus action line'
624
' (invalid object kind %r): %r' % (kind, action_line))
625
extra = action_line[second+1:]
627
if action not in valid_actions:
628
raise BzrError('Bogus action line'
629
' (unrecognized action): %r' % action_line)
630
valid_actions[action](kind, extra, lines)
633
class BundleTree(Tree):
634
def __init__(self, base_tree, revision_id):
635
self.base_tree = base_tree
636
self._renamed = {} # Mapping from old_path => new_path
637
self._renamed_r = {} # new_path => old_path
638
self._new_id = {} # new_path => new_id
639
self._new_id_r = {} # new_id => new_path
640
self._kinds = {} # new_id => kind
641
self._last_changed = {} # new_id => revision_id
642
self._executable = {} # new_id => executable value
644
self._targets = {} # new path => new symlink target
646
self.contents_by_id = True
647
self.revision_id = revision_id
648
self._inventory = None
651
return pprint.pformat(self.__dict__)
653
def note_rename(self, old_path, new_path):
654
"""A file/directory has been renamed from old_path => new_path"""
655
assert not self._renamed.has_key(new_path)
656
assert not self._renamed_r.has_key(old_path)
657
self._renamed[new_path] = old_path
658
self._renamed_r[old_path] = new_path
660
def note_id(self, new_id, new_path, kind='file'):
661
"""Files that don't exist in base need a new id."""
662
self._new_id[new_path] = new_id
663
self._new_id_r[new_id] = new_path
664
self._kinds[new_id] = kind
666
def note_last_changed(self, file_id, revision_id):
667
if (self._last_changed.has_key(file_id)
668
and self._last_changed[file_id] != revision_id):
669
raise BzrError('Mismatched last-changed revision for file_id {%s}'
670
': %s != %s' % (file_id,
671
self._last_changed[file_id],
673
self._last_changed[file_id] = revision_id
675
def note_patch(self, new_path, patch):
676
"""There is a patch for a given filename."""
677
self.patches[new_path] = patch
679
def note_target(self, new_path, target):
680
"""The symlink at the new path has the given target"""
681
self._targets[new_path] = target
683
def note_deletion(self, old_path):
684
"""The file at old_path has been deleted."""
685
self.deleted.append(old_path)
687
def note_executable(self, new_path, executable):
688
self._executable[new_path] = executable
690
def old_path(self, new_path):
691
"""Get the old_path (path in the base_tree) for the file at new_path"""
692
assert new_path[:1] not in ('\\', '/')
693
old_path = self._renamed.get(new_path)
694
if old_path is not None:
696
dirname,basename = os.path.split(new_path)
697
# dirname is not '' doesn't work, because
698
# dirname may be a unicode entry, and is
699
# requires the objects to be identical
701
old_dir = self.old_path(dirname)
705
old_path = os.path.join(old_dir, basename)
708
#If the new path wasn't in renamed, the old one shouldn't be in
710
if self._renamed_r.has_key(old_path):
714
def new_path(self, old_path):
715
"""Get the new_path (path in the target_tree) for the file at old_path
718
assert old_path[:1] not in ('\\', '/')
719
new_path = self._renamed_r.get(old_path)
720
if new_path is not None:
722
if self._renamed.has_key(new_path):
724
dirname,basename = os.path.split(old_path)
726
new_dir = self.new_path(dirname)
730
new_path = os.path.join(new_dir, basename)
733
#If the old path wasn't in renamed, the new one shouldn't be in
735
if self._renamed.has_key(new_path):
739
def path2id(self, path):
740
"""Return the id of the file present at path in the target tree."""
741
file_id = self._new_id.get(path)
742
if file_id is not None:
744
old_path = self.old_path(path)
747
if old_path in self.deleted:
749
if hasattr(self.base_tree, 'path2id'):
750
return self.base_tree.path2id(old_path)
752
return self.base_tree.inventory.path2id(old_path)
754
def id2path(self, file_id):
755
"""Return the new path in the target tree of the file with id file_id"""
756
path = self._new_id_r.get(file_id)
759
old_path = self.base_tree.id2path(file_id)
762
if old_path in self.deleted:
764
return self.new_path(old_path)
766
def old_contents_id(self, file_id):
767
"""Return the id in the base_tree for the given file_id.
768
Return None if the file did not exist in base.
770
if self.contents_by_id:
771
if self.base_tree.has_id(file_id):
775
new_path = self.id2path(file_id)
776
return self.base_tree.path2id(new_path)
778
def get_file(self, file_id):
779
"""Return a file-like object containing the new contents of the
780
file given by file_id.
782
TODO: It might be nice if this actually generated an entry
783
in the text-store, so that the file contents would
786
base_id = self.old_contents_id(file_id)
787
if base_id is not None:
788
patch_original = self.base_tree.get_file(base_id)
790
patch_original = None
791
file_patch = self.patches.get(self.id2path(file_id))
792
if file_patch is None:
793
if (patch_original is None and
794
self.get_kind(file_id) == 'directory'):
796
assert patch_original is not None, "None: %s" % file_id
797
return patch_original
799
assert not file_patch.startswith('\\'), \
800
'Malformed patch for %s, %r' % (file_id, file_patch)
801
return patched_file(file_patch, patch_original)
803
def get_symlink_target(self, file_id):
804
new_path = self.id2path(file_id)
806
return self._targets[new_path]
808
return self.base_tree.get_symlink_target(file_id)
810
def get_kind(self, file_id):
811
if file_id in self._kinds:
812
return self._kinds[file_id]
813
return self.base_tree.inventory[file_id].kind
815
def is_executable(self, file_id):
816
path = self.id2path(file_id)
817
if path in self._executable:
818
return self._executable[path]
820
return self.base_tree.inventory[file_id].executable
822
def get_last_changed(self, file_id):
823
path = self.id2path(file_id)
824
if path in self._last_changed:
825
return self._last_changed[path]
826
return self.base_tree.inventory[file_id].revision
828
def get_size_and_sha1(self, file_id):
829
"""Return the size and sha1 hash of the given file id.
830
If the file was not locally modified, this is extracted
831
from the base_tree. Rather than re-reading the file.
833
new_path = self.id2path(file_id)
836
if new_path not in self.patches:
837
# If the entry does not have a patch, then the
838
# contents must be the same as in the base_tree
839
ie = self.base_tree.inventory[file_id]
840
if ie.text_size is None:
841
return ie.text_size, ie.text_sha1
842
return int(ie.text_size), ie.text_sha1
843
fileobj = self.get_file(file_id)
844
content = fileobj.read()
845
return len(content), sha_string(content)
847
def _get_inventory(self):
848
"""Build up the inventory entry for the BundleTree.
850
This need to be called before ever accessing self.inventory
852
from os.path import dirname, basename
854
assert self.base_tree is not None
855
base_inv = self.base_tree.inventory
856
root_id = base_inv.root.file_id
858
# New inventories have a unique root_id
859
inv = Inventory(root_id, self.revision_id)
861
inv = Inventory(revision_id=self.revision_id)
863
def add_entry(file_id):
864
path = self.id2path(file_id)
867
parent_path = dirname(path)
868
if parent_path == u'':
871
parent_id = self.path2id(parent_path)
873
kind = self.get_kind(file_id)
874
revision_id = self.get_last_changed(file_id)
876
name = basename(path)
877
if kind == 'directory':
878
ie = InventoryDirectory(file_id, name, parent_id)
880
ie = InventoryFile(file_id, name, parent_id)
881
ie.executable = self.is_executable(file_id)
882
elif kind == 'symlink':
883
ie = InventoryLink(file_id, name, parent_id)
884
ie.symlink_target = self.get_symlink_target(file_id)
885
ie.revision = revision_id
887
if kind in ('directory', 'symlink'):
888
ie.text_size, ie.text_sha1 = None, None
890
ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
891
if (ie.text_size is None) and (kind == 'file'):
892
raise BzrError('Got a text_size of None for file_id %r' % file_id)
895
sorted_entries = self.sorted_path_id()
896
for path, file_id in sorted_entries:
897
if file_id == inv.root.file_id:
903
# Have to overload the inherited inventory property
904
# because _get_inventory is only called in the parent.
905
# Reading the docs, property() objects do not use
906
# overloading, they use the function as it was defined
908
inventory = property(_get_inventory)
911
for path, entry in self.inventory.iter_entries():
914
def sorted_path_id(self):
916
for result in self._new_id.iteritems():
918
for id in self.base_tree:
919
path = self.id2path(id)
922
paths.append((path, id))
927
def patched_file(file_patch, original):
928
"""Produce a file-like object with the patched version of a text"""
929
from bzrlib.patches import iter_patched
930
from bzrlib.iterablefile import IterableFile
932
return IterableFile(())
933
return IterableFile(iter_patched(original, file_patch.splitlines(True)))