~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Martin Pool
  • Date: 2008-07-03 10:44:34 UTC
  • mfrom: (3512.3.2 setlocale.mini)
  • mto: This revision was merged to the branch mainline in revision 3518.
  • Revision ID: mbp@sourcefrog.net-20080703104434-v4qgzvxd2wxg8etl
Set locale from environment for third party libs and day of week.

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