~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-06-22 06:37:43 UTC
  • Revision ID: mbp@sourcefrog.net-20050622063743-e395f04c4db8977f
- move old blackbox code from testbzr into bzrlib.selftest.blackbox

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