/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/bundle/bundle_data.py

  • Committer: Jelmer Vernooij
  • Date: 2019-05-28 23:22:25 UTC
  • mto: This revision was merged to the branch mainline in revision 7303.
  • Revision ID: jelmer@jelmer.uk-20190528232225-xalg131vp3to7a13
Install dulwich from git.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
 
19
from __future__ import absolute_import
 
20
 
 
21
import base64
 
22
from io import BytesIO
 
23
import os
 
24
import pprint
 
25
 
 
26
from .. import (
 
27
    cache_utf8,
 
28
    osutils,
 
29
    timestamp,
 
30
    )
 
31
from . import apply_bundle
 
32
from ..errors import (
 
33
    TestamentMismatch,
 
34
    BzrError,
 
35
    )
 
36
from ..bzr.inventory import (
 
37
    Inventory,
 
38
    InventoryDirectory,
 
39
    InventoryFile,
 
40
    InventoryLink,
 
41
    )
 
42
from ..osutils import sha_string, pathjoin
 
43
from ..revision import Revision, NULL_REVISION
 
44
from ..sixish import (
 
45
    viewitems,
 
46
    )
 
47
from ..bzr.testament import StrictTestament
 
48
from ..trace import mutter, warning
 
49
from ..tree import Tree
 
50
from ..bzr.xml5 import serializer_v5
 
51
 
 
52
 
 
53
class 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
                                    viewitems(revision.properties)]
 
112
        return revision_info
 
113
 
 
114
 
 
115
class 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 viewitems(rev_to_sha):
 
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
        s = serializer_v5.write_inventory_to_string(inv)
 
286
        sha1 = sha_string(s)
 
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.write(s)
 
