/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: 2018-05-06 11:48:54 UTC
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@jelmer.uk-20180506114854-h4qd9ojaqy8wxjsd
Move .mailmap to root.

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