~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/bundle/bundle_data.py

  • Committer: Robert Collins
  • Date: 2007-03-08 04:06:06 UTC
  • mfrom: (2323.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 2442.
  • Revision ID: robertc@robertcollins.net-20070308040606-84gsniv56huiyjt4
Merge bzr.dev.

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) 2006 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Read in a bundle stream, and process it into a BundleReader object."""
5
18
 
6
19
import base64
7
20
from cStringIO import StringIO
8
21
import os
9
22
import pprint
10
23
 
 
24
from bzrlib import (
 
25
    osutils,
 
26
    )
 
27
import bzrlib.errors
11
28
from bzrlib.errors import (TestamentMismatch, BzrError, 
12
 
                           MalformedHeader, MalformedPatches)
13
 
from bzrlib.bundle.common import get_header, header_str
 
29
                           MalformedHeader, MalformedPatches, NotABundle)
14
30
from bzrlib.inventory import (Inventory, InventoryEntry,
15
31
                              InventoryDirectory, InventoryFile,
16
32
                              InventoryLink)
17
 
from bzrlib.osutils import sha_file, sha_string
 
33
from bzrlib.osutils import sha_file, sha_string, pathjoin
18
34
from bzrlib.revision import Revision, NULL_REVISION
19
35
from bzrlib.testament import StrictTestament
20
36
from bzrlib.trace import mutter, warning
 
37
import bzrlib.transport
21
38
from bzrlib.tree import Tree
 
39
import bzrlib.urlutils
22
40
from bzrlib.xml5 import serializer_v5
23
41
 
24
42
 
94
112
        split up, based on the assumptions that can be made
95
113
        when information is missing.
96
114
        """
97
 
        from bzrlib.bundle.common import unpack_highres_date
 
115
        from bzrlib.bundle.serializer import unpack_highres_date
98
116
        # Put in all of the guessable information.
99
117
        if not self.timestamp and self.date:
100
118
            self.timestamp, self.timezone = unpack_highres_date(self.date)
152
170
                return r
153
171
        raise KeyError(revision_id)
154
172
 
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
 
173
    def revision_tree(self, repository, revision_id, base=None):
 
174
        revision_id = osutils.safe_revision_id(revision_id)
 
175
        revision = self.get_revision(revision_id)
 
176
        base = self.get_base(revision)
 
177
        assert base != revision_id
 
178
        self._validate_references_from_repository(repository)
 
179
        revision_info = self.get_revision_info(revision_id)
 
180
        inventory_revision_id = revision_id
 
181
        bundle_tree = BundleTree(repository.revision_tree(base), 
 
182
                                  inventory_revision_id)
 
183
        self._update_tree(bundle_tree, revision_id)
 
184
 
 
185
        inv = bundle_tree.inventory
 
186
        self._validate_inventory(inv, revision_id)
 
187
        self._validate_revision(inv, revision_id)
 
188
 
 
189
        return bundle_tree
211
190
 
212
191
    def _validate_references_from_repository(self, repository):