294
            warning('Inventory sha hash mismatch for revision %s. %s'
 
295
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
 
296
 
 
297
    def _validate_revision(self, tree, revision_id):
 
298
        """Make sure all revision entries match their checksum."""
 
299
 
 
300
        # This is a mapping from each revision id to its sha hash
 
301
        rev_to_sha1 = {}
 
302
 
 
303
        rev = self.get_revision(revision_id)
 
304
        rev_info = self.get_revision_info(revision_id)
 
305
        if not (rev.revision_id == rev_info.revision_id):
 
306
            raise AssertionError()
 
307
        if not (rev.revision_id == revision_id):
 
308
            raise AssertionError()
 
309
        sha1 = self._testament_sha1(rev, tree)
 
310
        if sha1 != rev_info.sha1:
 
311
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
 
312
        if rev.revision_id in rev_to_sha1:
 
313
            raise BzrError('Revision {%s} given twice in the list'
 
314
                           % (rev.revision_id))
 
315
        rev_to_sha1[rev.revision_id] = sha1
 
316
 
 
317
    def _update_tree(self, bundle_tree, revision_id):
 
318
        """This fills out a BundleTree based on the information
 
319
        that was read in.
 
320
 
 
321
        :param bundle_tree: A BundleTree to update with the new information.
 
322
        """
 
323
 
 
324
        def get_rev_id(last_changed, path, kind):
 
325
            if last_changed is not None:
 
326
                # last_changed will be a Unicode string because of how it was
 
327
                # read. Convert it back to utf8.
 
328
                changed_revision_id = cache_utf8.encode(last_changed)
 
329
            else:
 
330
                changed_revision_id = revision_id
 
331
            bundle_tree.note_last_changed(path, changed_revision_id)
 
332
            return changed_revision_id
 
333
 
 
334
        def extra_info(info, new_path):
 
335
            last_changed = None
 
336
            encoding = None
 
337
            for info_item in info:
 
338
                try:
 
339
                    name, value = info_item.split(':', 1)
 
340
                except ValueError:
 
341
                    raise ValueError('Value %r has no colon' % info_item)
 
342
                if name == 'last-changed':
 
343
                    last_changed = value
 
344
                elif name == 'executable':
 
345
                    val = (value == 'yes')
 
346
                    bundle_tree.note_executable(new_path, val)
 
347
                elif name == 'target':
 
348
                    bundle_tree.note_target(new_path, value)
 
349
                elif name == 'encoding':
 
350
                    encoding = value
 
351
            return last_changed, encoding
 
352
 
 
353
        def do_patch(path, lines, encoding):
 
354
            if encoding == 'base64':
 
355
                patch = base64.b64decode(b''.join(lines))
 
356
            elif encoding is None:
 
357
                patch = b''.join(lines)
 
358
            else:
 
359
                raise ValueError(encoding)
 
360
            bundle_tree.note_patch(path, patch)
 
361
 
 
362
        def renamed(kind, extra, lines):
 
363
            info = extra.split(' // ')
 
364
            if len(info) < 2:
 
365
                raise BzrError('renamed action lines need both a from and to'
 
366
                               ': %r' % extra)
 
367
            old_path = info[0]
 
368
            if info[1].startswith('=> '):
 
369
                new_path = info[1][3:]
 
370
            else:
 
371
                new_path = info[1]
 
372
 
 
373
            bundle_tree.note_rename(old_path, new_path)
 
374
            last_modified, encoding = extra_info(info[2:], new_path)
 
375
            revision = get_rev_id(last_modified, new_path, kind)
 
376
            if lines:
 
377
                do_patch(new_path, lines, encoding)
 
378
 
 
379
        def removed(kind, extra, lines):
 
380
            info = extra.split(' // ')
 
381
            if len(info) > 1:
 
382
                # TODO: in the future we might allow file ids to be
 
383
                # given for removed entries
 
384
                raise BzrError('removed action lines should only have the path'
 
385
                               ': %r' % extra)
 
386
            path = info[0]
 
387
            bundle_tree.note_deletion(path)
 
388
 
 
389
        def added(kind, extra, lines):
 
390
            info = extra.split(' // ')
 
391
            if len(info) <= 1:
 
392
                raise BzrError('add action lines require the path and file id'
 
393
                               ': %r' % extra)
 
394
            elif len(info) > 5:
 
395
                raise BzrError('add action lines have fewer than 5 entries.'
 
396
                               ': %r' % extra)
 
397
            path = info[0]
 
398
            if not info[1].startswith('file-id:'):
 
399
                raise BzrError('The file-id should follow the path for an add'
 
400
                               ': %r' % extra)
 
401
            # This will be Unicode because of how the stream is read. Turn it
 
402
            # back into a utf8 file_id
 
403
            file_id = cache_utf8.encode(info[1][8:])
 
404
 
 
405
            bundle_tree.note_id(file_id, path, kind)
 
406
            # this will be overridden in extra_info if executable is specified.
 
407
            bundle_tree.note_executable(path, False)
 
408
            last_changed, encoding = extra_info(info[2:], path)
 
409
            revision = get_rev_id(last_changed, path, kind)
 
410
            if kind == 'directory':
 
411
                return
 
412
            do_patch(path, lines, encoding)
 
413
 
 
414
        def modified(kind, extra, lines):
 
415
            info = extra.split(' // ')
 
416
            if len(info) < 1:
 
417
                raise BzrError('modified action lines have at least'
 
418
                               'the path in them: %r' % extra)
 
419
            path = info[0]
 
420
 
 
421
            last_modified, encoding = extra_info(info[1:], path)
 
422
            revision = get_rev_id(last_modified, path, kind)
 
423
            if lines:
 
424
                do_patch(path, lines, encoding)
 
425
 
 
426
        valid_actions = {
 
427
            'renamed': renamed,
 
428
            'removed': removed,
 
429
            'added': added,
 
430
            'modified': modified
 
431
        }
 
432
        for action_line, lines in \
 
433
                self.get_revision_info(revision_id).tree_actions:
 
434
            first = action_line.find(' ')
 
435
            if first == -1:
 
436
                raise BzrError('Bogus action line'
 
437
                               ' (no opening space): %r' % action_line)
 
438
            second = action_line.find(' ', first + 1)
 
439
            if second == -1:
 
440
                raise BzrError('Bogus action line'
 
441
                               ' (missing second space): %r' % action_line)
 
442
            action = action_line[:first]
 
443
            kind = action_line[first + 1:second]
 
444
            if kind not in ('file', 'directory', 'symlink'):
 
445
                raise BzrError('Bogus action line'
 
446
                               ' (invalid object kind %r): %r' % (kind, action_line))
 
447
            extra = action_line[second + 1:]
 
448
 
 
449
            if action not in valid_actions:
 
450
                raise BzrError('Bogus action line'
 
451
                               ' (unrecognized action): %r' % action_line)
 
452
            valid_actions[action](kind, extra, lines)
 
453
 
 
454
    def install_revisions(self, target_repo, stream_input=True):
 
455
        """Install revisions and return the target revision
 
456
 
 
457
        :param target_repo: The repository to install into
 
458
        :param stream_input: Ignored by this implementation.
 
459
        """
 
460
        apply_bundle.install_bundle(target_repo, self)
 
461
        return self.target
 
462
 
 
463
    def get_merge_request(self, target_repo):
 
464
        """Provide data for performing a merge
 
465
 
 
466
        Returns suggested base, suggested target, and patch verification status
 
467
        """
 
468
        return None, self.target, 'inapplicable'
 
469
 
 
470
 
 
471
class BundleTree(Tree):
 
472
 
 
473
    def __init__(self, base_tree, revision_id):
 
474
        self.base_tree = base_tree
 
475
        self._renamed = {}  # Mapping from old_path => new_path
 
476
        self._renamed_r = {}  # new_path => old_path
 
477
        self._new_id = {}  # new_path => new_id
 
478
        self._new_id_r = {}  # new_id => new_path
 
479
        self._kinds = {}  # new_path => kind
 
480
        self._last_changed = {}  # new_id => revision_id
 
481
        self._executable = {}  # new_id => executable value
 
482
        self.patches = {}
 
483
        self._targets = {}  # new path => new symlink target
 
484
        self.deleted = []
 
485
        self.contents_by_id = True
 
486
        self.revision_id = revision_id
 
487
        self._inventory = None
 
488
 
 
489
    def __str__(self):
 
490
        return pprint.pformat(self.__dict__)
 
491
 
 
492
    def note_rename(self, old_path, new_path):
 
493
        """A file/directory has been renamed from old_path => new_path"""
 
494
        if new_path in self._renamed:
 
495
            raise AssertionError(new_path)
 
496
        if old_path in self._renamed_r:
 
497
            raise AssertionError(old_path)
 
498
        self._renamed[new_path] = old_path
 
499
        self._renamed_r[old_path] = new_path
 
500
 
 
501
    def note_id(self, new_id, new_path, kind='file'):
 
502
        """Files that don't exist in base need a new id."""
 
503
        self._new_id[new_path] = new_id
 
504
        self._new_id_r[new_id] = new_path
 
505
        self._kinds[new_path] = kind
 
506
 
 
507
    def note_last_changed(self, file_id, revision_id):
 
508
        if (file_id in self._last_changed
 
509
                and self._last_changed[file_id] != revision_id):
 
510
            raise BzrError('Mismatched last-changed revision for file_id {%s}'
 
511
                           ': %s != %s' % (file_id,
 
512
                                           self._last_changed[file_id],
 
513
                                           revision_id))
 
514
        self._last_changed[file_id] = revision_id
 
515
 
 
516
    def note_patch(self, new_path, patch):
 
517
        """There is a patch for a given filename."""
 
518
        self.patches[new_path] = patch
 
519
 
 
520
    def note_target(self, new_path, target):
 
521
        """The symlink at the new path has the given target"""
 
522
        self._targets[new_path] = target
 
523
 
 
524
    def note_deletion(self, old_path):
 
525
        """The file at old_path has been deleted."""
 
526
        self.deleted.append(old_path)
 
527
 
 
528
    def note_executable(self, new_path, executable):
 
529
        self._executable[new_path] = executable
 
530
 
 
531
    def old_path(self, new_path):
 
532
        """Get the old_path (path in the base_tree) for the file at new_path"""
 
533
        if new_path[:1] in ('\\', '/'):
 
534
            raise ValueError(new_path)
 
535
        old_path = self._renamed.get(new_path)
 
536
        if old_path is not None:
 
537
            return old_path
 
538
        dirname, basename = os.path.split(new_path)
 
539
        # dirname is not '' doesn't work, because
 
540
        # dirname may be a unicode entry, and is
 
541
        # requires the objects to be identical
 
542
        if dirname != '':
 
543
            old_dir = self.old_path(dirname)
 
544
            if old_dir is None:
 
545
                old_path = None
 
546
            else:
 
547
                old_path = pathjoin(old_dir, basename)
 
548
        else:
 
549
            old_path = new_path
 
550
        # If the new path wasn't in renamed, the old one shouldn't be in
 
551
        # renamed_r
 
552
        if old_path in self._renamed_r:
 
553
            return None
 
554
        return old_path
 
555
 
 
556
    def new_path(self, old_path):
 
557
        """Get the new_path (path in the target_tree) for the file at old_path
 
558
        in the base tree.
 
559
        """
 
560
        if old_path[:1] in ('\\', '/'):
 
561
            raise ValueError(old_path)
 
562
        new_path = self._renamed_r.get(old_path)
 
563
        if new_path is not None:
 
564
            return new_path
 
565
        if new_path in self._renamed:
 
566
            return None
 
567
        dirname, basename = os.path.split(old_path)
 
568
        if dirname != '':
 
569
            new_dir = self.new_path(dirname)
 
570
            if new_dir is None:
 
571
                new_path = None
 
572
            else:
 
573
                new_path = pathjoin(new_dir, basename)
 
574
        else:
 
575
            new_path = old_path
 
576
        # If the old path wasn't in renamed, the new one shouldn't be in
 
577
        # renamed_r
 
578
        if new_path in self._renamed:
 
579
            return None
 
580
        return new_path
 
581
 
 
582
    def get_root_id(self):
 
583
        return self.path2id('')
 
584
 
 
585
    def path2id(self, path):
 
586
        """Return the id of the file present at path in the target tree."""
 
587
        file_id = self._new_id.get(path)
 
588
        if file_id is not None:
 
589
            return file_id
 
590
        old_path = self.old_path(path)
 
591
        if old_path is None:
 
592
            return None
 
593
        if old_path in self.deleted:
 
594
            return None
 
595
        return self.base_tree.path2id(old_path)
 
596
 
 
597
    def id2path(self, file_id):
 
598
        """Return the new path in the target tree of the file with id file_id"""
 
599
        path = self._new_id_r.get(file_id)
 
600
        if path is not None:
 
601
            return path
 
602
        old_path = self.base_tree.id2path(file_id)
 
603
        if old_path is None:
 
604
            return None
 
605
        if old_path in self.deleted:
 
606
            return None
 
607
        return self.new_path(old_path)
 
608
 
 
609
    def old_contents_id(self, file_id):
 
610
        """Return the id in the base_tree for the given file_id.
 
611
        Return None if the file did not exist in base.
 
612
        """
 
613
        if self.contents_by_id:
 
614
            if self.base_tree.has_id(file_id):
 
615
                return file_id
 
616
            else:
 
617
                return None
 
618
        new_path = self.id2path(file_id)
 
619
        return self.base_tree.path2id(new_path)
 
620
 
 
621
    def get_file(self, path):
 
622
        """Return a file-like object containing the new contents of the
 
623
        file given by file_id.
 
624
 
 
625
        TODO:   It might be nice if this actually generated an entry
 
626
                in the text-store, so that the file contents would
 
627
                then be cached.
 
628
        """
 
629
        file_id = self.path2id(path)
 
630
        base_id = self.old_contents_id(file_id)
 
631
        if (base_id is not None and
 
632
                base_id != self.base_tree.get_root_id()):
 
633
            old_path = self.base_tree.id2path(base_id)
 
634
            patch_original = self.base_tree.get_file(old_path)
 
635
        else:
 
636
            patch_original = None
 
637
        file_patch = self.patches.get(path)
 
638
        if file_patch is None:
 
639
            if (patch_original is None and
 
640
                    self.kind(path) == 'directory'):
 
641
                return BytesIO()
 
642
            if patch_original is None:
 
643
                raise AssertionError("None: %s" % file_id)
 
644
            return patch_original
 
645
 
 
646
        if file_patch.startswith(b'\\'):
 
647
            raise ValueError(
 
648
                'Malformed patch for %s, %r' % (file_id, file_patch))
 
649
        return patched_file(file_patch, patch_original)
 
650
 
 
651
    def get_symlink_target(self, path):
 
652
        try:
 
653
            return self._targets[path]
 
654
        except KeyError:
 
655
            old_path = self.old_path(path)
 
656
            return self.base_tree.get_symlink_target(old_path)
 
657
 
 
658
    def kind(self, path):
 
659
        try:
 
660
            return self._kinds[path]
 
661
        except KeyError:
 
662
            old_path = self.old_path(path)
 
663
            return self.base_tree.kind(old_path)
 
664
 
 
665
    def get_file_revision(self, path):
 
666
        if path in self._last_changed:
 
667
            return self._last_changed[path]
 
668
        else:
 
669
            old_path = self.old_path(path)
 
670
            return self.base_tree.get_file_revision(old_path)
 
671
 
 
672
    def is_executable(self, path):
 
673
        if path in self._executable:
 
674
            return self._executable[path]
 
675
        else:
 
676
            old_path = self.old_path(path)
 
677
            return self.base_tree.is_executable(old_path)
 
678
 
 
679
    def get_last_changed(self, path):
 
680
        if path in self._last_changed:
 
681
            return self._last_changed[path]
 
682
        old_path = self.old_path(path)
 
683
        return self.base_tree.get_file_revision(old_path)
 
684
 
 
685
    def get_size_and_sha1(self, new_path):
 
686
        """Return the size and sha1 hash of the given file id.
 
687
        If the file was not locally modified, this is extracted
 
688
        from the base_tree. Rather than re-reading the file.
 
689
        """
 
690
        if new_path is None:
 
691
            return None, None
 
692
        if new_path not in self.patches:
 
693
            # If the entry does not have a patch, then the
 
694
            # contents must be the same as in the base_tree
 
695
            base_path = self.old_path(new_path)
 
696
            text_size = self.base_tree.get_file_size(base_path)
 
697
            text_sha1 = self.base_tree.get_file_sha1(base_path)
 
698
            return text_size, text_sha1
 
699
        fileobj = self.get_file(new_path)
 
700
        content = fileobj.read()
 
701
        return len(content), sha_string(content)
 
702
 
 
703
    def _get_inventory(self):
 
704
        """Build up the inventory entry for the BundleTree.
 
705
 
 
706
        This need to be called before ever accessing self.inventory
 
707
        """
 
708
        from os.path import dirname, basename
 
709
        inv = Inventory(None, self.revision_id)
 
710
 
 
711
        def add_entry(path, file_id):
 
712
            if path == '':
 
713
                parent_id = None
 
714
            else:
 
715
                parent_path = dirname(path)
 
716
                parent_id = self.path2id(parent_path)
 
717
 
 
718
            kind = self.kind(path)
 
719
            revision_id = self.get_last_changed(path)
 
720
 
 
721
            name = basename(path)
 
722
            if kind == 'directory':
 
723
                ie = InventoryDirectory(file_id, name, parent_id)
 
724
            elif kind == 'file':
 
725
                ie = InventoryFile(file_id, name, parent_id)
 
726
                ie.executable = self.is_executable(path)
 
727
            elif kind == 'symlink':
 
728
                ie = InventoryLink(file_id, name, parent_id)
 
729
                ie.symlink_target = self.get_symlink_target(path)
 
730
            ie.revision = revision_id
 
731
 
 
732
            if kind == 'file':
 
733
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(path)
 
734
                if ie.text_size is None:
 
735
                    raise BzrError(
 
736
                        'Got a text_size of None for file_id %r' % file_id)
 
737
            inv.add(ie)
 
738
 
 
739
        sorted_entries = self.sorted_path_id()
 
740
        for path, file_id in sorted_entries:
 
741
            add_entry(path, file_id)
 
742
 
 
743
        return inv
 
744
 
 
745
    # Have to overload the inherited inventory property
 
746
    # because _get_inventory is only called in the parent.
 
747
    # Reading the docs, property() objects do not use
 
748
    # overloading, they use the function as it was defined
 
749
    # at that instant
 
750
    inventory = property(_get_inventory)
 
751
 
 
752
    root_inventory = property(_get_inventory)
 
753
 
 
754
    def all_file_ids(self):
 
755
        return {entry.file_id for path, entry in self.inventory.iter_entries()}
 
756
 
 
757
    def all_versioned_paths(self):
 
758
        return {path for path, entry in self.inventory.iter_entries()}
 
759
 
 
760
    def list_files(self, include_root=False, from_dir=None, recursive=True):
 
761
        # The only files returned by this are those from the version
 
762
        inv = self.inventory
 
763
        if from_dir is None:
 
764
            from_dir_id = None
 
765
        else:
 
766
            from_dir_id = inv.path2id(from_dir)
 
767
            if from_dir_id is None:
 
768
                # Directory not versioned
 
769
                return
 
770
        entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
 
771
        if inv.root is not None and not include_root and from_dir is None:
 
772
            # skip the root for compatibility with the current apis.
 
773
            next(entries)
 
774
        for path, entry in entries:
 
775
            yield path, 'V', entry.kind, entry
 
776
 
 
777
    def sorted_path_id(self):
 
778
        paths = []
 
779
        for result in viewitems(self._new_id):
 
780
            paths.append(result)
 
781
        for id in self.base_tree.all_file_ids():
 
782
            path = self.id2path(id)
 
783
            if path is None:
 
784
                continue
 
785
            paths.append((path, id))
 
786
        paths.sort()
 
787
        return paths
 
788
 
 
789
 
 
790
def patched_file(file_patch, original):
 
791
    """Produce a file-like object with the patched version of a text"""
 
792
    from breezy.patches import iter_patched
 
793
    from breezy.iterablefile import IterableFile
 
794
    if file_patch == b"":
 
795
        return IterableFile(())
 
796
    # string.splitlines(True) also splits on '\r', but the iter_patched code
 
797
    # only expects to iterate over '\n' style lines
 
798
    return IterableFile(iter_patched(original,
 
799
                                     BytesIO(file_patch).readlines()))