~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Martin Pool
  • Date: 2005-05-12 02:18:48 UTC
  • Revision ID: mbp@sourcefrog.net-20050512021848-d1a727373aee2c85
- WorkingTree loads statcache in constructor and holds
  it permanently

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