213
192
        """Now that we have a repository which should have some of the
235
214
        # All of the contained revisions were checked
236
215
        # in _validate_revisions
237
216
        checked = {}
238
 
        for rev_info in self.info.revisions:
 
217
        for rev_info in self.revisions:
239
218
            checked[rev_info.revision_id] = True
240
219
            add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1)
241
220
                
242
 
        for (rev, rev_info) in zip(self.info.real_revisions, self.info.revisions):
 
221
        for (rev, rev_info) in zip(self.real_revisions, self.revisions):
243
222
            add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1)
244
223
 
245
224
        count = 0
248
227
            if repository.has_revision(revision_id):
249
228
                testament = StrictTestament.from_revision(repository, 
250
229
                                                          revision_id)
251
 
                local_sha1 = testament.as_sha1()
 
230
                local_sha1 = self._testament_sha1_from_revision(repository,
 
231
                                                                revision_id)
252
232
                if sha1 != local_sha1:
253
233
                    raise BzrError('sha1 mismatch. For revision id {%s}' 
254
234
                            'local: %s, bundle: %s' % (revision_id, local_sha1, sha1))
289
269
        s = serializer_v5.write_inventory_to_string(inv)
290
270
        sha1 = sha_string(s)
291
271
        # Target revision is the last entry in the real_revisions list
292
 
        rev = self.info.get_revision(revision_id)
 
272
        rev = self.get_revision(revision_id)
293
273
        assert rev.revision_id == revision_id
294
274
        if sha1 != rev.inventory_sha1:
295
275
            open(',,bogus-inv', 'wb').write(s)
296
276
            warning('Inventory sha hash mismatch for revision %s. %s'
297
277
                    ' != %s' % (revision_id, sha1, rev.inventory_sha1))
298
278
 
299
 
    def get_bundle(self, repository):
300
 
        """Return the meta information, and a Bundle tree which can
301
 
        be used to populate the local stores and working tree, respectively.
302
 
        """
303
 
        return self.info, self.revision_tree(repository, self.info.target)
304
 
 
305
 
    def revision_tree(self, repository, revision_id, base=None):
306
 
        revision = self.info.get_revision(revision_id)
307
 
        base = self.info.get_base(revision)
308
 
        assert base != revision_id
309
 
        self._validate_references_from_repository(repository)
310
 
        revision_info = self.info.get_revision_info(revision_id)
311
 
        inventory_revision_id = revision_id
312
 
        bundle_tree = BundleTree(repository.revision_tree(base), 
313
 
                                  inventory_revision_id)
314
 
        self._update_tree(bundle_tree, revision_id)
315
 
 
316
 
        inv = bundle_tree.inventory
317
 
        self._validate_inventory(inv, revision_id)
318
 
        self._validate_revision(inv, revision_id)
319
 
 
320
 
        return bundle_tree
321
 
 
322
 
    def _next(self):
323
 
        """yield the next line, but secretly
324
 
        keep 1 extra line for peeking.
325
 
        """
326
 
        for line in self.from_file:
327
 
            last = self._next_line
328
 
            self._next_line = line
329
 
            if last is not None:
330
 
                #mutter('yielding line: %r' % last)
331
 
                yield last
332
 
        last = self._next_line
333
 
        self._next_line = None
334
 
        #mutter('yielding line: %r' % last)
335
 
        yield last
336
 
 
337
 
    def _read_header(self):
338
 
        """Read the bzr header"""
339
 
        header = get_header()
340
 
        found = False
341
 
        for line in self._next():
342
 
            if found:
343
 
                # not all mailers will keep trailing whitespace
344
 
                if line == '#\n':
345
 
                    line = '# \n'
346
 
                if (not line.startswith('# ') or not line.endswith('\n')
347
 
                        or line[2:-1].decode('utf-8') != header[0]):
348
 
                    raise MalformedHeader('Found a header, but it'
349
 
                        ' was improperly formatted')
350
 
                header.pop(0) # We read this line.
351
 
                if not header:
352
 
                    break # We found everything.
353
 
            elif (line.startswith('#') and line.endswith('\n')):
354
 
                line = line[1:-1].strip().decode('utf-8')
355
 
                if line[:len(header_str)] == header_str:
356
 
                    if line == header[0]:
357
 
                        found = True
358
 
                    else:
359
 
                        raise MalformedHeader('Found what looks like'
360
 
                                ' a header, but did not match')
361
 
                    header.pop(0)
362
 
        else:
363
 
            raise MalformedHeader('Did not find an opening header')
364
 
 
365
 
    def _read_revision_header(self):
366
 
        self.info.revisions.append(RevisionInfo(None))
367
 
        for line in self._next():
368
 
            # The bzr header is terminated with a blank line
369
 
            # which does not start with '#'
370
 
            if line is None or line == '\n':
371
 
                break
372
 
            self._handle_next(line)
373
 
 
374
 
    def _read_next_entry(self, line, indent=1):
375
 
        """Read in a key-value pair
