~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Patch Queue Manager
  • Date: 2012-10-25 11:13:27 UTC
  • mfrom: (6570.1.6 rubberstamp)
  • Revision ID: pqm@pqm.ubuntu.com-20121025111327-p0ylql0nh9fla0rs
(gz) Set approved revision and vote "Approve" when using lp-propose
 --approve (Jonathan Lange)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
 
"""\
3
 
Read in a bundle stream, and process it into a BundleReader object.
4
 
"""
 
1
# Copyright (C) 2005-2010 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."""
 
18
 
 
19
from __future__ import absolute_import
5
20
 
6
21
import base64
7
22
from cStringIO import StringIO
8
23
import os
9
24
import pprint
10
25
 
11
 
from bzrlib.errors import (TestamentMismatch, BzrError, 
12
 
                           MalformedHeader, MalformedPatches, NotABundle)
13
 
from bzrlib.bundle.common import get_header, header_str
14
 
from bzrlib.inventory import (Inventory, InventoryEntry,
15
 
                              InventoryDirectory, InventoryFile,
16
 
                              InventoryLink)
17
 
from bzrlib.osutils import sha_file, sha_string
 
26
from bzrlib import (
 
27
    osutils,
 
28
    timestamp,
 
29
    )
 
30
from bzrlib.bundle import apply_bundle
 
31
from bzrlib.errors import (
 
32
    TestamentMismatch,
 
33
    BzrError,
 
34
    )
 
35
from bzrlib.inventory import (
 
36
    Inventory,
 
37
    InventoryDirectory,
 
38
    InventoryFile,
 
39
    InventoryLink,
 
40
    )
 
41
from bzrlib.osutils import sha_string, pathjoin
18
42
from bzrlib.revision import Revision, NULL_REVISION
19
43
from bzrlib.testament import StrictTestament
20
44
from bzrlib.trace import mutter, warning
57
81
        if self.properties:
58
82
            for property in self.properties:
59
83
                key_end = property.find(': ')
60
 
                assert key_end is not None
61
 
                key = property[:key_end].encode('utf-8')
62
 
                value = property[key_end+2:].encode('utf-8')
 
84
                if key_end == -1:
 
85
                    if not property.endswith(':'):
 
86
                        raise ValueError(property)
 
87
                    key = str(property[:-1])
 
88
                    value = ''
 
89
                else:
 
90
                    key = str(property[:key_end])
 
91
                    value = property[key_end+2:]
63
92
                rev.properties[key] = value
64
93
 
65
94
        return rev
66
95
 
 
96
    @staticmethod
 
97
    def from_revision(revision):
 
98
        revision_info = RevisionInfo(revision.revision_id)
 
99
        date = timestamp.format_highres_date(revision.timestamp,
 
100
                                             revision.timezone)
 
101
        revision_info.date = date
 
102
        revision_info.timezone = revision.timezone
 
103
        revision_info.timestamp = revision.timestamp
 
104
        revision_info.message = revision.message.split('\n')
 
105
        revision_info.properties = [': '.join(p) for p in
 
106
                                    revision.properties.iteritems()]
 
107
        return revision_info
 
108
 
67
109
 
68
110
class BundleInfo(object):
69
111
    """This contains the meta information. Stuff that allows you to
70
112
    recreate the revision or inventory XML.
71
113
    """
72
 
    def __init__(self):
 
114
    def __init__(self, bundle_format=None):
 
115
        self.bundle_format = None
73
116
        self.committer = None
74
117
        self.date = None
75
118
        self.message = None
86
129
        self.timestamp = None
87
130
        self.timezone = None
88
131
 
 
132
        # Have we checked the repository yet?
 
133
        self._validated_revisions_against_repo = False
 
134
 
89
135
    def __str__(self):
90
136
        return pprint.pformat(self.__dict__)
91
137
 
94
140
        split up, based on the assumptions that can be made
95
141
        when information is missing.
