~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/read_bundle.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-06-04 10:55:31 UTC
  • mfrom: (1728.2.4 bzr.mbp.integration)
  • Revision ID: pqm@pqm.ubuntu.com-20060604105531-0071ce43b2156b7a
(mbp) several small fixes

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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
"""Read in a bundle stream, and process it into a BundleReader object."""
 
1
#!/usr/bin/env python
 
2
"""\
 
3
Read in a bundle stream, and process it into a BundleReader object.
 
4
"""
18
5
 
19
6
import base64
20
7
from cStringIO import StringIO
21
8
import os
22
9
import pprint
23
10
 
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)
 
11
from bzrlib.errors import (TestamentMismatch, BzrError, 
 
12
                           MalformedHeader, MalformedPatches)
 
13
from bzrlib.bundle.common import get_header, header_str
32
14
from bzrlib.inventory import (Inventory, InventoryEntry,
33
15
                              InventoryDirectory, InventoryFile,
34
16
                              InventoryLink)
35
 
from bzrlib.osutils import sha_file, sha_string, pathjoin
 
17
from bzrlib.osutils import sha_file, sha_string
36
18
from bzrlib.revision import Revision, NULL_REVISION
37
19
from bzrlib.testament import StrictTestament
38
20
from bzrlib.trace import mutter, warning
39
 
import bzrlib.transport
40
21
from bzrlib.tree import Tree
41
 
import bzrlib.urlutils
42
22
from bzrlib.xml5 import serializer_v5
43
23
 
44
24
 
77
57
        if self.properties:
78
58
            for property in self.properties:
79
59
                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:]
 
60
                assert key_end is not None
 
61
                key = property[:key_end].encode('utf-8')
 
62
                value = property[key_end+2:].encode('utf-8')
88
63
                rev.properties[key] = value
89
64
 
90
65
        return rev
91
66
 
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
67
 
106
68
class BundleInfo(object):
107
69
    """This contains the meta information. Stuff that allows you to
108
70
    recreate the revision or inventory XML.
109
71
    """
110
 
    def __init__(self, bundle_format=None):
111
 
        self.bundle_format = None
 
72
    def __init__(self):
112
73
        self.committer = None
113
74
        self.date = None
114
75
        self.message = None
125
86
        self.timestamp = None
126
87
        self.timezone = None
127
88
 
128
 
        # Have we checked the repository yet?
129
 
        self._validated_revisions_against_repo = False
130
 
 
131
89
    def __str__(self):
132
90
        return pprint.pformat(self.__dict__)
133
91
 
136
94
        split up, based on the assumptions that can be made
137
95
        when information is missing.
138
96
        """
139
 
        from bzrlib.timestamp import unpack_highres_date
 
97
        from bzrlib.bundle.common import unpack_highres_date
140
98
        # Put in all of the guessable information.
141
99
        if not self.timestamp and self.date:
142
100
            self.timestamp, self.timezone = unpack_highres_date(self.date)
159
117
    def get_base(self, revision):
160
118
        revision_info = self.get_revision_info(revision.revision_id)
161
119
        if revision_info.base_id is not None:
162
 
            return revision_info.base_id
 
120
            if revision_info.base_id == NULL_REVISION:
 
121
                return None
 
122
            else:
 
123
                return revision_info.base_id
163
124
        if len(revision.parent_ids) == 0:
164
125
            # There is no base listed, and
165
126
            # the lowest revision doesn't have a parent
166
127
            # so this is probably against the empty tree
167
 
            # and thus base truly is NULL_REVISION
168
 
            return NULL_REVISION
 
128
            # and thus base truly is None
 
129
            return None
169
130
        else:
170
131
            return revision.parent_ids[-1]
171
132
 
191
152
                return r
192
153
        raise KeyError(revision_id)
193
154
 
194
 
    def revision_tree(self, repository, revision_id, base=None):
195
 
        revision = self.get_revision(revision_id)
196
 
        base = self.get_base(revision)
197
 
        if base == revision_id:
198
 
            raise AssertionError()
199
 
        if not self._validated_revisions_against_repo:
200
 
            self._validate_references_from_repository(repository)
201
 
        revision_info = self.get_revision_info(revision_id)
202
 
        inventory_revision_id = revision_id
203
 
        bundle_tree = BundleTree(repository.revision_tree(base),
204
 
                                  inventory_revision_id)
205
 
        self._update_tree(bundle_tree, revision_id)
206
 
 
207
 
        inv = bundle_tree.inventory
208
 
        self._validate_inventory(inv, revision_id)
209
 
        self._validate_revision(inv, revision_id)
210
 
 
211
 
        return bundle_tree
 
155
 
 
156
class BundleReader(object):
 
157
    """This class reads in a bundle from a file, and returns
 
