~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/read_bundle.py

  • Committer: Martin Pool
  • Date: 2005-08-30 03:10:32 UTC
  • Revision ID: mbp@sourcefrog.net-20050830031032-92ae5f0abb866ab8
- remove dead code and remove some small errors (pychecker)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006 by Canonical Ltd
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
"""Read in a bundle stream, and process it into a BundleReader object."""
18
 
 
19
 
import base64
20
 
from cStringIO import StringIO
21
 
import os
22
 
import pprint
23
 
 
24
 
from bzrlib.bundle.common import get_header, header_str
25
 
import bzrlib.errors
26
 
from bzrlib.errors import (TestamentMismatch, BzrError, 
27
 
                           MalformedHeader, MalformedPatches, NotABundle)
28
 
from bzrlib.inventory import (Inventory, InventoryEntry,
29
 
                              InventoryDirectory, InventoryFile,
30
 
                              InventoryLink)
31
 
from bzrlib.osutils import sha_file, sha_string
32
 
from bzrlib.revision import Revision, NULL_REVISION
33
 
from bzrlib.testament import StrictTestament
34
 
from bzrlib.trace import mutter, warning
35
 
import bzrlib.transport
36
 
from bzrlib.tree import Tree
37
 
import bzrlib.urlutils
38
 
from bzrlib.xml5 import serializer_v5
39
 
 
40
 
 
41
 
class RevisionInfo(object):
42
 
    """Gets filled out for each revision object that is read.
43
 
    """
44
 
    def __init__(self, revision_id):
45
 
        self.revision_id = revision_id
46
 
        self.sha1 = None
47
 
        self.committer = None
48
 
        self.date = None
49
 
        self.timestamp = None
50
 
        self.timezone = None
51
 
        self.inventory_sha1 = None
52
 
 
53
 
        self.parent_ids = None
54
 
        self.base_id = None
55
 
        self.message = None
56
 
        self.properties = None
57
 
        self.tree_actions = None
58
 
 
59
 
    def __str__(self):
60
 
        return pprint.pformat(self.__dict__)
61
 
 
62
 
    def as_revision(self):
63
 
        rev = Revision(revision_id=self.revision_id,
64
 
            committer=self.committer,
65
 
            timestamp=float(self.timestamp),
66
 
            timezone=int(self.timezone),
67
 
            inventory_sha1=self.inventory_sha1,
68
 
            message='\n'.join(self.message))
69
 
 
70
 
        if self.parent_ids:
71
 
            rev.parent_ids.extend(self.parent_ids)
72
 
 
73
 
        if self.properties:
74
 
            for property in self.properties:
75
 
                key_end = property.find(': ')
76
 
                assert key_end is not None
77
 
                key = property[:key_end].encode('utf-8')
78
 
                value = property[key_end+2:].encode('utf-8')
79
 
                rev.properties[key] = value
80
 
 
81
 
        return rev
82
 
 
83
 
 
84
 
class BundleInfo(object):
85
 
    """This contains the meta information. Stuff that allows you to
86
 
    recreate the revision or inventory XML.
87
 
    """
88
 
    def __init__(self):
89
 
        self.committer = None
90
 
        self.date = None
91
 
        self.message = None
92
 
 
93
 
        # A list of RevisionInfo objects
94
 
        self.revisions = []
95
 
 
96
 
        # The next entries are created during complete_info() and
97
 
        # other post-read functions.
98
 
 
99
 
        # A list of real Revision objects
100
 
        self.real_revisions = []
101
 
 
102
 
        self.timestamp = None
103
 
        self.timezone = None
104
 
 
105
 
    def __str__(self):
106
 
        return pprint.pformat(self.__dict__)
107
 
 
108
 
    def complete_info(self):
109
 
        """This makes sure that all information is properly
110
 
        split up, based on the assumptions that can be made
111
 
        when information is missing.
112
 
        """
113
 
        from bzrlib.bundle.common import unpack_highres_date
114
 
        # Put in all of the guessable information.
115
 
        if not self.timestamp and self.date:
116
 
            self.timestamp, self.timezone = unpack_highres_date(self.date)
117
 
 
118
 
        self.real_revisions = []
119
 
        for rev in self.revisions:
120
 
            if rev.timestamp is None:
121
 
                if rev.date is not None:
122
 
                    rev.timestamp, rev.timezone = \
123
 
                            unpack_highres_date(rev.date)
124
 
                else:
125
 
                    rev.timestamp = self.timestamp
126
 
                    rev.timezone = self.timezone
127
 
            if rev.message is None and self.message:
128
 
                rev.message = self.message
129
 
            if rev.committer is None and self.committer:
130
 
                rev.committer = self.committer
131
 
            self.real_revisions.append(rev.as_revision())
132
 
 
133
 
    def get_base(self, revision):