96
142
        """
97
 
        from bzrlib.bundle.common import unpack_highres_date
 
143
        from bzrlib.timestamp import unpack_highres_date
98
144
        # Put in all of the guessable information.
99
145
        if not self.timestamp and self.date:
100
146
            self.timestamp, self.timezone = unpack_highres_date(self.date)
117
163
    def get_base(self, revision):
118
164
        revision_info = self.get_revision_info(revision.revision_id)
119
165
        if revision_info.base_id is not None:
120
 
            if revision_info.base_id == NULL_REVISION:
121
 
                return None
122
 
            else:
123
 
                return revision_info.base_id
 
166
            return revision_info.base_id
124
167
        if len(revision.parent_ids) == 0:
125
168
            # There is no base listed, and
126
169
            # the lowest revision doesn't have a parent
127
170
            # so this is probably against the empty tree
128
 
            # and thus base truly is None
129
 
            return None
 
171
            # and thus base truly is NULL_REVISION
 
172
            return NULL_REVISION
130
173
        else:
131
174
            return revision.parent_ids[-1]
132
175
 
152
195
                return r
153
196
        raise KeyError(revision_id)
154
197
 
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
 
198
    def revision_tree(self, repository, revision_id, base=None):
 
199
        revision = self.get_revision(revision_id)
 
200
        base = self.get_base(revision)
 
201
        if base == revision_id:
 
202
            raise AssertionError()
 
203
        if not self._validated_revisions_against_repo:
 
204
            self._validate_references_from_repository(repository)
 
205
        revision_info = self.get_revision_info(revision_id)
 
206
        inventory_revision_id = revision_id
 
207
        bundle_tree = BundleTree(repository.revision_tree(base),
 
208
                                  inventory_revision_id)
 
209
        self._update_tree(bundle_tree, revision_id)
 
210
 
 
211
        inv = bundle_tree.inventory
 
212
        self._validate_inventory(inv, revision_id)
 
213
        self._validate_revision(bundle_tree, revision_id)
 
214
 
 
215
        return bundle_tree
211
216
 
212
217
    def _validate_references_from_repository(self, repository):
213
218
        """Now that we have a repository which should have some of the
235
240
        # All of the contained revisions were checked
236
241
        # in _validate_revisions
237
242
        checked = {}
238
 
        for rev_info in self.info.revisions:
 
243
        for rev_info in self.revisions:
239
244
            checked[rev_info.revision_id] = True
240
245
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
241
 
                
242
 
        for (rev, rev_info) in zip(self.info.real_revisions, self.info.revisions):
 
246
 
 
247
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
243
248
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
244
249
 
245
250
        count = 0
246
251
        missing = {}
247
252
        for revision_id, sha1 in rev_to_sha.iteritems():
248
253
            if repository.has_revision(revision_id):