158
    a Bundle object, which can then be applied against a tree.
 
159
    """
 
160
    def __init__(self, from_file):
 
161
        """Read in the bundle from the file.
 
162
 
 
163
        :param from_file: A file-like object (must have iterator support).
 
164
        """
 
165
        object.__init__(self)
 
166
        self.from_file = iter(from_file)
 
167
        self._next_line = None
 
168
        
 
169
        self.info = BundleInfo()
 
170
        # We put the actual inventory ids in the footer, so that the patch
 
171
        # is easier to read for humans.
 
172
        # Unfortunately, that means we need to read everything before we
 
173
        # can create a proper bundle.
 
174
        self._read()
 
175
        self._validate()
 
176
 
 
177
    def _read(self):
 
178
        self._read_header()
 
179
        while self._next_line is not None:
 
180
            self._read_revision_header()
 
181
            if self._next_line is None:
 
182
                break
 
183
            self._read_patches()
 
184
            self._read_footer()
 
185
 
 
186
    def _validate(self):
 
187
        """Make sure that the information read in makes sense
 
188
        and passes appropriate checksums.
 
189
        """
 
190
        # Fill in all the missing blanks for the revisions
 
191
        # and generate the real_revisions list.
 
192
        self.info.complete_info()
 
193
 
 
194
    def _validate_revision(self, inventory, revision_id):
 
195
        """Make sure all revision entries match their checksum."""
 
196
 
 
197
        # This is a mapping from each revision id to it's sha hash
 
198
        rev_to_sha1 = {}
 
199
        
 
200
        rev = self.info.get_revision(revision_id)
 
201
        rev_info = self.info.get_revision_info(revision_id)
 
202
        assert rev.revision_id == rev_info.revision_id
 
203
        assert rev.revision_id == revision_id
 
204
        sha1 = StrictTestament(rev, inventory).as_sha1()
 
205
        if sha1 != rev_info.sha1:
 
206
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
 
207
        if rev_to_sha1.has_key(rev.revision_id):
 
208
            raise BzrError('Revision {%s} given twice in the list'
 
209
                    % (rev.revision_id))
 
210
        rev_to_sha1[rev.revision_id] = sha1
212
211
 
213
212
    def _validate_references_from_repository(self, repository):
214
213
        """Now that we have a repository which should have some of the
236
235
        # All of the contained revisions were checked
237
236
        # in _validate_revisions
238
237
        checked = {}
239
 
        for rev_info in self.revisions:
 
238
        for rev_info in self.info.revisions:
240
239
            checked[rev_info.revision_id] = True
241
240
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
242
 
 
243
 
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
 
241
                
 
242
        for (rev, rev_info) in zip(self.info.real_revisions, self.info.revisions):
244
243
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
245
244
 
246
245
        count = 0
247
246
        missing = {}
248
247
        for revision_id, sha1 in rev_to_sha.iteritems():
249
248
            if repository.has_revision(revision_id):
