/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: Martin Pool
  • Date: 2008-04-24 07:37:55 UTC
  • mto: This revision was merged to the branch mainline in revision 3415.
  • Revision ID: mbp@sourcefrog.net-20080424073755-0804cx05t4qb2672
Make PatchesTester use bzrlib TestCase base

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
        for inv_id, sha1 in inv_to_sha.iteritems():
 
266
            if repository.has_revision(inv_id):
 
267
                # Note: branch.get_inventory_sha1() just returns the value that
 
268
                # is stored in the revision text, and that value may be out
 
269
                # of date. This is bogus, because that means we aren't
 
270
                # validating the actual text, just that we wrote and read the
 
271
                # string. But for now, what the hell.
 
272
                local_sha1 = repository.get_inventory_sha1(inv_id)
 
273
                if sha1 != local_sha1:
 
274
                    raise BzrError('sha1 mismatch. For inventory id {%s}' 
 
275
                                   'local: %s, bundle: %s' % 
 
276
                                   (inv_id, local_sha1, sha1))
 
277
                else:
 
278
                    count += 1
 
279
 
 
280
        if len(missing) > 0:
 
281
            # I don't know if this is an error yet
 
282
            warning('Not all revision hashes could be validated.'
 
283
                    ' Unable validate %d hashes' % len(missing))
 
284
        mutter('Verified %d sha hashes for the bundle.' % count)
 
285
        self._validated_revisions_against_repo = True
 
286
 
 
287
    def _validate_inventory(self, inv, revision_id):
 
288
        """At this point we should have generated the BundleTree,
 
289
        so build up an inventory, and make sure the hashes match.
 
290
        """
 
291
        # Now we should have a complete inventory entry.
 
292
        s = serializer_v5.write_inventory_to_string(inv)
 
293
        sha1 = sha_string(s)
 
294
        # Target revision is the last entry in the real_revisions list
 
295
        rev = self.get_revision(revision_id)
 
296
        if rev.revision_id != revision_id:
 
297
            raise AssertionError()
 
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
        if not (rev.revision_id == rev_info.revision_id):
 
312
            raise AssertionError()
 
313
        if not (rev.revision_id == revision_id):
 
314
            raise AssertionError()
 
315
        sha1 = self._testament_sha1(rev, inventory)
 
316
        if sha1 != rev_info.sha1:
 
317
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
 
318
        if rev.revision_id in rev_to_sha1:
 
319
            raise BzrError('Revision {%s} given twice in the list'
 
320
                    % (rev.revision_id))
 
321
        rev_to_sha1[rev.revision_id] = sha1
 
322
 
 
323
    def _update_tree(self, bundle_tree, revision_id):
 
324
        """This fills out a BundleTree based on the information
 
325
        that was read in.
 
326
 
 
327
        :param bundle_tree: A BundleTree to update with the new information.
 
328
        """
 
329
 
 
330
        def get_rev_id(last_changed, path, kind):
 
331
            if last_changed is not None:
 
332
                # last_changed will be a Unicode string because of how it was
 
333
                # read. Convert it back to utf8.
 
334
                changed_revision_id = osutils.safe_revision_id(last_changed,
 
335
                                                               warn=False)
 
336
            else:
 
337
                changed_revision_id = revision_id
 
338
            bundle_tree.note_last_changed(path, changed_revision_id)
 
339
            return changed_revision_id
 
340
 
 
341
        def extra_info(info, new_path):
 
342
            last_changed = None
 
343
            encoding = None
 
344
            for info_item in info:
 
345
                try:
 
346
                    name, value = info_item.split(':', 1)
 
347
                except ValueError:
 
348
                    raise 'Value %r has no colon' % info_item
 
349
                if name == 'last-changed':
 
350
                    last_changed = value
 
351
                elif name == 'executable':
 
352
                    val = (value == 'yes')
 
353
                    bundle_tree.note_executable(new_path, val)
 
354
                elif name == 'target':
 
355
                    bundle_tree.note_target(new_path, value)
 
356
                elif name == 'encoding':
 
357
                    encoding = value
 
358
            return last_changed, encoding
 
359
 
 
360
        def do_patch(path, lines, encoding):
 
361
            if encoding == 'base64':
 
362
                patch = base64.decodestring(''.join(lines))
 
363
            elif encoding is None:
 
364
                patch =  ''.join(lines)
 
