/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Aaron Bentley
  • Date: 2007-03-16 15:14:40 UTC
  • mto: This revision was merged to the branch mainline in revision 2389.
  • Revision ID: abentley@panoramicfeedback.com-20070316151440-kwqaumvxjcyiurv2
Change bundle reader and merge directive to both be 'mergeables'

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