~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: John Arbash Meinel
  • Date: 2007-05-04 18:59:36 UTC
  • mto: This revision was merged to the branch mainline in revision 2643.
  • Revision ID: john@arbash-meinel.com-20070504185936-1mjdoqmtz74xe5mg
A C implementation of _fields_to_entry_0_parents drops the time from 400ms to 330ms for a 21k-entry tree

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