365
            else:
 
366
                raise ValueError(encoding)
 
367
            bundle_tree.note_patch(path, patch)
 
368
 
 
369
        def renamed(kind, extra, lines):
 
370
            info = extra.split(' // ')
 
371
            if len(info) < 2:
 
372
                raise BzrError('renamed action lines need both a from and to'
 
373
                        ': %r' % extra)
 
374
            old_path = info[0]
 
375
            if info[1].startswith('=> '):
 
376
                new_path = info[1][3:]
 
377
            else:
 
378
                new_path = info[1]
 
379
 
 
380
            bundle_tree.note_rename(old_path, new_path)
 
381
            last_modified, encoding = extra_info(info[2:], new_path)
 
382
            revision = get_rev_id(last_modified, new_path, kind)
 
383
            if lines:
 
384
                do_patch(new_path, lines, encoding)
 
385
 
 
386
        def removed(kind, extra, lines):
 
387
            info = extra.split(' // ')
 
388
            if len(info) > 1:
 
389
                # TODO: in the future we might allow file ids to be
 
390
                # given for removed entries
 
391
                raise BzrError('removed action lines should only have the path'
 
392
                        ': %r' % extra)
 
393
            path = info[0]
 
394
            bundle_tree.note_deletion(path)
 
395
 
 
396
        def added(kind, extra, lines):
 
397
            info = extra.split(' // ')
 
398
            if len(info) <= 1:
 
399
                raise BzrError('add action lines require the path and file id'
 
400
                        ': %r' % extra)
 
401
            elif len(info) > 5:
 
402
                raise BzrError('add action lines have fewer than 5 entries.'
 
403
                        ': %r' % extra)
 
404
            path = info[0]
 
405
            if not info[1].startswith('file-id:'):
 
406
                raise BzrError('The file-id should follow the path for an add'
 
407
                        ': %r' % extra)
 
408
            # This will be Unicode because of how the stream is read. Turn it
 
409
            # back into a utf8 file_id
 
410
            file_id = osutils.safe_file_id(info[1][8:], warn=False)
 
411
 
 
412
            bundle_tree.note_id(file_id, path, kind)
 
413
            # this will be overridden in extra_info if executable is specified.
 
414
            bundle_tree.note_executable(path, False)
 
415
            last_changed, encoding = extra_info(info[2:], path)
 
416
            revision = get_rev_id(last_changed, path, kind)
 
417
            if kind == 'directory':
 
418
                return
 
419
            do_patch(path, lines, encoding)
 
420
 
 
421
        def modified(kind, extra, lines):
 
422
            info = extra.split(' // ')
 
423
            if len(info) < 1:
 
424
                raise BzrError('modified action lines have at least'
 
425
                        'the path in them: %r' % extra)
 
426
            path = info[0]
 
427
 
 
428
            last_modified, encoding = extra_info(info[1:], path)
 
429
            revision = get_rev_id(last_modified, path, kind)
 
430
            if lines:
 
431
                do_patch(path, lines, encoding)
 
432
            
 
433
        valid_actions = {
 
434
            'renamed':renamed,
 
435
            'removed':removed,
 
436
            'added':added,
 
437
            'modified':modified
 
438
        }
 
439
        for action_line, lines in \
 
440
            self.get_revision_info(revision_id).tree_actions:
 
441
            first = action_line.find(' ')
 
442
            if first == -1:
 
443
                raise BzrError('Bogus action line'
 
444
                        ' (no opening space): %r' % action_line)
 
445
            second = action_line.find(' ', first+1)
 
446
            if second == -1:
 
447
                raise BzrError('Bogus action line'
 
448
                        ' (missing second space): %r' % action_line)
 
449
            action = action_line[:first]
 
450
            kind = action_line[first+1:second]
 
451
            if kind not in ('file', 'directory', 'symlink'):
 
452
                raise BzrError('Bogus action line'
 
453
                        ' (invalid object kind %r): %r' % (kind, action_line))
 
454
            extra = action_line[second+1:]
 
455
 
 
456
            if action not in valid_actions:
 
457
                raise BzrError('Bogus action line'
 
458
                        ' (unrecognized action): %r' % action_line)
 
459
            valid_actions[action](kind, extra, lines)
 
460
 
 
461
    def install_revisions(self, target_repo, stream_input=True):
 