250
 
                testament = StrictTestament.from_revision(repository,
 
249
                testament = StrictTestament.from_revision(repository, 
251
250
                                                          revision_id)
252
 
                local_sha1 = self._testament_sha1_from_revision(repository,
253
 
                                                                revision_id)
 
251
                local_sha1 = testament.as_sha1()
254
252
                if sha1 != local_sha1:
255
 
                    raise BzrError('sha1 mismatch. For revision id {%s}'
 
253
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
256
254
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
257
255
                else:
258
256
                    count += 1
259
257
            elif revision_id not in checked:
260
258
                missing[revision_id] = sha1
261
259
 
 
260
        for inv_id, sha1 in inv_to_sha.iteritems():
 
261
            if repository.has_revision(inv_id):
 
262
                # Note: branch.get_inventory_sha1() just returns the value that
 
263
                # is stored in the revision text, and that value may be out
 
264
                # of date. This is bogus, because that means we aren't
 
265
                # validating the actual text, just that we wrote and read the
 
266
                # string. But for now, what the hell.
 
267
                local_sha1 = repository.get_inventory_sha1(inv_id)
 
268
                if sha1 != local_sha1:
 
269
                    raise BzrError('sha1 mismatch. For inventory id {%s}' 
 
270
                                   'local: %s, bundle: %s' % 
 
271
                                   (inv_id, local_sha1, sha1))
 
272
                else:
 
273
                    count += 1
 
274
 
262
275
        if len(missing) > 0:
263
276
            # I don't know if this is an error yet
264
277
            warning('Not all revision hashes could be validated.'
265
278
                    ' Unable validate %d hashes' % len(missing))
266
279
        mutter('Verified %d sha hashes for the bundle.' % count)
267
 
        self._validated_revisions_against_repo = True
268
280
 
269
281
    def _validate_inventory(self, inv, revision_id):
270
282
        """At this point we should have generated the BundleTree,
271
283
        so build up an inventory, and make sure the hashes match.
272
284
        """
 
285
 
 
286
        assert inv is not None
 
287
 
273
288
        # Now we should have a complete inventory entry.
274
289
        s = serializer_v5.write_inventory_to_string(inv)
275
290
        sha1 = sha_string(s)
276
291
        # Target revision is the last entry in the real_revisions list
277
 
        rev = self.get_revision(revision_id)
278
 
        if rev.revision_id != revision_id:
279
 
            raise AssertionError()
 
292
        rev = self.info.get_revision(revision_id)
 
293
        assert rev.revision_id == revision_id
280
294
        if sha1 != rev.inventory_sha1:
281
295
            open(',,bogus-inv', 'wb').write(s)
282
296
            warning('Inventory sha hash mismatch for revision %s. %s'
283
297
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
284
298
 
285
 
    def _validate_revision(self, inventory, revision_id):
286
 
        """Make sure all revision entries match their checksum."""
287
 
 
288
 
        # This is a mapping from each revision id to it's sha hash
289
 
        rev_to_sha1 = {}
290
 
 
291
 
        rev = self.get_revision(revision_id)
292
 
        rev_info = self.get_revision_info(revision_id)
293
 
        if not (rev.revision_id == rev_info.revision_id):
294
 
            raise AssertionError()
295
 
        if not (rev.revision_id == revision_id):
296
 
            raise AssertionError()
297
 
        sha1 = self._testament_sha1(rev, inventory)
298
 
        if sha1 != rev_info.sha1:
299
 
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
300
 
        if rev.revision_id in rev_to_sha1:
301
 
            raise BzrError('Revision {%s} given twice in the list'
302
 
                    % (rev.revision_id))
303
 
        rev_to_sha1[rev.revision_id] = sha1
 
299
    def get_bundle(self, repository):
 
300
        """Return the meta information, and a Bundle tree which can
 
301
        be used to populate the local stores and working tree, respectively.
 
302
        """
 
303
        return self.info, self.revision_tree(repository, self.info.target)
 
304
 
 
305
    def revision_tree(self, repository, revision_id, base=None):
 
306
        revision = self.info.get_revision(revision_id)
 
307
        base = self.info.get_base(revision)
 
308
        assert base != revision_id
 
309
        self._validate_references_from_repository(repository)
 
310
        revision_info = self.info.get_revision_info(revision_id)
 
311
        inventory_revision_id = revision_id
 
312
        bundle_tree = BundleTree(repository.revision_tree(base), 
 
313
                                  inventory_revision_id)
 
314
        self._update_tree(bundle_tree, revision_id)
 
315
 
 
316
        inv = bundle_tree.inventory
 
317
        self._validate_inventory(inv, revision_id)
 
318
        self._validate_revision(inv, revision_id)
 
319
 
 
320
        return bundle_tree
 
321
 
 
322
    def _next(self):
 
323
        """yield the next line, but secretly
 
324
        keep 1 extra line for peeking.
 
325
        """
 
326
        for line in self.from_file:
 
327
            last = self._next_line
 
328
            self._next_line = line
 
329
            if last is not None:
 
330
                #mutter('yielding line: %r' % last)
 
331
                yield last
 
332
        last = self._next_line
 
333
        self._next_line = None
 
334
        #mutter('yielding line: %r' % last)
 
335
        yield last
 
336
 
 
337
    def _read_header(self):
 
338
        """Read the bzr header"""
 
339
        header = get_header()
 
340
        found = False
 
341
        for line in self._next():
 
342
            if found:
 
343
                # not all mailers will keep trailing whitespace
 
344
                if line == '#\n':
 
345
                    line = '# \n'
 
346
                if (not line.startswith('# ') or not line.endswith('\n')
 
347
                        or line[2:-1].decode('utf-8') != header[0]):
 
348
                    raise MalformedHeader('Found a header, but it'
 
349
                        ' was improperly formatted')
 
350
                header.pop(0) # We read this line.
 
351
                if not header:
 
352
                    break # We found everything.
 
353
            elif (line.startswith('#') and line.endswith('\n')):
 
354
                line = line[1:-1].strip().decode('utf-8')
 
355
                if line[:len(header_str)] == header_str:
 
356
                    if line == header[0]:
 
357
                        found = True
 
358
                    else:
 
359
                        raise MalformedHeader('Found what looks like'
 
360
                                ' a header, but did not match')
 
361
                    header.pop(0)
 
362
        else:
 
363
            raise MalformedHeader('Did not find an opening header')
 
364
 
 
365
    def _read_revision_header(self):
 
366
        self.info.revisions.append(RevisionInfo(None))
 
367
        for line in self._next():
 
368
            # The bzr header is terminated with a blank line
 
369
            # which does not start with '#'
 
370
            if line is None or line == '\n':
 
371
                break
 
372
            self._handle_next(line)
 
373
 
 
374
    def _read_next_entry(self, line, indent=1):
 
375
        """Read in a key-value pair
 
376
        """
 
377
        if not line.startswith('#'):
 
378
            raise MalformedHeader('Bzr header did not start with #')
 
379
        line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
 
380
        if line[:indent] == ' '*indent:
 
381
            line = line[indent:]
 
382
        if not line:
 
383
            return None, None# Ignore blank lines
 
384
 
 
385
        loc = line.find(': ')
 
386
        if loc != -1:
 
387
            key = line[:loc]
 
388
            value = line[loc+2:]
 
389
            if not value:
 
390
                value = self._read_many(indent=indent+2)
 
391
        elif line[-1:] == ':':
 
392
            key = line[:-1]
 
393
            value = self._read_many(indent=indent+2)
 
394
        else:
 
395
            raise MalformedHeader('While looking for key: value pairs,'
 
396
                    ' did not find the colon %r' % (line))
 
397
 
 
398
        key = key.replace(' ', '_')
 
399
        #mutter('found %s: %s' % (key, value))
 
400
        return key, value
 
401
 
 
402
    def _handle_next(self, line):
 
403
        if line is None:
 
404
            return
 
405
        key, value = self._read_next_entry(line, indent=1)
 
406
        mutter('_handle_next %r => %r' % (key, value))
 
407
        if key is None:
 
408
            return
 
409
 
 
410
        revision_info = self.info.revisions[-1]
 
411
        if hasattr(revision_info, key):
 
412
            if getattr(revision_info, key) is None:
 
413
                setattr(revision_info, key, value)
 
414
            else:
 
415
                raise MalformedHeader('Duplicated Key: %s' % key)
 
416
        else:
 
417
            # What do we do with a key we don't recognize
 
418
            raise MalformedHeader('Unknown Key: "%s"' % key)
 
419
    
 
420
    def _read_many(self, indent):
 
421
        """If a line ends with no entry, that means that it should be
 
422
        followed with multiple lines of values.
 
423
 
 
424
        This detects the end of the list, because it will be a line that
 
425
        does not start properly indented.
 
426
        """
 
427
        values = []
 
428
        start = '#' + (' '*indent)
 
429
 
 
430
        if self._next_line is None or self._next_line[:len(start)] != start:
 
431
            return values
 
432
 
 
433
        for line in self._next():
 
434
            values.append(line[len(start):-1].decode('utf-8'))
 
435
            if self._next_line is None or self._next_line[:len(start)] != start:
 
436
                break
 
437
        return values
 
438
 
 
439
    def _read_one_patch(self):
 
440
        """Read in one patch, return the complete patch, along with
 
441
        the next line.
 
442
 
 
443
        :return: action, lines, do_continue
 
444
        """
 
445
        #mutter('_read_one_patch: %r' % self._next_line)
 
446
        # Peek and see if there are no patches
 
447
        if self._next_line is None or self._next_line.startswith('#'):
 
448
            return None, [], False
 
449
 
 
450
        first = True
 
451
        lines = []
 
452
        for line in self._next():
 
453
            if first:
 
454
                if not line.startswith('==='):
 
455
                    raise MalformedPatches('The first line of all patches'
 
456
                        ' should be a bzr meta line "==="'
 
457
                        ': %r' % line)
 
458
                action = line[4:-1].decode('utf-8')
 
459
            elif line.startswith('... '):
 
460
                action += line[len('... '):-1].decode('utf-8')
 
461
 
 
462
            if (self._next_line is not None and 
 
463
                self._next_line.startswith('===')):
 
464
                return action, lines, True
 
465
            elif self._next_line is None or self._next_line.startswith('#'):
 
466
                return action, lines, False
 
467
 
 
468
            if first:
 
469
                first = False
 
470
            elif not line.startswith('... '):
 
471
                lines.append(line)
 
472
 
 
473
        return action, lines, False
 
474
            
 
475
    def _read_patches(self):
 
476
        do_continue = True
 
477
        revision_actions = []
 
478
        while do_continue:
 
479
            action, lines, do_continue = self._read_one_patch()
 
480
            if action is not None:
 
481
                revision_actions.append((action, lines))
 
482
        assert self.info.revisions[-1].tree_actions is None
 
483
        self.info.revisions[-1].tree_actions = revision_actions
 
484
 
 
485
    def _read_footer(self):
 
486
        """Read the rest of the meta information.
 
487
 
 
488
        :param first_line:  The previous step iterates past what it
 
489
                            can handle. That extra line is given here.
 
490
        """
 
491
        for line in self._next():
 
492
            self._handle_next(line)
 
493
            if not self._next_line.startswith('#'):
 
494
                self._next().next()
 
495
                break
 
496
            if self._next_line is None:
 
497
                break
304
498
 
305
499
    def _update_tree(self, bundle_tree, revision_id):
306
500
        """This fills out a BundleTree based on the information
311
505
 
312
506
        def get_rev_id(last_changed, path, kind):
313
507
            if last_changed is not None:
314
 
                # last_changed will be a Unicode string because of how it was
315
 
                # read. Convert it back to utf8.
316
 
                changed_revision_id = osutils.safe_revision_id(last_changed,
317
 
                                                               warn=False)
 
508
                changed_revision_id = last_changed.decode('utf-8')
318
509
            else:
319
510
                changed_revision_id = revision_id
320
511
            bundle_tree.note_last_changed(path, changed_revision_id)
331
522
                if name == 'last-changed':
332
523
                    last_changed = value
333
524
                elif name == 'executable':
 
525
                    assert value in ('yes', 'no'), value
334
526
                    val = (value == 'yes')
335
527
                    bundle_tree.note_executable(new_path, val)
336
528
                elif name == 'target':
340
532
            return last_changed, encoding
341
533
 
342
534
        def do_patch(path, lines, encoding):
343
 
            if encoding == 'base64':
 
535
            if encoding is not None:
 
536
                assert encoding == 'base64'
344
537
                patch = base64.decodestring(''.join(lines))
345
 
            elif encoding is None:
 
538
            else:
346
539
                patch =  ''.join(lines)
347
 
            else:
348
 
                raise ValueError(encoding)
349
540
            bundle_tree.note_patch(path, patch)
350
541
 
351
542
        def renamed(kind, extra, lines):
387
578
            if not info[1].startswith('file-id:'):
388
579
                raise BzrError('The file-id should follow the path for an add'
389
580
                        ': %r' % extra)
390
 
            # This will be Unicode because of how the stream is read. Turn it
391
 
            # back into a utf8 file_id
392
 
            file_id = osutils.safe_file_id(info[1][8:], warn=False)
 
581
            file_id = info[1][8:]
393
582
 
394
583
            bundle_tree.note_id(file_id, path, kind)
395
584
            # this will be overridden in extra_info if executable is specified.
411
600
            revision = get_rev_id(last_modified, path, kind)
412
601
            if lines:
413
602
                do_patch(path, lines, encoding)
414
 
 
 
603
            
415
604
        valid_actions = {
416
605
            'renamed':renamed,
417
606
            'removed':removed,
419
608
            'modified':modified
420
609
        }
421
610
        for action_line, lines in \
422
 
            self.get_revision_info(revision_id).tree_actions:
 
611
            self.info.get_revision_info(revision_id).tree_actions:
423
612
            first = action_line.find(' ')
424
613
            if first == -1:
425
614
                raise BzrError('Bogus action line'
440
629
                        ' (unrecognized action): %r' % action_line)
441
630
            valid_actions[action](kind, extra, lines)
442
631
 
443
 
    def install_revisions(self, target_repo, stream_input=True):
444
 
        """Install revisions and return the target revision
445
 
 
446
 
        :param target_repo: The repository to install into
447
 
        :param stream_input: Ignored by this implementation.
448
 
        """
449
 
        apply_bundle.install_bundle(target_repo, self)
450
 
        return self.target
451
 
 
452
 
    def get_merge_request(self, target_repo):
453
 
        """Provide data for performing a merge
454
 
 
455
 
        Returns suggested base, suggested target, and patch verification status
456
 
        """
457
 
        return None, self.target, 'inapplicable'
458
 
 
459
632
 
460
633
class BundleTree(Tree):
461
634
    def __init__(self, base_tree, revision_id):
479
652
 
480
653
    def note_rename(self, old_path, new_path):
481
654
        """A file/directory has been renamed from old_path => new_path"""
482
 
        if new_path in self._renamed:
483
 
            raise AssertionError(new_path)
484
 
        if old_path in self._renamed_r:
485
 
            raise AssertionError(old_path)
 
655
        assert not self._renamed.has_key(new_path)
 
656
        assert not self._renamed_r.has_key(old_path)
486
657
        self._renamed[new_path] = old_path
487
658
        self._renamed_r[old_path] = new_path
488
659
 
493
664
        self._kinds[new_id] = kind
494
665
 
495
666
    def note_last_changed(self, file_id, revision_id):
496
 
        if (file_id in self._last_changed
 
667
        if (self._last_changed.has_key(file_id)
497
668
                and self._last_changed[file_id] != revision_id):
498
669
            raise BzrError('Mismatched last-changed revision for file_id {%s}'
499
670
                    ': %s != %s' % (file_id,
518
689
 
519
690
    def old_path(self, new_path):
520
691
        """Get the old_path (path in the base_tree) for the file at new_path"""
521
 
        if new_path[:1] in ('\\', '/'):
522
 
            raise ValueError(new_path)
 
692
        assert new_path[:1] not in ('\\', '/')
523
693
        old_path = self._renamed.get(new_path)
524
694
        if old_path is not None:
525
695
            return old_path
532
702
            if old_dir is None:
533
703
                old_path = None
534
704
            else:
535
 
                old_path = pathjoin(old_dir, basename)
 
705
                old_path = os.path.join(old_dir, basename)
536
706
        else:
537
707
            old_path = new_path
538
708
        #If the new path wasn't in renamed, the old one shouldn't be in
539
709
        #renamed_r
540
 
        if old_path in self._renamed_r:
 
710
        if self._renamed_r.has_key(old_path):
541
711
            return None
542
 
        return old_path
 
712
        return old_path 
543
713
 
544
714
    def new_path(self, old_path):
545
715
        """Get the new_path (path in the target_tree) for the file at old_path
546
716
        in the base tree.
547
717
        """
548
 
        if old_path[:1] in ('\\', '/'):
549
 
            raise ValueError(old_path)
 
718
        assert old_path[:1] not in ('\\', '/')
550
719
        new_path = self._renamed_r.get(old_path)
551
720
        if new_path is not None:
552
721
            return new_path
553
 
        if new_path in self._renamed:
 
722
        if self._renamed.has_key(new_path):
554
723
            return None
555
724
        dirname,basename = os.path.split(old_path)
556
725
        if dirname != '':
558
727
            if new_dir is None:
559
728
                new_path = None
560
729
            else:
561
 
                new_path = pathjoin(new_dir, basename)
 
730
                new_path = os.path.join(new_dir, basename)
562
731
        else:
563
732
            new_path = old_path
564
733
        #If the old path wasn't in renamed, the new one shouldn't be in
565
734
        #renamed_r
566
 
        if new_path in self._renamed:
 
735
        if self._renamed.has_key(new_path):
567
736
            return None
568
 
        return new_path
 
737
        return new_path 
569
738
 
570
739
    def path2id(self, path):
571
740
        """Return the id of the file present at path in the target tree."""
577
746
            return None
578
747
        if old_path in self.deleted:
579
748
            return None
580
 
        if getattr(self.base_tree, 'path2id', None) is not None:
 
749
        if hasattr(self.base_tree, 'path2id'):
581
750
            return self.base_tree.path2id(old_path)
582
751
        else:
583
752
            return self.base_tree.inventory.path2id(old_path)
605
774
                return None
606
775
        new_path = self.id2path(file_id)
607
776
        return self.base_tree.path2id(new_path)
608
 
 
 
777
        
609
778
    def get_file(self, file_id):
610
779
        """Return a file-like object containing the new contents of the
611
780
        file given by file_id.
615
784
                then be cached.
616
785
        """
617
786
        base_id = self.old_contents_id(file_id)
618
 
        if (base_id is not None and
619
 
            base_id != self.base_tree.inventory.root.file_id):
 
787
        if base_id is not None:
620
788
            patch_original = self.base_tree.get_file(base_id)
621
789
        else:
622
790
            patch_original = None
623
791
        file_patch = self.patches.get(self.id2path(file_id))
624
792
        if file_patch is None:
625
 
            if (patch_original is None and
 
793
            if (patch_original is None and 
626
794
                self.get_kind(file_id) == 'directory'):
627
795
                return StringIO()
628
 
            if patch_original is None:
629
 
                raise AssertionError("None: %s" % file_id)
 
796
            assert patch_original is not None, "None: %s" % file_id
630
797
            return patch_original
631
798
 
632
 
        if file_patch.startswith('\\'):
633
 
            raise ValueError(
634
 
                'Malformed patch for %s, %r' % (file_id, file_patch))
 
799
        assert not file_patch.startswith('\\'), \
 
800
            'Malformed patch for %s, %r' % (file_id, file_patch)
635
801
        return patched_file(file_patch, patch_original)
636
802
 
637
803
    def get_symlink_target(self, file_id):
684
850
        This need to be called before ever accessing self.inventory
685
851
        """
686
852
        from os.path import dirname, basename
 
853
 
 
854
        assert self.base_tree is not None
687
855
        base_inv = self.base_tree.inventory
688
 
        inv = Inventory(None, self.revision_id)
 
856
        root_id = base_inv.root.file_id
 
857
        try:
 
858
            # New inventories have a unique root_id
 
859
            inv = Inventory(root_id, self.revision_id)
 
860
        except TypeError:
 
861
            inv = Inventory(revision_id=self.revision_id)
689
862
 
690
863
        def add_entry(file_id):
691
864
            path = self.id2path(file_id)
692
865
            if path is None:
693
866
                return
694
 
            if path == '':
695
 
                parent_id = None
 
867
            parent_path = dirname(path)
 
868
            if parent_path == u'':
 
869
                parent_id = root_id
696
870
            else:
697
 
                parent_path = dirname(path)
698
871
                parent_id = self.path2id(parent_path)
699
872
 
700
873
            kind = self.get_kind(file_id)
721
894
 
722
895
        sorted_entries = self.sorted_path_id()
723
896
        for path, file_id in sorted_entries:
 
897
            if file_id == inv.root.file_id:
 
898
                continue
724
899
            add_entry(file_id)
725
900
 
726
901
        return inv
755
930
    from bzrlib.iterablefile import IterableFile
756
931
    if file_patch == "":
757
932
        return IterableFile(())
758
 
    # string.splitlines(True) also splits on '\r', but the iter_patched code
759
 
    # only expects to iterate over '\n' style lines
760
 
    return IterableFile(iter_patched(original,
761
 
                StringIO(file_patch).readlines()))
 
933
    return IterableFile(iter_patched(original, file_patch.splitlines(True)))