~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Aaron Bentley
  • Date: 2008-03-11 14:29:08 UTC
  • mto: This revision was merged to the branch mainline in revision 3264.
  • Revision ID: aaron@aaronbentley.com-20080311142908-yyrvcpn2mldt0fnn
Update documentation to reflect conflict-handling difference

Show diffs side-by-side

added added

removed removed

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