462
        """Install revisions and return the target revision
 
463
 
 
464
        :param target_repo: The repository to install into
 
465
        :param stream_input: Ignored by this implementation.
 
466
        """
 
467
        apply_bundle.install_bundle(target_repo, self)
 
468
        return self.target
 
469
 
 
470
    def get_merge_request(self, target_repo):
 
471
        """Provide data for performing a merge
 
472
 
 
473
        Returns suggested base, suggested target, and patch verification status
 
474
        """
 
475
        return None, self.target, 'inapplicable'
 
476
 
 
477
 
 
478
class BundleTree(Tree):
 
479
    def __init__(self, base_tree, revision_id):
 
480
        self.base_tree = base_tree
 
481
        self._renamed = {} # Mapping from old_path => new_path
 
482
        self._renamed_r = {} # new_path => old_path
 
483
        self._new_id = {} # new_path => new_id
 
484
        self._new_id_r = {} # new_id => new_path
 
485
        self._kinds = {} # new_id => kind
 
486
        self._last_changed = {} # new_id => revision_id
 
487
        self._executable = {} # new_id => executable value
 
488
        self.patches = {}
 
489
        self._targets = {} # new path => new symlink target
 
490
        self.deleted = []
 
491
        self.contents_by_id = True
 
492
        self.revision_id = revision_id
 
493
        self._inventory = None
 
494
 
 
495
    def __str__(self):
 
496
        return pprint.pformat(self.__dict__)
 
497
 
 
498
    def note_rename(self, old_path, new_path):
 
499
        """A file/directory has been renamed from old_path => new_path"""
 
500
        if new_path in self._renamed:
 
501
            raise AssertionError(new_path)
 
502
        if old_path in self._renamed_r:
 
503
            raise AssertionError(old_path)
 
504
        self._renamed[new_path] = old_path
 
505
        self._renamed_r[old_path] = new_path
 
506
 
 
507
    def note_id(self, new_id, new_path, kind='file'):
 
508
        """Files that don't exist in base need a new id."""
 
509
        self._new_id[new_path] = new_id
 
510
        self._new_id_r[new_id] = new_path
 
511
        self._kinds[new_id] = kind
 
512
 
 
513
    def note_last_changed(self, file_id, revision_id):
 
514
        if (file_id in self._last_changed
 
515
                and self._last_changed[file_id] != revision_id):
 
516
            raise BzrError('Mismatched last-changed revision for file_id {%s}'
 
517
                    ': %s != %s' % (file_id,
 
518
                                    self._last_changed[file_id],
 
519
                                    revision_id))
 
520
        self._last_changed[file_id] = revision_id
 
521
 
 
522
    def note_patch(self, new_path, patch):
 
523
        """There is a patch for a given filename."""
 
524
        self.patches[new_path] = patch
 
525
 
 
526
    def note_target(self, new_path, target):
 
527
        """The symlink at the new path has the given target"""
 
528
        self._targets[new_path] = target
 
529
 
 
530
    def note_deletion(self, old_path):
 
531
        """The file at old_path has been deleted."""
 
532
        self.deleted.append(old_path)
 
533
 
 
534
    def note_executable(self, new_path, executable):
 
535
        self._executable[new_path] = executable
 
536
 
 
537
    def old_path(self, new_path):
 
538
        """Get the old_path (path in the base_tree) for the file at new_path"""
 
539
        if new_path[:1] in ('\\', '/'):
 
540
            raise ValueError(new_path)
 
541
        old_path = self._renamed.get(new_path)
 
542
        if old_path is not None:
 
543
            return old_path
 
544
        dirname,basename = os.path.split(new_path)
 
545
        # dirname is not '' doesn't work, because
 
546
        # dirname may be a unicode entry, and is
 
547
        # requires the objects to be identical
 
548
        if dirname != '':
 
549
            old_dir = self.old_path(dirname)
 
550
            if old_dir is None:
 
551
                old_path = None
 
552
            else:
 
553
                old_path = pathjoin(old_dir, basename)
 
554
        else:
 
555
            old_path = new_path
 
556
        #If the new path wasn't in renamed, the old one shouldn't be in
 
557
        #renamed_r
 
558
        if old_path in self._renamed_r:
 
559
            return None
 
560
        return old_path 
 
561
 
 
562
    def new_path(self, old_path):
 
563
        """Get the new_path (path in the target_tree) for the file at old_path
 
564
        in the base tree.
 
565
        """
 