134
 
        revision_info = self.get_revision_info(revision.revision_id)
135
 
        if revision_info.base_id is not None:
136
 
            if revision_info.base_id == NULL_REVISION:
137
 
                return None
138
 
            else:
139
 
                return revision_info.base_id
140
 
        if len(revision.parent_ids) == 0:
141
 
            # There is no base listed, and
142
 
            # the lowest revision doesn't have a parent
143
 
            # so this is probably against the empty tree
144
 
            # and thus base truly is None
145
 
            return None
146
 
        else:
147
 
            return revision.parent_ids[-1]
148
 
 
149
 
    def _get_target(self):
150
 
        """Return the target revision."""
151
 
        if len(self.real_revisions) > 0:
152
 
            return self.real_revisions[0].revision_id
153
 
        elif len(self.revisions) > 0:
154
 
            return self.revisions[0].revision_id
155
 
        return None
156
 
 
157
 
    target = property(_get_target, doc='The target revision id')
158
 
 
159
 
    def get_revision(self, revision_id):
160
 
        for r in self.real_revisions:
161
 
            if r.revision_id == revision_id:
162
 
                return r
163
 
        raise KeyError(revision_id)
164
 
 
165
 
    def get_revision_info(self, revision_id):
166
 
        for r in self.revisions:
167
 
            if r.revision_id == revision_id:
168
 
                return r
169
 
        raise KeyError(revision_id)
170
 
 
171
 
 
172
 
class BundleReader(object):
173
 
    """This class reads in a bundle from a file, and returns
174
 
    a Bundle object, which can then be applied against a tree.
175
 
    """
176
 
    def __init__(self, from_file):
177
 
        """Read in the bundle from the file.
178
 
 
179
 
        :param from_file: A file-like object (must have iterator support).
180
 
        """
181
 
        object.__init__(self)
182
 
        self.from_file = iter(from_file)
183
 
        self._next_line = None
184
 
        
185
 
        self.info = BundleInfo()
186
 
        # We put the actual inventory ids in the footer, so that the patch
187
 
        # is easier to read for humans.
188
 
        # Unfortunately, that means we need to read everything before we
189
 
        # can create a proper bundle.
190
 
        self._read()
191
 
        self._validate()
192
 
 
193
 
    def _read(self):
194
 
        self._read_header()
195
 
        while self._next_line is not None:
196
 
            self._read_revision_header()
197
 
            if self._next_line is None:
198
 
                break
199
 
            self._read_patches()
200
 
            self._read_footer()
201
 
 
202
 
    def _validate(self):
203
 
        """Make sure that the information read in makes sense
204
 
        and passes appropriate checksums.
205
 
        """
206
 
        # Fill in all the missing blanks for the revisions
207
 
        # and generate the real_revisions list.
208
 
        self.info.complete_info()
209
 
 
210
 
    def _validate_revision(self, inventory, revision_id):
211
 
        """Make sure all revision entries match their checksum."""
212
 
 
213
 
        # This is a mapping from each revision id to it's sha hash
214
 
        rev_to_sha1 = {}
215
 
        
216
 
        rev = self.info.get_revision(revision_id)
217
 
        rev_info = self.info.get_revision_info(revision_id)
218
 
        assert rev.revision_id == rev_info.revision_id
219
 
        assert rev.revision_id == revision_id
220
 
        sha1 = StrictTestament(rev, inventory).as_sha1()
221
 
        if sha1 != rev_info.sha1:
222
 
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
223
 
        if rev_to_sha1.has_key(rev.revision_id):
224
 
            raise BzrError('Revision {%s} given twice in the list'
225
 
                    % (rev.revision_id))
226
 
        rev_to_sha1[rev.revision_id] = sha1
227
 
 
228
 
    def _validate_references_from_repository(self, repository):
229
 
        """Now that we have a repository which should have some of the
230
 
        revisions we care about, go through and validate all of them
231
 
        that we can.
232
 
        """
233
 
        rev_to_sha = {}
234
 
        inv_to_sha = {}
235
 
        def add_sha(d, revision_id, sha1):
236
 
            if revision_id is None:
237
 
                if sha1 is not None:
238
 
                    raise BzrError('A Null revision should always'
239
 
                        'have a null sha1 hash')
240
 
                return
241
 
            if revision_id in d:
242
 
                # This really should have been validated as part
243
 
                # of _validate_revisions but lets do it again
244
 
                if sha1 != d[revision_id]:
245
 
                    raise BzrError('** Revision %r referenced with 2 different'
246
 
                            ' sha hashes %s != %s' % (revision_id,
247
 
                                sha1, d[revision_id]))
248
 
            else:
249
 
                d[revision_id] = sha1
250
 
 
251
 
        # All of the contained revisions were checked
252
 
        # in _validate_revisions
253
 
        checked = {}
254
 
        for rev_info in self.info.revisions:
