~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Martin Packman
  • Date: 2011-11-17 13:45:49 UTC
  • mto: This revision was merged to the branch mainline in revision 6271.
  • Revision ID: martin.packman@canonical.com-20111117134549-080e1fhtrzoicexg
Only assert FileExists path in test_transform directory clash tests to avoid stringification fallout

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