~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Robert Collins
  • Date: 2005-09-30 02:54:51 UTC
  • mfrom: (1395)
  • mto: This revision was merged to the branch mainline in revision 1397.
  • Revision ID: robertc@robertcollins.net-20050930025451-47b9e412202be44b
symlink and weaves, whaddya know

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