~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-05-02 04:07:43 UTC
  • mto: (1692.4.1 integration)
  • mto: This revision was merged to the branch mainline in revision 1694.
  • Revision ID: robertc@robertcollins.net-20060502040743-c3eae60f1b9edf1c
Fix knit based push to only perform 2 appends to the target, rather that 2*new-versions.

Show diffs side-by-side

added added

removed removed

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