249
 
                testament = StrictTestament.from_revision(repository, 
 
254
                testament = StrictTestament.from_revision(repository,
250
255
                                                          revision_id)
251
 
                local_sha1 = testament.as_sha1()
 
256
                local_sha1 = self._testament_sha1_from_revision(repository,
 
257
                                                                revision_id)
252
258
                if sha1 != local_sha1:
253
 
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
 
259
                    raise BzrError('sha1 mismatch. For revision id {%s}'
254
260
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
255
261
                else:
256
262
                    count += 1
257
263
            elif revision_id not in checked:
258
264
                missing[revision_id] = sha1
259
265
 
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
 
 
275
266
        if len(missing) > 0:
276
267
            # I don't know if this is an error yet
277
268
            warning('Not all revision hashes could be validated.'
278
269
                    ' Unable validate %d hashes' % len(missing))
279
270
        mutter('Verified %d sha hashes for the bundle.' % count)
 
271
        self._validated_revisions_against_repo = True
280
272
 
281
273
    def _validate_inventory(self, inv, revision_id):
282
274
        """At this point we should have generated the BundleTree,
283
275
        so build up an inventory, and make sure the hashes match.
284
276
        """
285
 
 
286
 
        assert inv is not None
287
 
 
288
277
        # Now we should have a complete inventory entry.
289
278
        s = serializer_v5.write_inventory_to_string(inv)
290
279
        sha1 = sha_string(s)
291
280
        # Target revision is the last entry in the real_revisions list
292
 
        rev = self.info.get_revision(revision_id)
293
 
        assert rev.revision_id == revision_id
 
281
        rev = self.get_revision(revision_id)
 
282
        if rev.revision_id != revision_id:
 
283
            raise AssertionError()
294
284
        if sha1 != rev.inventory_sha1:
295
 
            open(',,bogus-inv', 'wb').write(s)
 
285
            f = open(',,bogus-inv', 'wb')
 
286
            try:
 
287
                f.write(s)
 
288
            finally:
 
289
                f.close()
296
290
            warning('Inventory sha hash mismatch for revision %s. %s'
297
291
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
298
292
 
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 NotABundle('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
 
293
    def _validate_revision(self, tree, revision_id):
 
294
        """Make sure all revision entries match their checksum."""
 
295
 
 
296
        # This is a mapping from each revision id to its sha hash
 
297
        rev_to_sha1 = {}
 
298
 
 
299
        rev = self.get_revision(revision_id)
 
300
        rev_info = self.get_revision_info(revision_id)
 
301
        if not (rev.revision_id == rev_info.revision_id):
 
302
            raise AssertionError()
 
303
        if not (rev.revision_id == revision_id):
 
304
            raise AssertionError()
 
305
        sha1 = self._testament_sha1(rev, tree)
 
306
        if sha1 != rev_info.sha1:
 
307
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
 
308
        if rev.revision_id in rev_to_sha1:
 
309
            raise BzrError('Revision {%s} given twice in the list'
 
310
                    % (rev.revision_id))
 
311
        rev_to_sha1[rev.revision_id] = sha1
498
312
 
499
313
    def _update_tree(self, bundle_tree, revision_id):
500
314
        """This fills out a BundleTree based on the information
505
319
 
506
320
        def get_rev_id(last_changed, path, kind):
507
321
            if last_changed is not None:
508
 
                changed_revision_id = last_changed.decode('utf-8')
 
322
                # last_changed will be a Unicode string because of how it was
 
323
                # read. Convert it back to utf8.
 
324
                changed_revision_id = osutils.safe_revision_id(last_changed,
 
325
                                                               warn=False)
509
326
            else:
510
327
                changed_revision_id = revision_id
511
328
            bundle_tree.note_last_changed(path, changed_revision_id)
518
335
                try:
519
336
                    name, value = info_item.split(':', 1)
520
337
                except ValueError:
521
 
                    raise 'Value %r has no colon' % info_item
 
338
                    raise ValueError('Value %r has no colon' % info_item)
522
339
                if name == 'last-changed':
523
340
                    last_changed = value
524
341
                elif name == 'executable':
525
 
                    assert value in ('yes', 'no'), value
526
342
                    val = (value == 'yes')
527
343
                    bundle_tree.note_executable(new_path, val)
528
344
                elif name == 'target':
532
348
            return last_changed, encoding
533
349
 
534
350
        def do_patch(path, lines, encoding):
535
 
            if encoding is not None:
536
 
                assert encoding == 'base64'
 
351
            if encoding == 'base64':
537
352
                patch = base64.decodestring(''.join(lines))
538
 
            else:
 
353
            elif encoding is None:
539
354
                patch =  ''.join(lines)
 
355
            else:
 
356
                raise ValueError(encoding)
540
357
            bundle_tree.note_patch(path, patch)
541
358
 
542
359
        def renamed(kind, extra, lines):
578
395
            if not info[1].startswith('file-id:'):
579
396
                raise BzrError('The file-id should follow the path for an add'
580
397
                        ': %r' % extra)
581
 
            file_id = info[1][8:]
 
398
            # This will be Unicode because of how the stream is read. Turn it
 
399
            # back into a utf8 file_id
 
400
            file_id = osutils.safe_file_id(info[1][8:], warn=False)
582
401
 
583
402
            bundle_tree.note_id(file_id, path, kind)
584
403
            # this will be overridden in extra_info if executable is specified.
600
419
            revision = get_rev_id(last_modified, path, kind)
601
420
            if lines:
602
421
                do_patch(path, lines, encoding)
603
 
            
 
422
 
604
423
        valid_actions = {
605
424
            'renamed':renamed,
606
425
            'removed':removed,
608
427
            'modified':modified
609
428
        }
610
429
        for action_line, lines in \
611
 
            self.info.get_revision_info(revision_id).tree_actions:
 
430
            self.get_revision_info(revision_id).tree_actions:
612
431
            first = action_line.find(' ')
613
432
            if first == -1:
614
433
                raise BzrError('Bogus action line'
629
448
                        ' (unrecognized action): %r' % action_line)
630
449
            valid_actions[action](kind, extra, lines)
631
450
 
 
451
    def install_revisions(self, target_repo, stream_input=True):
 
452
        """Install revisions and return the target revision
 
453
 
 
454
        :param target_repo: The repository to install into
 
455
        :param stream_input: Ignored by this implementation.
 
456
        """
 
457
        apply_bundle.install_bundle(target_repo, self)
 
458
        return self.target
 
459
 
 
460
    def get_merge_request(self, target_repo):
 
461
        """Provide data for performing a merge
 
462
 
 
463
        Returns suggested base, suggested target, and patch verification status
 
464
        """
 
465
        return None, self.target, 'inapplicable'
 
466
 
632
467
 
633
468
class BundleTree(Tree):
 
469
 
634
470
    def __init__(self, base_tree, revision_id):
635
471
        self.base_tree = base_tree
636
472
        self._renamed = {} # Mapping from old_path => new_path
652
488
 
653
489
    def note_rename(self, old_path, new_path):
654
490
        """A file/directory has been renamed from old_path => new_path"""
655
 
        assert not self._renamed.has_key(new_path)
656
 
        assert not self._renamed_r.has_key(old_path)
 
491
        if new_path in self._renamed:
 
492
            raise AssertionError(new_path)
 
493
        if old_path in self._renamed_r:
 
494
            raise AssertionError(old_path)
657
495
        self._renamed[new_path] = old_path
658
496
        self._renamed_r[old_path] = new_path
659
497
 
664
502
        self._kinds[new_id] = kind
665
503
 
666
504
    def note_last_changed(self, file_id, revision_id):
667
 
        if (self._last_changed.has_key(file_id)
 
505
        if (file_id in self._last_changed
668
506
                and self._last_changed[file_id] != revision_id):
669
507
            raise BzrError('Mismatched last-changed revision for file_id {%s}'
670
508
                    ': %s != %s' % (file_id,
689
527
 
690
528
    def old_path(self, new_path):
691
529
        """Get the old_path (path in the base_tree) for the file at new_path"""
692
 
        assert new_path[:1] not in ('\\', '/')
 
530
        if new_path[:1] in ('\\', '/'):
 
531
            raise ValueError(new_path)
693
532
        old_path = self._renamed.get(new_path)
694
533
        if old_path is not None:
695
534
            return old_path
702
541
            if old_dir is None:
703
542
                old_path = None
704
543
            else:
705
 
                old_path = os.path.join(old_dir, basename)
 
544
                old_path = pathjoin(old_dir, basename)
706
545
        else:
707
546
            old_path = new_path
708
547
        #If the new path wasn't in renamed, the old one shouldn't be in
709
548
        #renamed_r
710
 
        if self._renamed_r.has_key(old_path):
 
549
        if old_path in self._renamed_r:
711
550
            return None
712
 
        return old_path 
 
551
        return old_path
713
552
 
714
553
    def new_path(self, old_path):
715
554
        """Get the new_path (path in the target_tree) for the file at old_path
716
555
        in the base tree.
717
556
        """
718
 
        assert old_path[:1] not in ('\\', '/')
 
557
        if old_path[:1] in ('\\', '/'):
 
558
            raise ValueError(old_path)
719
559
        new_path = self._renamed_r.get(old_path)
720
560
        if new_path is not None:
721
561
            return new_path
722
 
        if self._renamed.has_key(new_path):
 
562
        if new_path in self._renamed:
723
563
            return None
724
564
        dirname,basename = os.path.split(old_path)
725
565
        if dirname != '':
727
567
            if new_dir is None:
728
568
                new_path = None
729
569
            else:
730
 
                new_path = os.path.join(new_dir, basename)
 
570
                new_path = pathjoin(new_dir, basename)
731
571
        else:
732
572
            new_path = old_path
733
573
        #If the old path wasn't in renamed, the new one shouldn't be in
734
574
        #renamed_r
735
 
        if self._renamed.has_key(new_path):
 
575
        if new_path in self._renamed:
736
576
            return None
737
 
        return new_path 
 
577
        return new_path
 
578
 
 
579
    def get_root_id(self):
 
580
        return self.path2id('')
738
581
 
739
582
    def path2id(self, path):
740
583
        """Return the id of the file present at path in the target tree."""
746
589
            return None
747
590
        if old_path in self.deleted:
748
591
            return None
749
 
        if hasattr(self.base_tree, 'path2id'):
750
 
            return self.base_tree.path2id(old_path)
751
 
        else:
752
 
            return self.base_tree.inventory.path2id(old_path)
 
592
        return self.base_tree.path2id(old_path)
753
593
 
754
594
    def id2path(self, file_id):
755
595
        """Return the new path in the target tree of the file with id file_id"""
774
614
                return None
775
615
        new_path = self.id2path(file_id)
776
616
        return self.base_tree.path2id(new_path)
777
 
        
 
617
 
778
618
    def get_file(self, file_id):
779
619
        """Return a file-like object containing the new contents of the
780
620
        file given by file_id.
784
624
                then be cached.
785
625
        """
786
626
        base_id = self.old_contents_id(file_id)
787
 
        if base_id is not None:
 
627
        if (base_id is not None and
 
628
            base_id != self.base_tree.get_root_id()):
788
629
            patch_original = self.base_tree.get_file(base_id)
789
630
        else:
790
631
            patch_original = None
791
632
        file_patch = self.patches.get(self.id2path(file_id))
792
633
        if file_patch is None:
793
 
            if (patch_original is None and 
794
 
                self.get_kind(file_id) == 'directory'):
 
634
            if (patch_original is None and
 
635
                self.kind(file_id) == 'directory'):
795
636
                return StringIO()
796
 
            assert patch_original is not None, "None: %s" % file_id
 
637
            if patch_original is None:
 
638
                raise AssertionError("None: %s" % file_id)
797
639
            return patch_original
798
640
 
799
 
        assert not file_patch.startswith('\\'), \
800
 
            'Malformed patch for %s, %r' % (file_id, file_patch)
 
641
        if file_patch.startswith('\\'):
 
642
            raise ValueError(
 
643
                'Malformed patch for %s, %r' % (file_id, file_patch))
801
644
        return patched_file(file_patch, patch_original)
802
645
 
803
 
    def get_symlink_target(self, file_id):
804
 
        new_path = self.id2path(file_id)
 
646
    def get_symlink_target(self, file_id, path=None):
 
647
        if path is None:
 
648
            path = self.id2path(file_id)
805
649
        try:
806
 
            return self._targets[new_path]
 
650
            return self._targets[path]
807
651
        except KeyError:
808
652
            return self.base_tree.get_symlink_target(file_id)
809
653
 
810
 
    def get_kind(self, file_id):
 
654
    def kind(self, file_id):
811
655
        if file_id in self._kinds:
812
656
            return self._kinds[file_id]
813
 
        return self.base_tree.inventory[file_id].kind
 
657
        return self.base_tree.kind(file_id)
 
658
 
 
659
    def get_file_revision(self, file_id):
 
660
        path = self.id2path(file_id)
 
661
        if path in self._last_changed:
 
662
            return self._last_changed[path]
 
663
        else:
 
664
            return self.base_tree.get_file_revision(file_id)
814
665
 
815
666
    def is_executable(self, file_id):
816
667
        path = self.id2path(file_id)
817
668
        if path in self._executable:
818
669
            return self._executable[path]
819
670
        else:
820
 
            return self.base_tree.inventory[file_id].executable
 
671
            return self.base_tree.is_executable(file_id)
821
672
 
822
673
    def get_last_changed(self, file_id):
823
674
        path = self.id2path(file_id)
824
675
        if path in self._last_changed:
825
676
            return self._last_changed[path]
826
 
        return self.base_tree.inventory[file_id].revision
 
677
        return self.base_tree.get_file_revision(file_id)
827
678
 
828
679
    def get_size_and_sha1(self, file_id):
829
680
        """Return the size and sha1 hash of the given file id.
836
687
        if new_path not in self.patches:
837
688
            # If the entry does not have a patch, then the
838
689
            # contents must be the same as in the base_tree
839
 
            ie = self.base_tree.inventory[file_id]
840
 
            if ie.text_size is None:
841
 
                return ie.text_size, ie.text_sha1
842
 
            return int(ie.text_size), ie.text_sha1
 
690
            text_size = self.base_tree.get_file_size(file_id)
 
691
            text_sha1 = self.base_tree.get_file_sha1(file_id)
 
692
            return text_size, text_sha1
843
693
        fileobj = self.get_file(file_id)
844
694
        content = fileobj.read()
845
695
        return len(content), sha_string(content)
850
700
        This need to be called before ever accessing self.inventory
851
701
        """
852
702
        from os.path import dirname, basename
853
 
 
854
 
        assert self.base_tree is not None
855
 
        base_inv = self.base_tree.inventory
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)
 
703
        inv = Inventory(None, self.revision_id)
862
704
 
863
705
        def add_entry(file_id):
864
706
            path = self.id2path(file_id)
865
707
            if path is None:
866
708
                return
867
 
            parent_path = dirname(path)
868
 
            if parent_path == u'':
869
 
                parent_id = root_id
 
709
            if path == '':
 
710
                parent_id = None
870
711
            else:
 
712
                parent_path = dirname(path)
871
713
                parent_id = self.path2id(parent_path)
872
714
 
873
 
            kind = self.get_kind(file_id)
 
715
            kind = self.kind(file_id)
874
716
            revision_id = self.get_last_changed(file_id)
875
717
 
876
718
            name = basename(path)
881
723
                ie.executable = self.is_executable(file_id)
882
724
            elif kind == 'symlink':
883
725
                ie = InventoryLink(file_id, name, parent_id)
884
 
                ie.symlink_target = self.get_symlink_target(file_id)
 
726
                ie.symlink_target = self.get_symlink_target(file_id, path)
885
727
            ie.revision = revision_id
886
728
 
887
 
            if kind in ('directory', 'symlink'):
888
 
                ie.text_size, ie.text_sha1 = None, None
889
 
            else:
 
729
            if kind == 'file':
890
730
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
891
 
            if (ie.text_size is None) and (kind == 'file'):
892
 
                raise BzrError('Got a text_size of None for file_id %r' % file_id)
 
731
                if ie.text_size is None:
 
732
                    raise BzrError(
 
733
                        'Got a text_size of None for file_id %r' % file_id)
893
734
            inv.add(ie)
894
735
 
895
736
        sorted_entries = self.sorted_path_id()
896
737
        for path, file_id in sorted_entries:
897
 
            if file_id == inv.root.file_id:
898
 
                continue
899
738
            add_entry(file_id)
900
739
 
901
740
        return inv
907
746
    # at that instant
908
747
    inventory = property(_get_inventory)
909
748
 
910
 
    def __iter__(self):
911
 
        for path, entry in self.inventory.iter_entries():
912
 
            yield entry.file_id
 
749
    root_inventory = property(_get_inventory)
 
750
 
 
751
    def all_file_ids(self):
 
752
        return set(
 
753
            [entry.file_id for path, entry in self.inventory.iter_entries()])
 
754
 
 
755
    def list_files(self, include_root=False, from_dir=None, recursive=True):
 
756
        # The only files returned by this are those from the version
 
757
        inv = self.inventory
 
758
        if from_dir is None:
 
759
            from_dir_id = None
 
760
        else:
 
761
            from_dir_id = inv.path2id(from_dir)
 
762
            if from_dir_id is None:
 
763
                # Directory not versioned
 
764
                return
 
765
        entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
 
766
        if inv.root is not None and not include_root and from_dir is None:
 
767
            # skip the root for compatability with the current apis.
 
768
            entries.next()
 
769
        for path, entry in entries:
 
770
            yield path, 'V', entry.kind, entry.file_id, entry
913
771
 
914
772
    def sorted_path_id(self):
915
773
        paths = []
916
774
        for result in self._new_id.iteritems():
917
775
            paths.append(result)
918
 
        for id in self.base_tree:
 
776
        for id in self.base_tree.all_file_ids():
919
777
            path = self.id2path(id)
920
778
            if path is None:
921
779
                continue
930
788
    from bzrlib.iterablefile import IterableFile
931
789
    if file_patch == "":
932
790
        return IterableFile(())
933
 
    return IterableFile(iter_patched(original, file_patch.splitlines(True)))
 
791
    # string.splitlines(True) also splits on '\r', but the iter_patched code
 
792
    # only expects to iterate over '\n' style lines
 
793
    return IterableFile(iter_patched(original,
 
794
                StringIO(file_patch).readlines()))