~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Alexander Belchenko
  • Date: 2007-04-14 12:17:31 UTC
  • mto: This revision was merged to the branch mainline in revision 2422.
  • Revision ID: bialix@ukr.net-20070414121731-jtc76rfulndihkh3
workingtree_implementations: make usage of symlinks optional

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()))