~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Aaron Bentley
  • Date: 2007-03-06 06:34:19 UTC
  • mto: (2323.6.9 0.15-integration)
  • mto: This revision was merged to the branch mainline in revision 2330.
  • Revision ID: aaron.bentley@utoronto.ca-20070306063419-ils9x2j89l1sz2u4
Get email working, with optional message

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