376
 
        """
377
 
        if not line.startswith('#'):
378
 
            raise MalformedHeader('Bzr header did not start with #')
379
 
        line = line[1:-1].decode('utf-8') # Remove the '#' and '\n'
380
 
        if line[:indent] == ' '*indent:
381
 
            line = line[indent:]
382
 
        if not line:
383
 
            return None, None# Ignore blank lines
384
 
 
385
 
        loc = line.find(': ')
386
 
        if loc != -1:
387
 
            key = line[:loc]
388
 
            value = line[loc+2:]
389
 
            if not value:
390
 
                value = self._read_many(indent=indent+2)
391
 
        elif line[-1:] == ':':
392
 
            key = line[:-1]
393
 
            value = self._read_many(indent=indent+2)
394
 
        else:
395
 
            raise MalformedHeader('While looking for key: value pairs,'
396
 
                    ' did not find the colon %r' % (line))
397
 
 
398
 
        key = key.replace(' ', '_')
399
 
        #mutter('found %s: %s' % (key, value))
400
 
        return key, value
401
 
 
402
 
    def _handle_next(self, line):
403
 
        if line is None:
404
 
            return
405
 
        key, value = self._read_next_entry(line, indent=1)
406
 
        mutter('_handle_next %r => %r' % (key, value))
407
 
        if key is None:
408
 
            return
409
 
 
410
 
        revision_info = self.info.revisions[-1]
411
 
        if hasattr(revision_info, key):
412
 
            if getattr(revision_info, key) is None:
413
 
                setattr(revision_info, key, value)
414
 
            else:
415
 
                raise MalformedHeader('Duplicated Key: %s' % key)
416
 
        else:
417
 
            # What do we do with a key we don't recognize
418
 
            raise MalformedHeader('Unknown Key: "%s"' % key)
419
 
    
420
 
    def _read_many(self, indent):
421
 
        """If a line ends with no entry, that means that it should be
422
 
        followed with multiple lines of values.
423
 
 
424
 
        This detects the end of the list, because it will be a line that
425
 
        does not start properly indented.
426
 
        """
427
 
        values = []
428
 
        start = '#' + (' '*indent)
429
 
 
430
 
        if self._next_line is None or self._next_line[:len(start)] != start:
431
 
            return values
432
 
 
433
 
        for line in self._next():
434
 
            values.append(line[len(start):-1].decode('utf-8'))
435
 
            if self._next_line is None or self._next_line[:len(start)] != start:
436
 
                break
437
 
        return values
438
 
 
439
 
    def _read_one_patch(self):
440
 
        """Read in one patch, return the complete patch, along with
441
 
        the next line.
442
 
 
443
 
        :return: action, lines, do_continue
444
 
        """
445
 
        #mutter('_read_one_patch: %r' % self._next_line)
446
 
        # Peek and see if there are no patches
447
 
        if self._next_line is None or self._next_line.startswith('#'):
448
 
            return None, [], False
449
 
 
450
 
        first = True
451
 
        lines = []
452
 
        for line in self._next():
453
 
            if first:
454
 
                if not line.startswith('==='):
455
 
                    raise MalformedPatches('The first line of all patches'
456
 
                        ' should be a bzr meta line "==="'
457
 
                        ': %r' % line)
458
 
                action = line[4:-1].decode('utf-8')
459
 
            elif line.startswith('... '):
460
 
                action += line[len('... '):-1].decode('utf-8')
461
 
 
462
 
            if (self._next_line is not None and 
463
 
                self._next_line.startswith('===')):
464
 
                return action, lines, True
465
 
            elif self._next_line is None or self._next_line.startswith('#'):
466
 
                return action, lines, False
467
 
 
468
 
            if first:
469
 
                first = False
470
 
            elif not line.startswith('... '):
471
 
                lines.append(line)
472
 
 
473
 
        return action, lines, False
474
 
            
475
 
    def _read_patches(self):
476
 
        do_continue = True
477
 
        revision_actions = []
478
 
        while do_continue:
479
 
            action, lines, do_continue = self._read_one_patch()
480
 
            if action is not None:
481
 
                revision_actions.append((action, lines))
482
 
        assert self.info.revisions[-1].tree_actions is None
483
 
        self.info.revisions[-1].tree_actions = revision_actions
484
 
 
485
 
    def _read_footer(self):
486
 
        """Read the rest of the meta information.
