~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Robert Collins
  • Date: 2007-07-13 16:35:54 UTC
  • mto: (2592.3.3 repository)
  • mto: This revision was merged to the branch mainline in revision 2624.
  • Revision ID: robertc@robertcollins.net-20070713163554-ok2qtnzv6rcbpt3z
Change the missing key interface in index operations to not raise, allowing callers to set policy.

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