~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Robert Collins
  • Date: 2006-04-26 08:21:09 UTC
  • mto: (1686.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 1687.
  • Revision ID: robertc@robertcollins.net-20060426082109-4dcd5b716ac322a1
Add a special cased weaves to knit converter.

Show diffs side-by-side

added added

removed removed

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