/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/read_bundle.py

  • Committer: Aaron Bentley
  • Date: 2006-05-30 15:18:12 UTC
  • mto: This revision was merged to the branch mainline in revision 1738.
  • Revision ID: abentley@panoramicfeedback.com-20060530151812-0e3e9b78cc15a804
Rename changesets to revision bundles

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