487
 
 
488
 
        :param first_line:  The previous step iterates past what it
489
 
                            can handle. That extra line is given here.
490
 
        """
491
 
        for line in self._next():
492
 
            self._handle_next(line)
493
 
            if not self._next_line.startswith('#'):
494
 
                self._next().next()
495
 
                break
496
 
            if self._next_line is None:
497
 
                break
 
279
    def _validate_revision(self, inventory, revision_id):
 
280
        """Make sure all revision entries match their checksum."""
 
281
 
 
282
        # This is a mapping from each revision id to it's sha hash
 
283
        rev_to_sha1 = {}
 
284
        
 
285
        rev = self.get_revision(revision_id)
 
286
        rev_info = self.get_revision_info(revision_id)
 
287
        assert rev.revision_id == rev_info.revision_id
 
288
        assert rev.revision_id == revision_id
 
289
        sha1 = self._testament_sha1(rev, inventory)
 
290
        if sha1 != rev_info.sha1:
 
291
            raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1)
 
292
        if rev.revision_id in rev_to_sha1:
 
293
            raise BzrError('Revision {%s} given twice in the list'
 
294
                    % (rev.revision_id))
 
295
        rev_to_sha1[rev.revision_id] = sha1
498
296
 
499
297
    def _update_tree(self, bundle_tree, revision_id):
500
298
        """This fills out a BundleTree based on the information
505
303
 
506
304
        def get_rev_id(last_changed, path, kind):
507
305
            if last_changed is not None:
508
 
                changed_revision_id = last_changed.decode('utf-8')
 
306
                # last_changed will be a Unicode string because of how it was
 
307
                # read. Convert it back to utf8.
 
308
                changed_revision_id = osutils.safe_revision_id(last_changed,
 
309
                                                               warn=False)
509
310
            else:
510
311
                changed_revision_id = revision_id
511
312
            bundle_tree.note_last_changed(path, changed_revision_id)
578
379
            if not info[1].startswith('file-id:'):
579
380
                raise BzrError('The file-id should follow the path for an add'
580
381
                        ': %r' % extra)
581
 
            file_id = info[1][8:]
 
382
            # This will be Unicode because of how the stream is read. Turn it
 
383
            # back into a utf8 file_id
 
384
            file_id = osutils.safe_file_id(info[1][8:], warn=False)
582
385
 
583
386
            bundle_tree.note_id(file_id, path, kind)
584
387
            # this will be overridden in extra_info if executable is specified.
608
411
            'modified':modified
609
412
        }
610
413
        for action_line, lines in \
611
 
            self.info.get_revision_info(revision_id).tree_actions:
 
414
            self.get_revision_info(revision_id).tree_actions:
612
415
            first = action_line.find(' ')
613
416
            if first == -1:
614
417
                raise BzrError('Bogus action line'
652
455
 
653
456
    def note_rename(self, old_path, new_path):
654
457
        """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)
 
458
        assert new_path not in self._renamed
 
459
        assert old_path not in self._renamed_r
657
460
        self._renamed[new_path] = old_path
658
461
        self._renamed_r[old_path] = new_path
659
462
 
664
467
        self._kinds[new_id] = kind
665
468
 
666
469
    def note_last_changed(self, file_id, revision_id):
667
 
        if (self._last_changed.has_key(file_id)
 
470
        if (file_id in self._last_changed
668
471
                and self._last_changed[file_id] != revision_id):
669
472
            raise BzrError('Mismatched last-changed revision for file_id {%s}'
670
473
                    ': %s != %s' % (file_id,
702
505
            if old_dir is None:
703
506
                old_path = None
704
507
            else:
705
 
                old_path = os.path.join(old_dir, basename)
 
508
                old_path = pathjoin(old_dir, basename)
706
509
        else:
707
510
            old_path = new_path
708
511
        #If the new path wasn't in renamed, the old one shouldn't be in
709
512
        #renamed_r
710
 
        if self._renamed_r.has_key(old_path):
 
513
        if old_path in self._renamed_r:
711
514
            return None
712
515
        return old_path 
713
516
 
719
522
        new_path = self._renamed_r.get(old_path)
720
523
        if new_path is not None:
721
524
            return new_path
722
 
        if self._renamed.has_key(new_path):
 
525
        if new_path in self._renamed:
723
526
            return None
724
527
        dirname,basename = os.path.split(old_path)
725
528
        if dirname != '':
727
530
            if new_dir is None:
728
531
                new_path = None
729
532
            else:
730
 
                new_path = os.path.join(new_dir, basename)
 
533
                new_path = pathjoin(new_dir, basename)
731
534
        else:
732
535
            new_path = old_path
733
536
        #If the old path wasn't in renamed, the new one shouldn't be in
734
537
        #renamed_r
735
 
        if self._renamed.has_key(new_path):
 
538
        if new_path in self._renamed:
736
539
            return None
737
540
        return new_path 
738
541
 
746
549
            return None
747
550
        if old_path in self.deleted:
748
551
            return None
749
 
        if hasattr(self.base_tree, 'path2id'):
 
552
        if getattr(self.base_tree, 'path2id', None) is not None:
750
553
            return self.base_tree.path2id(old_path)
751
554
        else:
752
555
            return self.base_tree.inventory.path2id(old_path)
784
587
                then be cached.
785
588
        """
786
589
        base_id = self.old_contents_id(file_id)
787
 
        if base_id is not None:
 
590
        if (base_id is not None and
 
591
            base_id != self.base_tree.inventory.root.file_id):
788
592
            patch_original = self.base_tree.get_file(base_id)
789
593
        else:
790
594
            patch_original = None
853
657
 
854
658
        assert self.base_tree is not None
855
659
        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)
 
660
        inv = Inventory(None, self.revision_id)
862
661
 
863
662
        def add_entry(file_id):
864
663
            path = self.id2path(file_id)
865
664
            if path is None:
866
665
                return
867
 
            parent_path = dirname(path)
868
 
            if parent_path == u'':
869
 
                parent_id = root_id
 
666
            if path == '':
 
667
                parent_id = None
870
668
            else:
 
669
                parent_path = dirname(path)
871
670
                parent_id = self.path2id(parent_path)
872
671
 
873
672
            kind = self.get_kind(file_id)
894
693
 
895
694
        sorted_entries = self.sorted_path_id()
896
695
        for path, file_id in sorted_entries:
897
 
            if file_id == inv.root.file_id:
898
 
                continue
899
696
            add_entry(file_id)
900
697
 
901
698
        return inv
930
727
    from bzrlib.iterablefile import IterableFile
931
728
    if file_patch == "":
932
729
        return IterableFile(())
933
 
    return IterableFile(iter_patched(original, file_patch.splitlines(True)))
 
730
    # string.splitlines(True) also splits on '\r', but the iter_patched code
 
731
    # only expects to iterate over '\n' style lines
 
732
    return IterableFile(iter_patched(original,
 
733
                StringIO(file_patch).readlines()))