~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-09-29 22:03:03 UTC
  • mfrom: (5416.2.6 jam-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20100929220303-cr95h8iwtggco721
(mbp) Add 'break-lock --force'

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