/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/bzr/bundle/bundle_data.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2020-01-26 04:46:18 UTC
  • mfrom: (7463.1.3 cf-size)
  • Revision ID: breezy.the.bot@gmail.com-20200126044618-y2p8kxo82sop30bw
Add a size attribute to ContentFactory.

Merged from https://code.launchpad.net/~jelmer/brz/cf-size/+merge/378080

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