1# Copyright (C) 2005-2010 Canonical Ltd
2#
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.
7#
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.
12#
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Read in a bundle stream, and process it into a BundleReader object."""
18
19import base64
20from io import BytesIO
21import os
22import pprint
23
24from ... import (
25    cache_utf8,
26    osutils,
27    timestamp,
28    )
29from . import apply_bundle
30from ...errors import (
31    TestamentMismatch,
32    BzrError,
33    NoSuchId,
34    )
35from ..inventory import (
36    Inventory,
37    InventoryDirectory,
38    InventoryFile,
39    InventoryLink,
40    )
41from ..inventorytree import InventoryTree
42from ...osutils import sha_string, sha_strings, pathjoin
43from ...revision import Revision, NULL_REVISION
44from ..testament import StrictTestament
45from ...trace import mutter, warning
46from ...tree import (
47    InterTree,
48    Tree,
49    )
50from ..xml5 import serializer_v5
51
52
53class RevisionInfo(object):
54    """Gets filled out for each revision object that is read.
55    """
56
57    def __init__(self, revision_id):
58        self.revision_id = revision_id
59        self.sha1 = None
60        self.committer = None
61        self.date = None
62        self.timestamp = None
63        self.timezone = None
64        self.inventory_sha1 = None
65
66        self.parent_ids = None
67        self.base_id = None
68        self.message = None
69        self.properties = None
70        self.tree_actions = None
71
72    def __str__(self):
73        return pprint.pformat(self.__dict__)
74
75    def as_revision(self):
76        rev = Revision(revision_id=self.revision_id,
77                       committer=self.committer,
78                       timestamp=float(self.timestamp),
79                       timezone=int(self.timezone),
80                       inventory_sha1=self.inventory_sha1,
81                       message='\n'.join(self.message))
82
83        if self.parent_ids:
84            rev.parent_ids.extend(self.parent_ids)
85
86        if self.properties:
87            for property in self.properties:
88                key_end = property.find(': ')
89                if key_end == -1:
90                    if not property.endswith(':'):
91                        raise ValueError(property)
92                    key = str(property[:-1])
93                    value = ''
94                else:
95                    key = str(property[:key_end])
96                    value = property[key_end + 2:]
97                rev.properties[key] = value
98
99        return rev
100
101    @staticmethod
102    def from_revision(revision):
103        revision_info = RevisionInfo(revision.revision_id)
104        date = timestamp.format_highres_date(revision.timestamp,
105                                             revision.timezone)
106        revision_info.date = date
107        revision_info.timezone = revision.timezone
108        revision_info.timestamp = revision.timestamp
109        revision_info.message = revision.message.split('\n')
110        revision_info.properties = [': '.join(p) for p in
111                                    revision.properties.items()]
112        return revision_info
113
114
115class BundleInfo(object):
116    """This contains the meta information. Stuff that allows you to
117    recreate the revision or inventory XML.
118    """
119
120    def __init__(self, bundle_format=None):
121        self.bundle_format = None
122        self.committer = None
123        self.date = None
124        self.message = None
125
126        # A list of RevisionInfo objects
127        self.revisions = []
128
129        # The next entries are created during complete_info() and
130        # other post-read functions.
131
132        # A list of real Revision objects
133        self.real_revisions = []
134
135        self.timestamp = None
136        self.timezone = None
137
138        # Have we checked the repository yet?
139        self._validated_revisions_against_repo = False
140
141    def __str__(self):
142        return pprint.pformat(self.__dict__)
143
144    def complete_info(self):
145        """This makes sure that all information is properly
146        split up, based on the assumptions that can be made
147        when information is missing.
148        """
149        from breezy.timestamp import unpack_highres_date
150        # Put in all of the guessable information.
151        if not self.timestamp and self.date:
152            self.timestamp, self.timezone = unpack_highres_date(self.date)
153
154        self.real_revisions = []
155        for rev in self.revisions:
156            if rev.timestamp is None:
157                if rev.date is not None:
158                    rev.timestamp, rev.timezone = \
159                        unpack_highres_date(rev.date)
160                else:
161                    rev.timestamp = self.timestamp
162                    rev.timezone = self.timezone
163            if rev.message is None and self.message:
164                rev.message = self.message
165            if rev.committer is None and self.committer:
166                rev.committer = self.committer
167            self.real_revisions.append(rev.as_revision())
168
169    def get_base(self, revision):
170        revision_info = self.get_revision_info(revision.revision_id)
171        if revision_info.base_id is not None:
172            return revision_info.base_id
173        if len(revision.parent_ids) == 0:
174            # There is no base listed, and
175            # the lowest revision doesn't have a parent
176            # so this is probably against the empty tree
177            # and thus base truly is NULL_REVISION
178            return NULL_REVISION
179        else:
180            return revision.parent_ids[-1]
181
182    def _get_target(self):
183        """Return the target revision."""
184        if len(self.real_revisions) > 0:
185            return self.real_revisions[0].revision_id
186        elif len(self.revisions) > 0:
187            return self.revisions[0].revision_id
188        return None
189
190    target = property(_get_target, doc='The target revision id')
191
192    def get_revision(self, revision_id):
193        for r in self.real_revisions:
194            if r.revision_id == revision_id:
195                return r
196        raise KeyError(revision_id)
197
198    def get_revision_info(self, revision_id):
199        for r in self.revisions:
200            if r.revision_id == revision_id:
201                return r
202        raise KeyError(revision_id)
203
204    def revision_tree(self, repository, revision_id, base=None):
205        revision = self.get_revision(revision_id)
206        base = self.get_base(revision)
207        if base == revision_id:
208            raise AssertionError()
209        if not self._validated_revisions_against_repo:
210            self._validate_references_from_repository(repository)
211        revision_info = self.get_revision_info(revision_id)
212        inventory_revision_id = revision_id
213        bundle_tree = BundleTree(repository.revision_tree(base),
214                                 inventory_revision_id)
215        self._update_tree(bundle_tree, revision_id)
216
217        inv = bundle_tree.inventory
218        self._validate_inventory(inv, revision_id)
219        self._validate_revision(bundle_tree, revision_id)
220
221        return bundle_tree
222
223    def _validate_references_from_repository(self, repository):
224        """Now that we have a repository which should have some of the
225        revisions we care about, go through and validate all of them
226        that we can.
227        """
228        rev_to_sha = {}
229        inv_to_sha = {}
230
231        def add_sha(d, revision_id, sha1):
232            if revision_id is None:
233                if sha1 is not None:
234                    raise BzrError('A Null revision should always'
235                                   'have a null sha1 hash')
236                return
237            if revision_id in d:
238                # This really should have been validated as part
239                # of _validate_revisions but lets do it again
240                if sha1 != d[revision_id]:
241                    raise BzrError('** Revision %r referenced with 2 different'
242                                   ' sha hashes %s != %s' % (revision_id,
243                                                             sha1, d[revision_id]))
244            else:
245                d[revision_id] = sha1
246
247        # All of the contained revisions were checked
248        # in _validate_revisions
249        checked = {}
250        for rev_info in self.revisions:
251            checked[rev_info.revision_id] = True
252            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
253
254        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
255            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
256
257        count = 0
258        missing = {}
259        for revision_id, sha1 in rev_to_sha.items():
260            if repository.has_revision(revision_id):
261                testament = StrictTestament.from_revision(repository,
262                                                          revision_id)
263                local_sha1 = self._testament_sha1_from_revision(repository,
264                                                                revision_id)
265                if sha1 != local_sha1:
266                    raise BzrError('sha1 mismatch. For revision id {%s}'
267                                   'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
268                else:
269                    count += 1
270            elif revision_id not in checked:
271                missing[revision_id] = sha1
272
273        if len(missing) > 0:
274            # I don't know if this is an error yet
275            warning('Not all revision hashes could be validated.'
276                    ' Unable validate %d hashes' % len(missing))
277        mutter('Verified %d sha hashes for the bundle.' % count)
278        self._validated_revisions_against_repo = True
279
280    def _validate_inventory(self, inv, revision_id):
281        """At this point we should have generated the BundleTree,
282        so build up an inventory, and make sure the hashes match.
283        """
284        # Now we should have a complete inventory entry.
285        cs = serializer_v5.write_inventory_to_chunks(inv)
286        sha1 = sha_strings(cs)
287        # Target revision is the last entry in the real_revisions list
288        rev = self.get_revision(revision_id)
289        if rev.revision_id != revision_id:
290            raise AssertionError()
291        if sha1 != rev.inventory_sha1:
292            with open(',,bogus-inv', 'wb') as f:
293                f.writelines(cs)
294            warning('Inventory sha hash mismatch for revision %s. %s'
295                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
296
297    def _testament(self, revision, tree):
298        raise NotImplementedError(self._testament)
299
300    def _validate_revision(self, tree, revision_id):
301        """Make sure all revision entries match their checksum."""
302
303        # This is a mapping from each revision id to its sha hash
304        rev_to_sha1 = {}
305
306        rev = self.get_revision(revision_id)
307        rev_info = self.get_revision_info(revision_id)
308        if not (rev.revision_id == rev_info.revision_id):
309            raise AssertionError()
310        if not (rev.revision_id == revision_id):
311            raise AssertionError()
312        testament = self._testament(rev, tree)
313        sha1 = testament.as_sha1()
314        if sha1 != rev_info.sha1:
315            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
316        if rev.revision_id in rev_to_sha1:
317            raise BzrError('Revision {%s} given twice in the list'
318                           % (rev.revision_id))
319        rev_to_sha1[rev.revision_id] = sha1
320
321    def _update_tree(self, bundle_tree, revision_id):
322        """This fills out a BundleTree based on the information
323        that was read in.
324
325        :param bundle_tree: A BundleTree to update with the new information.
326        """
327
328        def get_rev_id(last_changed, path, kind):
329            if last_changed is not None:
330                # last_changed will be a Unicode string because of how it was
331                # read. Convert it back to utf8.
332                changed_revision_id = cache_utf8.encode(last_changed)
333            else:
334                changed_revision_id = revision_id
335            bundle_tree.note_last_changed(path, changed_revision_id)
336            return changed_revision_id
337
338        def extra_info(info, new_path):
339            last_changed = None
340            encoding = None
341            for info_item in info:
342                try:
343                    name, value = info_item.split(':', 1)
344                except ValueError:
345                    raise ValueError('Value %r has no colon' % info_item)
346                if name == 'last-changed':
347                    last_changed = value
348                elif name == 'executable':
349                    val = (value == 'yes')
350                    bundle_tree.note_executable(new_path, val)
351                elif name == 'target':
352                    bundle_tree.note_target(new_path, value)
353                elif name == 'encoding':
354                    encoding = value
355            return last_changed, encoding
356
357        def do_patch(path, lines, encoding):
358            if encoding == 'base64':
359                patch = base64.b64decode(b''.join(lines))
360            elif encoding is None:
361                patch = b''.join(lines)
362            else:
363                raise ValueError(encoding)
364            bundle_tree.note_patch(path, patch)
365
366        def renamed(kind, extra, lines):
367            info = extra.split(' // ')
368            if len(info) < 2:
369                raise BzrError('renamed action lines need both a from and to'
370                               ': %r' % extra)
371            old_path = info[0]
372            if info[1].startswith('=> '):
373                new_path = info[1][3:]
374            else:
375                new_path = info[1]
376
377            bundle_tree.note_rename(old_path, new_path)
378            last_modified, encoding = extra_info(info[2:], new_path)
379            revision = get_rev_id(last_modified, new_path, kind)
380            if lines:
381                do_patch(new_path, lines, encoding)
382
383        def removed(kind, extra, lines):
384            info = extra.split(' // ')
385            if len(info) > 1:
386                # TODO: in the future we might allow file ids to be
387                # given for removed entries
388                raise BzrError('removed action lines should only have the path'
389                               ': %r' % extra)
390            path = info[0]
391            bundle_tree.note_deletion(path)
392
393        def added(kind, extra, lines):
394            info = extra.split(' // ')
395            if len(info) <= 1:
396                raise BzrError('add action lines require the path and file id'
397                               ': %r' % extra)
398            elif len(info) > 5:
399                raise BzrError('add action lines have fewer than 5 entries.'
400                               ': %r' % extra)
401            path = info[0]
402            if not info[1].startswith('file-id:'):
403                raise BzrError('The file-id should follow the path for an add'
404                               ': %r' % extra)
405            # This will be Unicode because of how the stream is read. Turn it
406            # back into a utf8 file_id
407            file_id = cache_utf8.encode(info[1][8:])
408
409            bundle_tree.note_id(file_id, path, kind)
410            # this will be overridden in extra_info if executable is specified.
411            bundle_tree.note_executable(path, False)
412            last_changed, encoding = extra_info(info[2:], path)
413            revision = get_rev_id(last_changed, path, kind)
414            if kind == 'directory':
415                return
416            do_patch(path, lines, encoding)
417
418        def modified(kind, extra, lines):
419            info = extra.split(' // ')
420            if len(info) < 1:
421                raise BzrError('modified action lines have at least'
422                               'the path in them: %r' % extra)
423            path = info[0]
424
425            last_modified, encoding = extra_info(info[1:], path)
426            revision = get_rev_id(last_modified, path, kind)
427            if lines:
428                do_patch(path, lines, encoding)
429
430        valid_actions = {
431            'renamed': renamed,
432            'removed': removed,
433            'added': added,
434            'modified': modified
435        }
436        for action_line, lines in \
437                self.get_revision_info(revision_id).tree_actions:
438            first = action_line.find(' ')
439            if first == -1:
440                raise BzrError('Bogus action line'
441                               ' (no opening space): %r' % action_line)
442            second = action_line.find(' ', first + 1)
443            if second == -1:
444                raise BzrError('Bogus action line'
445                               ' (missing second space): %r' % action_line)
446            action = action_line[:first]
447            kind = action_line[first + 1:second]
448            if kind not in ('file', 'directory', 'symlink'):
449                raise BzrError('Bogus action line'
450                               ' (invalid object kind %r): %r' % (kind, action_line))
451            extra = action_line[second + 1:]
452
453            if action not in valid_actions:
454                raise BzrError('Bogus action line'
455                               ' (unrecognized action): %r' % action_line)
456            valid_actions[action](kind, extra, lines)
457
458    def install_revisions(self, target_repo, stream_input=True):
459        """Install revisions and return the target revision
460
461        :param target_repo: The repository to install into
462        :param stream_input: Ignored by this implementation.
463        """
464        apply_bundle.install_bundle(target_repo, self)
465        return self.target
466
467    def get_merge_request(self, target_repo):
468        """Provide data for performing a merge
469
470        Returns suggested base, suggested target, and patch verification status
471        """
472        return None, self.target, 'inapplicable'
473
474
475class BundleTree(InventoryTree):
476
477    def __init__(self, base_tree, revision_id):
478        self.base_tree = base_tree
479        self._renamed = {}  # Mapping from old_path => new_path
480        self._renamed_r = {}  # new_path => old_path
481        self._new_id = {}  # new_path => new_id
482        self._new_id_r = {}  # new_id => new_path
483        self._kinds = {}  # new_path => kind
484        self._last_changed = {}  # new_id => revision_id
485        self._executable = {}  # new_id => executable value
486        self.patches = {}
487        self._targets = {}  # new path => new symlink target
488        self.deleted = []
489        self.revision_id = revision_id
490        self._inventory = None
491        self._base_inter = InterTree.get(self.base_tree, self)
492
493    def __str__(self):
494        return pprint.pformat(self.__dict__)
495
496    def note_rename(self, old_path, new_path):
497        """A file/directory has been renamed from old_path => new_path"""
498        if new_path in self._renamed:
499            raise AssertionError(new_path)
500        if old_path in self._renamed_r:
501            raise AssertionError(old_path)
502        self._renamed[new_path] = old_path
503        self._renamed_r[old_path] = new_path
504
505    def note_id(self, new_id, new_path, kind='file'):
506        """Files that don't exist in base need a new id."""
507        self._new_id[new_path] = new_id
508        self._new_id_r[new_id] = new_path
509        self._kinds[new_path] = kind
510
511    def note_last_changed(self, file_id, revision_id):
512        if (file_id in self._last_changed
513                and self._last_changed[file_id] != revision_id):
514            raise BzrError('Mismatched last-changed revision for file_id {%s}'
515                           ': %s != %s' % (file_id,
516                                           self._last_changed[file_id],
517                                           revision_id))
518        self._last_changed[file_id] = revision_id
519
520    def note_patch(self, new_path, patch):
521        """There is a patch for a given filename."""
522        self.patches[new_path] = patch
523
524    def note_target(self, new_path, target):
525        """The symlink at the new path has the given target"""
526        self._targets[new_path] = target
527
528    def note_deletion(self, old_path):
529        """The file at old_path has been deleted."""
530        self.deleted.append(old_path)
531
532    def note_executable(self, new_path, executable):
533        self._executable[new_path] = executable
534
535    def old_path(self, new_path):
536        """Get the old_path (path in the base_tree) for the file at new_path"""
537        if new_path[:1] in ('\\', '/'):
538            raise ValueError(new_path)
539        old_path = self._renamed.get(new_path)
540        if old_path is not None:
541            return old_path
542        dirname, basename = os.path.split(new_path)
543        # dirname is not '' doesn't work, because
544        # dirname may be a unicode entry, and is
545        # requires the objects to be identical
546        if dirname != '':
547            old_dir = self.old_path(dirname)
548            if old_dir is None:
549                old_path = None
550            else:
551                old_path = pathjoin(old_dir, basename)
552        else:
553            old_path = new_path
554        # If the new path wasn't in renamed, the old one shouldn't be in
555        # renamed_r
556        if old_path in self._renamed_r:
557            return None
558        return old_path
559
560    def new_path(self, old_path):
561        """Get the new_path (path in the target_tree) for the file at old_path
562        in the base tree.
563        """
564        if old_path[:1] in ('\\', '/'):
565            raise ValueError(old_path)
566        new_path = self._renamed_r.get(old_path)
567        if new_path is not None:
568            return new_path
569        if new_path in self._renamed:
570            return None
571        dirname, basename = os.path.split(old_path)
572        if dirname != '':
573            new_dir = self.new_path(dirname)
574            if new_dir is None:
575                new_path = None
576            else:
577                new_path = pathjoin(new_dir, basename)
578        else:
579            new_path = old_path
580        # If the old path wasn't in renamed, the new one shouldn't be in
581        # renamed_r
582        if new_path in self._renamed:
583            return None
584        return new_path
585
586    def path2id(self, path):
587        """Return the id of the file present at path in the target tree."""
588        file_id = self._new_id.get(path)
589        if file_id is not None:
590            return file_id
591        old_path = self.old_path(path)
592        if old_path is None:
593            return None
594        if old_path in self.deleted:
595            return None
596        return self.base_tree.path2id(old_path)
597
598    def id2path(self, file_id, recurse='down'):
599        """Return the new path in the target tree of the file with id file_id"""
600        path = self._new_id_r.get(file_id)
601        if path is not None:
602            return path
603        old_path = self.base_tree.id2path(file_id, recurse)
604        if old_path is None:
605            raise NoSuchId(file_id, self)
606        if old_path in self.deleted:
607            raise NoSuchId(file_id, self)
608        new_path = self.new_path(old_path)
609        if new_path is None:
610            raise NoSuchId(file_id, self)
611        return new_path
612
613    def get_file(self, path):
614        """Return a file-like object containing the new contents of the
615        file given by file_id.
616
617        TODO:   It might be nice if this actually generated an entry
618                in the text-store, so that the file contents would
619                then be cached.
620        """
621        old_path = self._base_inter.find_source_path(path)
622        if old_path is None:
623            patch_original = None
624        else:
625            patch_original = self.base_tree.get_file(old_path)
626        file_patch = self.patches.get(path)
627        if file_patch is None:
628            if (patch_original is None and
629                    self.kind(path) == 'directory'):
630                return BytesIO()
631            if patch_original is None:
632                raise AssertionError("None: %s" % file_id)
633            return patch_original
634
635        if file_patch.startswith(b'\\'):
636            raise ValueError(
637                'Malformed patch for %s, %r' % (file_id, file_patch))
638        return patched_file(file_patch, patch_original)
639
640    def get_symlink_target(self, path):
641        try:
642            return self._targets[path]
643        except KeyError:
644            old_path = self.old_path(path)
645            return self.base_tree.get_symlink_target(old_path)
646
647    def kind(self, path):
648        try:
649            return self._kinds[path]
650        except KeyError:
651            old_path = self.old_path(path)
652            return self.base_tree.kind(old_path)
653
654    def get_file_revision(self, path):
655        if path in self._last_changed:
656            return self._last_changed[path]
657        else:
658            old_path = self.old_path(path)
659            return self.base_tree.get_file_revision(old_path)
660
661    def is_executable(self, path):
662        if path in self._executable:
663            return self._executable[path]
664        else:
665            old_path = self.old_path(path)
666            return self.base_tree.is_executable(old_path)
667
668    def get_last_changed(self, path):
669        if path in self._last_changed:
670            return self._last_changed[path]
671        old_path = self.old_path(path)
672        return self.base_tree.get_file_revision(old_path)
673
674    def get_size_and_sha1(self, new_path):
675        """Return the size and sha1 hash of the given file id.
676        If the file was not locally modified, this is extracted
677        from the base_tree. Rather than re-reading the file.
678        """
679        if new_path is None:
680            return None, None
681        if new_path not in self.patches:
682            # If the entry does not have a patch, then the
683            # contents must be the same as in the base_tree
684            base_path = self.old_path(new_path)
685            text_size = self.base_tree.get_file_size(base_path)
686            text_sha1 = self.base_tree.get_file_sha1(base_path)
687            return text_size, text_sha1
688        fileobj = self.get_file(new_path)
689        content = fileobj.read()
690        return len(content), sha_string(content)
691
692    def _get_inventory(self):
693        """Build up the inventory entry for the BundleTree.
694
695        This need to be called before ever accessing self.inventory
696        """
697        from os.path import dirname, basename
698        inv = Inventory(None, self.revision_id)
699
700        def add_entry(path, file_id):
701            if path == '':
702                parent_id = None
703            else:
704                parent_path = dirname(path)
705                parent_id = self.path2id(parent_path)
706
707            kind = self.kind(path)
708            revision_id = self.get_last_changed(path)
709
710            name = basename(path)
711            if kind == 'directory':
712                ie = InventoryDirectory(file_id, name, parent_id)
713            elif kind == 'file':
714                ie = InventoryFile(file_id, name, parent_id)
715                ie.executable = self.is_executable(path)
716            elif kind == 'symlink':
717                ie = InventoryLink(file_id, name, parent_id)
718                ie.symlink_target = self.get_symlink_target(path)
719            ie.revision = revision_id
720
721            if kind == 'file':
722                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(path)
723                if ie.text_size is None:
724                    raise BzrError(
725                        'Got a text_size of None for file_id %r' % file_id)
726            inv.add(ie)
727
728        sorted_entries = self.sorted_path_id()
729        for path, file_id in sorted_entries:
730            add_entry(path, file_id)
731
732        return inv
733
734    # Have to overload the inherited inventory property
735    # because _get_inventory is only called in the parent.
736    # Reading the docs, property() objects do not use
737    # overloading, they use the function as it was defined
738    # at that instant
739    inventory = property(_get_inventory)
740
741    root_inventory = property(_get_inventory)
742
743    def all_file_ids(self):
744        return {entry.file_id for path, entry in self.inventory.iter_entries()}
745
746    def all_versioned_paths(self):
747        return {path for path, entry in self.inventory.iter_entries()}
748
749    def list_files(self, include_root=False, from_dir=None, recursive=True):
750        # The only files returned by this are those from the version
751        inv = self.inventory
752        if from_dir is None:
753            from_dir_id = None
754        else:
755            from_dir_id = inv.path2id(from_dir)
756            if from_dir_id is None:
757                # Directory not versioned
758                return
759        entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
760        if inv.root is not None and not include_root and from_dir is None:
761            # skip the root for compatibility with the current apis.
762            next(entries)
763        for path, entry in entries:
764            yield path, 'V', entry.kind, entry
765
766    def sorted_path_id(self):
767        paths = []
768        for result in self._new_id.items():
769            paths.append(result)
770        for id in self.base_tree.all_file_ids():
771            try:
772                path = self.id2path(id, recurse='none')
773            except NoSuchId:
774                continue
775            paths.append((path, id))
776        paths.sort()
777        return paths
778
779
780def patched_file(file_patch, original):
781    """Produce a file-like object with the patched version of a text"""
782    from breezy.patches import iter_patched
783    from breezy.iterablefile import IterableFile
784    if file_patch == b"":
785        return IterableFile(())
786    # string.splitlines(True) also splits on '\r', but the iter_patched code
787    # only expects to iterate over '\n' style lines
788    return IterableFile(iter_patched(original,
789                                     BytesIO(file_patch).readlines()))
790