255
 
            checked[rev_info.revision_id] = True
256
 
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
257
 
                
258
 
        for (rev, rev_info) in zip(self.info.real_revisions, self.info.revisions):
259
 
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
260
 
 
261
 
        count = 0
262
 
        missing = {}
263
 
        for revision_id, sha1 in rev_to_sha.iteritems():
264
 
            if repository.has_revision(revision_id):
265
 
                testament = StrictTestament.from_revision(repository, 
266
 
                                                          revision_id)
267
 
                local_sha1 = testament.as_sha1()
268
 
                if sha1 != local_sha1:
269
 
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
270
 
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
271
 
                else:
272
 
                    count += 1
273
 
            elif revision_id not in checked:
274
 
                missing[revision_id] = sha1
275
 
 
276
 
        for inv_id, sha1 in inv_to_sha.iteritems():
277
 
            if repository.has_revision(inv_id):
278
 
                # Note: branch.get_inventory_sha1() just returns the value that
279
 
                # is stored in the revision text, and that value may be out
280
 
                # of date. This is bogus, because that means we aren't
281
 
                # validating the actual text, just that we wrote and read the
282
 
                # string. But for now, what the hell.
283
 
                local_sha1 = repository.get_inventory_sha1(inv_id)
284
 
                if sha1 != local_sha1:
285
 
                    raise BzrError('sha1 mismatch. For inventory id {%s}' 
286
 
                                   'local: %s, bundle: %s' % 
287
 
                                   (inv_id, local_sha1, sha1))
288
 
                else:
289
 
                    count += 1
290
 
 
291
 
        if len(missing) > 0:
292
 
            # I don't know if this is an error yet
293
 
            warning('Not all revision hashes could be validated.'
294
 
                    ' Unable validate %d hashes' % len(missing))
295
 
        mutter('Verified %d sha hashes for the bundle.' % count)
296
 
 
297
 
    def _validate_inventory(self, inv, revision_id):
298
 
        """At this point we should have generated the BundleTree,
299
 
        so build up an inventory, and make sure the hashes match.
300
 
        """
301
 
 
302
 
        assert inv is not None
303
 
 
304
 
        # Now we should have a complete inventory entry.
305
 
        s = serializer_v5.write_inventory_to_string(inv)
306
 
        sha1 = sha_string(s)
307
 
        # Target revision is the last entry in the real_revisions list
308
 
        rev = self.info.get_revision(revision_id)
309
 
        assert rev.revision_id == revision_id
310
 
        if sha1 != rev.inventory_sha1:
311
 
            open(',,bogus-inv', 'wb').write(s)
