~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-01-24 13:03:32 UTC
  • mto: This revision was merged to the branch mainline in revision 2242.
  • Revision ID: bialix@ukr.net-20070124130332-ane2eqz3eqrtm9u1
Use new API for testing

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