~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: 2008-10-31 04:39:04 UTC
  • mfrom: (3565.6.16 switch_nick)
  • Revision ID: pqm@pqm.ubuntu.com-20081031043904-52fnbfrloojemvcc
(mbp) branch nickname documentation

Show diffs side-by-side

added added

removed removed

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