312
 
            warning('Inventory sha hash mismatch for revision %s. %s'
313
 
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
314
 
 
315
 
    def get_bundle(self, repository):
316
 
        """Return the meta information, and a Bundle tree which can
317
 
        be used to populate the local stores and working tree, respectively.
318
 
        """
319
 
        return self.info, self.revision_tree(repository, self.info.target)
320
 
 
321
 
    def revision_tree(self, repository, revision_id, base=None):
322
 
        revision = self.info.get_revision(revision_id)
323
 
        base = self.info.get_base(revision)
324
 
        assert base != revision_id
325
 
        self._validate_references_from_repository(repository)
326
 
        revision_info = self.info.get_revision_info(revision_id)
327
 
        inventory_revision_id = revision_id
328
 
        bundle_tree = BundleTree(repository.revision_tree(base), 
329
 
                                  inventory_revision_id)
330
 
        self._update_tree(bundle_tree, revision_id)
331
 
 
332
 
        inv = bundle_tree.inventory
333
 
        self._validate_inventory(inv, revision_id)
334
 
        self._validate_revision(inv, revision_id)
335
 
 
336
 
        return bundle_tree
337
 
 
338
 
    def _next(self):
339
 
        """yield the next line, but secretly
340
 
        keep 1 extra line for peeking.
341
 
        """
342
 
        for line in self.from_file:
343
 
            last = self._next_line
344
 
            self._next_line = line
345
 
            if last is not None:
346
 
                #mutter('yielding line: %r' % last)
347
 
                yield last
348
 
        last = self._next_line
349
 
        self._next_line = None
350
 
        #mutter('yielding line: %r' % last)
351
 
        yield last
352
 
 
353
 
    def _read_header(self):
354
 
        """Read the bzr header"""
355
 
        header = get_header()
356
 
        found = False
357
 
        for line in self._next():
358
 
            if found:
359
 
                # not all mailers will keep trailing whitespace
360
 
                if line == '#\n':
361
 
                    line = '# \n'
362
 
                if (not line.startswith('# ') or not line.endswith('\n')
363
 
                        or line[2:-1].decode('utf-8') != header[0]):
364
 
                    raise MalformedHeader('Found a header, but it'
365
 
                        ' was improperly formatted')
366
 
                header.pop(0) # We read this line.
367
 
                if not header:
368
 
                    break # We found everything.
369
 
            elif (line.startswith('#') and line.endswith('\n')):
370
 
                line = line[1:-1].strip().decode('utf-8')
371
 
                if line[:len(header_str)] == header_str:
372
 
                    if line == header[0]:
373
 
                        found = True
374
 
                    else:
375
 
                        raise MalformedHeader('Found what looks like'
376
 
                                ' a header, but did not match')
377
 
                    header.pop(0)
378
 
        else:
379
 
            raise NotABundle('Did not find an opening header')
380
 
 
381
 
    def _read_revision_header(self):
382
 
        self.info.revisions.append(RevisionInfo(None))
383
 
        for line in self._next():
384
 
            # The bzr header is terminated with a blank line
385
 
            # which does not start with '#'
386
 
            if line is None or line == '\n':
387
 
                break
388
 
            self._handle_next(line)
389
 
 
390
 
    def _read_next_entry(self, line, indent=1):
391
 
        """Read in a key-value pair
392
 
        """
393
 
        if not line.startswith('#'):
394
 
            raise MalformedHeader('Bzr header did not start with #')
395
 
        line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
396
 
        if line[:indent] == ' '*indent:
397
 
            line = line[indent:]
398
 
        if not line:
399
 
            return None, None# Ignore blank lines
400
 
 
401
 
        loc = line.find(': ')
402
 
        if loc != -1:
403
 
            key = line[:loc]
404
 
            value = line[loc+2:]
405
 
            if not value:
406
 
                value = self._read_many(indent=indent+2)
407
 
        elif line[-1:] == ':':
408
 
            key = line[:-1]
409
 
            value = self._read_many(indent=indent+2)
410
 
        else:
411
 
            raise MalformedHeader('While looking for key: value pairs,'
412
 
                    ' did not find the colon %r' % (line))
413
 
 
414
 
        key = key.replace(' ', '_')
415
 
        #mutter('found %s: %s' % (key, value))
416
 
        return key, value
417
 
 
418
 
    def _handle_next(self, line):
419
 
        if line is None:
420
 
            return
421
 
        key, value = self._read_next_entry(line, indent=1)
422
 
        mutter('_handle_next %r => %r' % (key, value))
423
 
        if key is None:
424
 
            return
425
 
 
426
 
        revision_info = self.info.revisions[-1]
427
 
        if hasattr(revision_info, key):
428
 
            if getattr(revision_info, key) is None:
429
 
                setattr(revision_info, key, value)
430
 
            else:
431
 
                raise MalformedHeader('Duplicated Key: %s' % key)
432
 
        else:
433
 
            # What do we do with a key we don't recognize
434
 
            raise MalformedHeader('Unknown Key: "%s"' % key)
435
 
    
436
 
    def _read_many(self, indent):
437
 
        """If a line ends with no entry, that means that it should be
438
 
        followed with multiple lines of values.
439
 
 
440
 
        This detects the end of the list, because it will be a line that
441
 
        does not start properly indented.
442
 
        """
443
 
        values = []
444
 
        start = '#' + (' '*indent)
445
 
 
446
 
        if self._next_line is None or self._next_line[:len(start)] != start:
447
 
            return values
448
 
 
449
 
        for line in self._next():
450
 
            values.append(line[len(start):-1].decode('utf-8'))
451
 
            if self._next_line is None or self._next_line[:len(start)] != start:
452
 
                break
453
 
        return values
454
 
 
455
 
    def _read_one_patch(self):
456
 
        """Read in one patch, return the complete patch, along with
457
 
        the next line.
458
 
 
459
 
        :return: action, lines, do_continue
460
 
        """
461
 
        #mutter('_read_one_patch: %r' % self._next_line)
462
 
        # Peek and see if there are no patches
463
 
        if self._next_line is None or self._next_line.startswith('#'):
464
 
            return None, [], False
465
 
 
466
 
        first = True
467
 
        lines = []
468
 
        for line in self._next():
469
 
            if first:
470
 
                if not line.startswith('==='):
471
 
                    raise MalformedPatches('The first line of all patches'
472
 
                        ' should be a bzr meta line "==="'
473
 
                        ': %r' % line)
474
 
                action = line[4:-1].decode('utf-8')
475
 
            elif line.startswith('... '):
476
 
                action += line[len('... '):-1].decode('utf-8')
477
 
 
478
 
            if (self._next_line is not None and 
479
 
                self._next_line.startswith('===')):
480
 
                return action, lines, True
481
 
            elif self._next_line is None or self._next_line.startswith('#'):
482
 
                return action, lines, False
483
 
 
484
 
            if first:
485
 
                first = False
486
 
            elif not line.startswith('... '):
487
 
                lines.append(line)
488
 
 
489
 
        return action, lines, False
490
 
            
491
 
    def _read_patches(self):
492
 
        do_continue = True
493
 
        revision_actions = []
494
 
        while do_continue:
495
 
            action, lines, do_continue = self._read_one_patch()
496
 
            if action is not None:
497
 
                revision_actions.append((action, lines))
498
 
        assert self.info.revisions[-1].tree_actions is None
499
 
        self.info.revisions[-1].tree_actions = revision_actions
500
 
 
501
 
    def _read_footer(self):
502
 
        """Read the rest of the meta information.
503
 
 
504
 
        :param first_line:  The previous step iterates past what it
505
 
                            can handle. That extra line is given here.
506
 
        """
507
 
        for line in self._next():
508
 
            self._handle_next(line)
509
 
            if not self._next_line.startswith('#'):
510
 
                self._next().next()
511
 
                break
512
 
            if self._next_line is None:
513
 
                break
514
 
 
515
 
    def _update_tree(self, bundle_tree, revision_id):
516
 
        """This fills out a BundleTree based on the information
517
 
        that was read in.
518
 
 
519
 
        :param bundle_tree: A BundleTree to update with the new information.
520
 
        """
521
 
 
522
 
        def get_rev_id(last_changed, path, kind):
523
 
            if last_changed is not None:
524
 
                changed_revision_id = last_changed.decode('utf-8')
525
 
            else:
526
 
                changed_revision_id = revision_id
527
 
            bundle_tree.note_last_changed(path, changed_revision_id)
528
 
            return changed_revision_id
529
 
 
530
 
        def extra_info(info, new_path):
531
 
            last_changed = None
532
 
            encoding = None
533
 
            for info_item in info:
534
 
                try:
535
 
                    name, value = info_item.split(':', 1)
536
 
                except ValueError:
537
 
                    raise 'Value %r has no colon' % info_item
538
 
                if name == 'last-changed':
539
 
                    last_changed = value
540
 
                elif name == 'executable':
541
 
                    assert value in ('yes', 'no'), value
542
 
                    val = (value == 'yes')
543
 
                    bundle_tree.note_executable(new_path, val)
544
 
                elif name == 'target':
545
 
                    bundle_tree.note_target(new_path, value)
546
 
                elif name == 'encoding':
547
 
                    encoding = value
548
 
            return last_changed, encoding
549
 
 
550
 
        def do_patch(path, lines, encoding):
551
 
            if encoding is not None:
552
 
                assert encoding == 'base64'
553
 
                patch = base64.decodestring(''.join(lines))
554
 
            else:
555
 
                patch =  ''.join(lines)
556
 
            bundle_tree.note_patch(path, patch)
557
 
 
558
 
        def renamed(kind, extra, lines):
559
 
            info = extra.split(' // ')
560
 
            if len(info) < 2:
561
 
                raise BzrError('renamed action lines need both a from and to'
562
 
                        ': %r' % extra)
563
 
            old_path = info[0]
564
 
            if info[1].startswith('=> '):
565
 
                new_path = info[1][3:]
566
 
            else:
567
 
                new_path = info[1]
568
 
 
569
 
            bundle_tree.note_rename(old_path, new_path)
570
 
            last_modified, encoding = extra_info(info[2:], new_path)
571
 
            revision = get_rev_id(last_modified, new_path, kind)
572
 
            if lines:
573
 
                do_patch(new_path, lines, encoding)
574
 
 
575
 
        def removed(kind, extra, lines):
576
 
            info = extra.split(' // ')
577
 
            if len(info) > 1:
578
 
                # TODO: in the future we might allow file ids to be
579
 
                # given for removed entries
580
 
                raise BzrError('removed action lines should only have the path'
581
 
                        ': %r' % extra)
582
 
            path = info[0]
583
 
            bundle_tree.note_deletion(path)
584
 
 
585
 
        def added(kind, extra, lines):
586
 
            info = extra.split(' // ')
587
 
            if len(info) <= 1:
588
 
                raise BzrError('add action lines require the path and file id'
589
 
                        ': %r' % extra)
590
 
            elif len(info) > 5:
591
 
                raise BzrError('add action lines have fewer than 5 entries.'
592
 
                        ': %r' % extra)
593
 
            path = info[0]
594
 
            if not info[1].startswith('file-id:'):
595
 
                raise BzrError('The file-id should follow the path for an add'
596
 
                        ': %r' % extra)
597
 
            file_id = info[1][8:]
598
 
 
599
 
            bundle_tree.note_id(file_id, path, kind)
600
 
            # this will be overridden in extra_info if executable is specified.
601
 
            bundle_tree.note_executable(path, False)
602
 
            last_changed, encoding = extra_info(info[2:], path)
603
 
            revision = get_rev_id(last_changed, path, kind)
604
 
            if kind == 'directory':
605
 
                return
606
 
            do_patch(path, lines, encoding)
607
 
 
608
 
        def modified(kind, extra, lines):
609
 
            info = extra.split(' // ')
610
 
            if len(info) < 1:
611
 
                raise BzrError('modified action lines have at least'
612
 
                        'the path in them: %r' % extra)
613
 
            path = info[0]
614
 
 
615
 
            last_modified, encoding = extra_info(info[1:], path)
616
 
            revision = get_rev_id(last_modified, path, kind)
617
 
            if lines:
618
 
                do_patch(path, lines, encoding)
619
 
            
620
 
        valid_actions = {
621
 
            'renamed':renamed,
622
 
            'removed':removed,
623
 
            'added':added,
624
 
            'modified':modified
625
 
        }
626
 
        for action_line, lines in \
627
 
            self.info.get_revision_info(revision_id).tree_actions:
628
 
            first = action_line.find(' ')
629
 
            if first == -1:
630
 
                raise BzrError('Bogus action line'
631
 
                        ' (no opening space): %r' % action_line)
632
 
            second = action_line.find(' ', first+1)
633
 
            if second == -1:
634
 
                raise BzrError('Bogus action line'
635
 
                        ' (missing second space): %r' % action_line)
636
 
            action = action_line[:first]
637
 
            kind = action_line[first+1:second]
638
 
            if kind not in ('file', 'directory', 'symlink'):
639
 
                raise BzrError('Bogus action line'
640
 
                        ' (invalid object kind %r): %r' % (kind, action_line))
641
 
            extra = action_line[second+1:]
642
 
 
643
 
            if action not in valid_actions:
644
 
                raise BzrError('Bogus action line'
645
 
                        ' (unrecognized action): %r' % action_line)
646
 
            valid_actions[action](kind, extra, lines)
647
 
 
648
 
 
649
 
class BundleTree(Tree):
650
 
    def __init__(self, base_tree, revision_id):
651
 
        self.base_tree = base_tree
652
 
        self._renamed = {} # Mapping from old_path => new_path
653
 
        self._renamed_r = {} # new_path => old_path
654
 
        self._new_id = {} # new_path => new_id
655
 
        self._new_id_r = {} # new_id => new_path
656
 
        self._kinds = {} # new_id => kind
657
 
        self._last_changed = {} # new_id => revision_id
658
 
        self._executable = {} # new_id => executable value
659
 
        self.patches = {}
660
 
        self._targets = {} # new path => new symlink target
661
 
        self.deleted = []
662
 
        self.contents_by_id = True
663
 
        self.revision_id = revision_id
664
 
        self._inventory = None
665
 
 
666
 
    def __str__(self):
667
 
        return pprint.pformat(self.__dict__)
668
 
 
669
 
    def note_rename(self, old_path, new_path):
670
 
        """A file/directory has been renamed from old_path => new_path"""
671
 
        assert not self._renamed.has_key(new_path)
672
 
        assert not self._renamed_r.has_key(old_path)
673
 
        self._renamed[new_path] = old_path
674
 
        self._renamed_r[old_path] = new_path
675
 
 
676
 
    def note_id(self, new_id, new_path, kind='file'):
677
 
        """Files that don't exist in base need a new id."""
678
 
        self._new_id[new_path] = new_id
679
 
        self._new_id_r[new_id] = new_path
680
 
        self._kinds[new_id] = kind
681
 
 
682
 
    def note_last_changed(self, file_id, revision_id):
683
 
        if (self._last_changed.has_key(file_id)
684
 
                and self._last_changed[file_id] != revision_id):
685
 
            raise BzrError('Mismatched last-changed revision for file_id {%s}'
686
 
                    ': %s != %s' % (file_id,
687
 
                                    self._last_changed[file_id],
688
 
                                    revision_id))
689
 
        self._last_changed[file_id] = revision_id
690
 
 
691
 
    def note_patch(self, new_path, patch):
692
 
        """There is a patch for a given filename."""
693
 
        self.patches[new_path] = patch
694
 
 
695
 
    def note_target(self, new_path, target):
696
 
        """The symlink at the new path has the given target"""
697
 
        self._targets[new_path] = target
698
 
 
699
 
    def note_deletion(self, old_path):
700
 
        """The file at old_path has been deleted."""
701
 
        self.deleted.append(old_path)
702
 
 
703
 
    def note_executable(self, new_path, executable):
704
 
        self._executable[new_path] = executable
705
 
 
706
 
    def old_path(self, new_path):
707
 
        """Get the old_path (path in the base_tree) for the file at new_path"""
708
 
        assert new_path[:1] not in ('\\', '/')
709
 
        old_path = self._renamed.get(new_path)
710
 
        if old_path is not None:
711
 
            return old_path
712
 
        dirname,basename = os.path.split(new_path)
713
 
        # dirname is not '' doesn't work, because
714
 
        # dirname may be a unicode entry, and is
715
 
        # requires the objects to be identical
716
 
        if dirname != '':
717
 
            old_dir = self.old_path(dirname)
718
 
            if old_dir is None:
719
 
                old_path = None
720
 
            else:
721
 
                old_path = os.path.join(old_dir, basename)
722
 
        else:
723
 
            old_path = new_path
724
 
        #If the new path wasn't in renamed, the old one shouldn't be in
725
 
        #renamed_r
726
 
        if self._renamed_r.has_key(old_path):
727
 
            return None
728
 
        return old_path 
729
 
 
730
 
    def new_path(self, old_path):
731
 
        """Get the new_path (path in the target_tree) for the file at old_path
732
 
        in the base tree.
733
 
        """
734
 
        assert old_path[:1] not in ('\\', '/')
735
 
        new_path = self._renamed_r.get(old_path)
736
 
        if new_path is not None:
737
 
            return new_path
738
 
        if self._renamed.has_key(new_path):
739
 
            return None
740
 
        dirname,basename = os.path.split(old_path)
741
 
        if dirname != '':
742
 
            new_dir = self.new_path(dirname)
743
 
            if new_dir is None:
744
 
                new_path = None
745
 
            else:
746
 
                new_path = os.path.join(new_dir, basename)
747
 
        else:
748
 
            new_path = old_path
749
 
        #If the old path wasn't in renamed, the new one shouldn't be in
750
 
        #renamed_r
751
 
        if self._renamed.has_key(new_path):
752
 
            return None
753
 
        return new_path 
754
 
 
755
 
    def path2id(self, path):
756
 
        """Return the id of the file present at path in the target tree."""
757
 
        file_id = self._new_id.get(path)
758
 
        if file_id is not None:
759
 
            return file_id
760
 
        old_path = self.old_path(path)
761
 
        if old_path is None:
762
 
            return None
763
 
        if old_path in self.deleted:
764
 
            return None
765
 
        if hasattr(self.base_tree, 'path2id'):
766
 
            return self.base_tree.path2id(old_path)
767
 
        else:
768
 
            return self.base_tree.inventory.path2id(old_path)
769
 
 
770
 
    def id2path(self, file_id):
771
 
        """Return the new path in the target tree of the file with id file_id"""
772
 
        path = self._new_id_r.get(file_id)
773
 
        if path is not None:
774
 
            return path
775
 
        old_path = self.base_tree.id2path(file_id)
776
 
        if old_path is None:
777
 
            return None
778
 
        if old_path in self.deleted:
779
 
            return None
780
 
        return self.new_path(old_path)
781
 
 
782
 
    def old_contents_id(self, file_id):
783
 
        """Return the id in the base_tree for the given file_id.
784
 
        Return None if the file did not exist in base.
785
 
        """
786
 
        if self.contents_by_id:
787
 
            if self.base_tree.has_id(file_id):
788
 
                return file_id
789
 
            else:
790
 
                return None
791
 
        new_path = self.id2path(file_id)
792
 
        return self.base_tree.path2id(new_path)
793
 
        
794
 
    def get_file(self, file_id):
795
 
        """Return a file-like object containing the new contents of the
796
 
        file given by file_id.
797
 
 
798
 
        TODO:   It might be nice if this actually generated an entry
799
 
                in the text-store, so that the file contents would
800
 
                then be cached.
801
 
        """
802
 
        base_id = self.old_contents_id(file_id)
803
 
        if base_id is not None:
804
 
            patch_original = self.base_tree.get_file(base_id)
805
 
        else:
806
 
            patch_original = None
807
 
        file_patch = self.patches.get(self.id2path(file_id))
808
 
        if file_patch is None:
809
 
            if (patch_original is None and 
810
 
                self.get_kind(file_id) == 'directory'):
811
 
                return StringIO()
812
 
            assert patch_original is not None, "None: %s" % file_id
813
 
            return patch_original
814
 
 
815
 
        assert not file_patch.startswith('\\'), \
816
 
            'Malformed patch for %s, %r' % (file_id, file_patch)
817
 
        return patched_file(file_patch, patch_original)
818
 
 
819
 
    def get_symlink_target(self, file_id):
820
 
        new_path = self.id2path(file_id)
821
 
        try:
822
 
            return self._targets[new_path]
823
 
        except KeyError:
824
 
            return self.base_tree.get_symlink_target(file_id)
825
 
 
826
 
    def get_kind(self, file_id):
827
 
        if file_id in self._kinds:
828
 
            return self._kinds[file_id]
829
 
        return self.base_tree.inventory[file_id].kind
830
 
 
831
 
    def is_executable(self, file_id):
832
 
        path = self.id2path(file_id)
833
 
        if path in self._executable:
834
 
            return self._executable[path]
835
 
        else:
836
 
            return self.base_tree.inventory[file_id].executable
837
 
 
838
 
    def get_last_changed(self, file_id):
839
 
        path = self.id2path(file_id)
840
 
        if path in self._last_changed:
841
 
            return self._last_changed[path]
842
 
        return self.base_tree.inventory[file_id].revision
843
 
 
844
 
    def get_size_and_sha1(self, file_id):
845
 
        """Return the size and sha1 hash of the given file id.
846
 
        If the file was not locally modified, this is extracted
847
 
        from the base_tree. Rather than re-reading the file.
848
 
        """
849
 
        new_path = self.id2path(file_id)
850
 
        if new_path is None:
851
 
            return None, None
852
 
        if new_path not in self.patches:
853
 
            # If the entry does not have a patch, then the
854
 
            # contents must be the same as in the base_tree
855
 
            ie = self.base_tree.inventory[file_id]
856
 
            if ie.text_size is None:
857
 
                return ie.text_size, ie.text_sha1
858
 
            return int(ie.text_size), ie.text_sha1
859
 
        fileobj = self.get_file(file_id)
860
 
        content = fileobj.read()
861
 
        return len(content), sha_string(content)
862
 
 
863
 
    def _get_inventory(self):
864
 
        """Build up the inventory entry for the BundleTree.
865
 
 
866
 
        This need to be called before ever accessing self.inventory
867
 
        """
868
 
        from os.path import dirname, basename
869
 
 
870
 
        assert self.base_tree is not None
871
 
        base_inv = self.base_tree.inventory
872
 
        root_id = base_inv.root.file_id
873
 
        try:
874
 
            # New inventories have a unique root_id
875
 
            inv = Inventory(root_id, self.revision_id)
876
 
        except TypeError:
877
 
            inv = Inventory(revision_id=self.revision_id)
878
 
 
879
 
        def add_entry(file_id):
880
 
            path = self.id2path(file_id)
881
 
            if path is None:
882
 
                return
883
 
            parent_path = dirname(path)
884
 
            if parent_path == u'':
885
 
                parent_id = root_id
886
 
            else:
887
 
                parent_id = self.path2id(parent_path)
888
 
 
889
 
            kind = self.get_kind(file_id)
890
 
            revision_id = self.get_last_changed(file_id)
891
 
 
892
 
            name = basename(path)
893
 
            if kind == 'directory':
894
 
                ie = InventoryDirectory(file_id, name, parent_id)
895
 
            elif kind == 'file':
896
 
                ie = InventoryFile(file_id, name, parent_id)
897
 
                ie.executable = self.is_executable(file_id)
898
 
            elif kind == 'symlink':
899
 
                ie = InventoryLink(file_id, name, parent_id)
900
 
                ie.symlink_target = self.get_symlink_target(file_id)
901
 
            ie.revision = revision_id
902
 
 
903
 
            if kind in ('directory', 'symlink'):
904
 
                ie.text_size, ie.text_sha1 = None, None
905
 
            else:
906
 
                ie.text_size, ie.text_sha1 = self.get_size_and_sha1(file_id)
907
 
            if (ie.text_size is None) and (kind == 'file'):
908
 
                raise BzrError('Got a text_size of None for file_id %r' % file_id)
909
 
            inv.add(ie)
910
 
 
911
 
        sorted_entries = self.sorted_path_id()
912
 
        for path, file_id in sorted_entries:
913
 
            if file_id == inv.root.file_id:
914
 
                continue
915
 
            add_entry(file_id)
916
 
 
917
 
        return inv
918
 
 
919
 
    # Have to overload the inherited inventory property
920
 
    # because _get_inventory is only called in the parent.
921
 
    # Reading the docs, property() objects do not use
922
 
    # overloading, they use the function as it was defined
923
 
    # at that instant
924
 
    inventory = property(_get_inventory)
925
 
 
926
 
    def __iter__(self):
927
 
        for path, entry in self.inventory.iter_entries():
928
 
            yield entry.file_id
929
 
 
930
 
    def sorted_path_id(self):
931
 
        paths = []
932
 
        for result in self._new_id.iteritems():
933
 
            paths.append(result)
934
 
        for id in self.base_tree:
935
 
            path = self.id2path(id)
936
 
            if path is None:
937
 
                continue
938
 
            paths.append((path, id))
939
 
        paths.sort()
940
 
        return paths
941
 
 
942
 
 
943
 
def patched_file(file_patch, original):
944
 
    """Produce a file-like object with the patched version of a text"""
945
 
    from bzrlib.patches import iter_patched
946
 
    from bzrlib.iterablefile import IterableFile
947
 
    if file_patch == "":
948
 
        return IterableFile(())
949
 
    return IterableFile(iter_patched(original, file_patch.splitlines(True)))