566
        if old_path[:1] in ('\\', '/'):
 
567
            raise ValueError(old_path)
 
568
        new_path = self._renamed_r.get(old_path)
 
569
        if new_path is not None:
 
570
            return new_path
 
571
        if new_path in self._renamed:
 
572
            return None
 
573
        dirname,basename = os.path.split(old_path)
 
574
        if dirname != '':
 
575
            new_dir = self.new_path(dirname)
 
576
            if new_dir is None:
 
577
                new_path = None
 
578
            else:
 
579
                new_path = pathjoin(new_dir, basename)
 
580
        else:
 
581
            new_path = old_path
 
582
        #If the old path wasn't in renamed, the new one shouldn't be in
 
583
        #renamed_r
 
584
        if new_path in self._renamed:
 
585
            return None
 
586
        return new_path 
 
587
 
 
588
    def path2id(self, path):
 
589
        """Return the id of the file present at path in the target tree."""
 
590
        file_id = self._new_id.get(path)
 
591
        if file_id is not None:
 
592
            return file_id
 
593
        old_path = self.old_path(path)
 
594
        if old_path is None:
 
595
            return None
 
596
        if old_path in self.deleted:
 
597
            return None
 
598
        if getattr(self.base_tree, 'path2id', None) is not None:
 
599
            return self.base_tree.path2id(old_path)
 
600
        else:
 
601
            return self.base_tree.inventory.path2id(old_path)
 
602
 
 
603
    def id2path(self, file_id):
 
604
        """Return the new path in the target tree of the file with id file_id"""
 
605
        path = self._new_id_r.get(file_id)
 
606
        if path is not None:
 
607
            return path
 
608
        old_path = self.base_tree.id2path(file_id)
 
609
        if old_path is None:
 
610
            return None
 
611
        if old_path in self.deleted:
 
612
            return None
 
613
        return self.new_path(old_path)
 
614
 
 
615
    def old_contents_id(self, file_id):
 
616
        """Return the id in the base_tree for the given file_id.
 
617
        Return None if the file did not exist in base.
 
618
        """
 
619
        if self.contents_by_id:
 
620
            if self.base_tree.has_id(file_id):
 
621
                return file_id
 
622
            else:
 
623
                return None
 
624
        new_path = self.id2path(file_id)
 
625
        return self.base_tree.path2id(new_path)
 
626
        
 
627
    def get_file(self, file_id):
 
628
        """Return a file-like object containing the new contents of the
 
629
        file given by file_id.
 
630
 
 
631
        TODO:   It might be nice if this actually generated an entry
 
632
                in the text-store, so that the file contents would
 
633
                then be cached.
 
634
        """
 
635
        base_id = self.old_contents_id(file_id)
 
636
        if (base_id is not None and
 
637
            base_id != self.base_tree.inventory.root.file_id):
 
638
            patch_original = self.base_tree.get_file(base_id)
 
639
        else:
 
640
            patch_original = None
 
641
        file_patch = self.patches.get(self.id2path(file_id))
 
642
        if file_patch is None:
 
643
            if (patch_original is None and 
 
644
                self.get_kind(file_id) == 'directory'):
 
645
                return StringIO()
 
646
            if patch_original is None:
 
647
                raise AssertionError("None: %s" % file_id)
 
648
            return patch_original
 
649
 
 
650
        if file_patch.startswith('\\'):
 
651
            raise ValueError(
 
652
                'Malformed patch for %s, %r' % (file_id, file_patch))
 
653
        return patched_file(file_patch, patch_original)
 
654
 
 
655
    def get_symlink_target(self, file_id):
 
656
        new_path = self.id2path(file_id)
 
657
        try:
 
658
            return self._targets[new_path]
 
659
        except KeyError:
 
660
            return self.base_tree.get_symlink_target(file_id)
 
661
 
 
662
    def get_kind(self, file_id):
 
663
        if file_id in self._kinds:
 
664
            return self._kinds[file_id]
 
665
        return self.base_tree.inventory[file_id].kind
 
666
 
 
667
    def is_executable(self, file_id):
 
668
        path = self.id2path(file_id)
 
669
        if path in self._executable:
 
670
            return self._executable[path]
 
671
        else:
 
672
            return self.base_tree.inventory[file_id].executable
 
673
 
 
674
    def get_last_changed(self, file_id):
 
675
        path = self.id2path(file_id)
 
676
        if path in self._last_changed:
 
677
            return self._last_changed[path]
 
678
        return self.base_tree.inventory[file_id].revision
 
679
 
 
680
    def get_size_and_sha1(self, file_id):
 
681
        """Return the size and sha1 hash of the given file id.
 
682
        If the file was not locally modified, this is extracted
 
683
        from the base_tree. Rather than re-reading the file.
 
684
        """
 
685
        new_path = self.id2path(file_id)
 
686
        if new_path is None:
 
687
            return None, None
 
688
        if new_path not in self.patches:
 
689
            # If the entry does not have a patch, then the
 
690
            # contents must be the same as in the base_tree
 
691
            ie = self.base_tree.inventory[file_id]
 
692
            if ie.text_size is None:
 
693
                return ie.text_size, ie.text_sha1
 
694
            return int(ie.text_size), ie.text_sha1
 
695
        fileobj = self.get_file(file_id)
 
696
        content = fileobj.read()
 
697
        return len(content), sha_string(content)
 
698
 
 
699
    def _get_inventory(self):
 
700
        """Build up the inventory entry for the BundleTree.
 
701
 
 
702
        This need to be called before ever accessing self.inventory
 
703
        """
 
704
        from os.path import dirname, basename
 
705
        base_inv = self.base_tree.inventory
 
706
        inv = Inventory(None, self.revision_id)
 
707
 
 
708
        def add_entry(file_id):
 
709
            path = self.id2path(file_id)
 
710
            if path is None:
 
711
                return
 
712
            if path == '':
 
713
                parent_id = None
 
714
            else:
 
715
                parent_path = dirname(path)
 
716
                parent_id = self.path2id(parent_path)
 
717
 
 
718
            kind = self.get_kind(file_id)
 
719
            revision_id = self.get_last_changed(file_id)
 
720
 
 
721
            name = basename(path)
 
722
            if kind == 'directory':
 
723
                ie = InventoryDirectory(file_id, name, parent_id)
 
724
            elif kind == 'file':
 
725
                ie = InventoryFile(file_id, name, parent_id)
 
726
                ie.executable = self.is_executable(file_id)
 
727
            elif kind == 'symlink':
 
728
                ie = InventoryLink(file_id, name, parent_id)
 
729
                ie.symlink_target = self.get_symlink_target(file_id)
 
730
            ie.revision = revision_id
 
731
 
 
732
            if kind in ('directory', 'symlink'):
 
733
                ie.text_size, ie.text_sha1 = None, None
 
734
            else:
 
735
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
 
736
            if (ie.text_size is None) and (kind == 'file'):
 
737
                raise BzrError('Got a text_size of None for file_id %r' % file_id)
 
738
            inv.add(ie)
 
739
 
 
740
        sorted_entries = self.sorted_path_id()
 
741
        for path, file_id in sorted_entries:
 
742
            add_entry(file_id)
 
743
 
 
744
        return inv
 
745
 
 
746
    # Have to overload the inherited inventory property
 
747
    # because _get_inventory is only called in the parent.
 
748
    # Reading the docs, property() objects do not use
 
749
    # overloading, they use the function as it was defined
 
750
    # at that instant
 
751
    inventory = property(_get_inventory)
 
752
 
 
753
    def __iter__(self):
 
754
        for path, entry in self.inventory.iter_entries():
 
755
            yield entry.file_id
 
756
 
 
757
    def sorted_path_id(self):
 
758
        paths = []
 
759
        for result in self._new_id.iteritems():
 
760
            paths.append(result)
 
761
        for id in self.base_tree:
 
762
            path = self.id2path(id)
 
763
            if path is None:
 
764
                continue
 
765
            paths.append((path, id))
 
766
        paths.sort()
 
767
        return paths
 
768
 
 
769
 
 
770
def patched_file(file_patch, original):
 
771
    """Produce a file-like object with the patched version of a text"""
 
772
    from bzrlib.patches import iter_patched
 
773
    from bzrlib.iterablefile import IterableFile
 
774
    if file_patch == "":
 
775
        return IterableFile(())
 
776
    # string.splitlines(True) also splits on '\r', but the iter_patched code
 
777
    # only expects to iterate over '\n' style lines
 
778
    return IterableFile(iter_patched(original,
 
779
                StringIO(file_patch).readlines()))