~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to read_changeset.py

  • Committer: John Arbash Meinel
  • Date: 2005-07-02 06:03:00 UTC
  • mto: (0.5.85) (1185.82.1 bzr-w-changeset)
  • mto: This revision was merged to the branch mainline in revision 1738.
  • Revision ID: john@arbash-meinel.com-20050702060300-e36fa2c400796477
Lots of updates. Using a minimized annotations for changesets.

Show diffs side-by-side

added added

removed removed

Lines of Context:
66
66
        return rev
67
67
 
68
68
class ChangesetInfo(object):
69
 
    """This is the intermediate class that gets filled out as
70
 
    the file is read.
 
69
    """This contains the meta information. Stuff that allows you to
 
70
    recreate the revision or inventory XML.
71
71
    """
72
72
    def __init__(self):
73
73
        self.committer = None
78
78
 
79
79
        # A list of RevisionInfo objects
80
80
        self.revisions = []
81
 
        # Tuples of (new_file_id, new_file_path)
82
 
        self.new_file_ids = []
83
 
 
84
 
        # This is a mapping from file_id to text_id
85
 
        self.text_ids = {}
86
 
 
87
 
        self.tree_root_id = None
88
 
        self.file_ids = None
89
 
        self.old_file_ids = None
90
 
 
91
 
        self.actions = [] #this is the list of things that happened
 
81
        self.text_ids = {} # file_id => text_id
 
82
 
 
83
        self.actions = []
 
84
 
 
85
        self.timestamp = None
 
86
        self.timezone = None
92
87
 
93
88
    def __str__(self):
94
89
        return pprint.pformat(self.__dict__)
103
98
            # is the first parent of the last revision listed
104
99
            rev = self.revisions[-1]
105
100
            self.base = rev.parents[0].revision_id
 
101
            # In general, if self.base is None, self.base_sha1 should
 
102
            # also be None
 
103
            if self.base_sha1 is not None:
 
104
                assert self.base_sha1 == rev.parent[0].revision_sha1
106
105
            self.base_sha1 = rev.parents[0].revision_sha1
107
106
 
 
107
        if not self.timestamp and self.date:
 
108
            self.timestamp, self.timezone = common.unpack_highres_date(self.date)
 
109
 
108
110
        for rev in self.revisions:
109
 
            pass
110
 
 
111
 
    def create_maps(self):
112
 
        """Go through the individual id sections, and generate the 
113
 
        id2path and path2id maps.
114
 
        """
115
 
        # Rather than use an empty path, the changeset code seems 
116
 
        # to like to use "./." for the tree root.
117
 
        self.id2path[self.tree_root_id] = './.'
118
 
        self.path2id['./.'] = self.tree_root_id
119
 
        self.id2parent[self.tree_root_id] = bzrlib.changeset.NULL_ID
120
 
        self.old_id2path = self.id2path.copy()
121
 
        self.old_path2id = self.path2id.copy()
122
 
        self.old_id2parent = self.id2parent.copy()
123
 
 
124
 
        if self.file_ids:
125
 
            for info in self.file_ids:
126
 
                path, f_id, parent_id = info.split('\t')
127
 
                self.id2path[f_id] = path
128
 
                self.path2id[path] = f_id
129
 
                self.id2parent[f_id] = parent_id
130
 
        if self.old_file_ids:
131
 
            for info in self.old_file_ids:
132
 
                path, f_id, parent_id = info.split('\t')
133
 
                self.old_id2path[f_id] = path
134
 
                self.old_path2id[path] = f_id
135
 
                self.old_id2parent[f_id] = parent_id
136
 
 
137
 
    def get_changeset(self):
138
 
        """Create a changeset from the data contained within."""
139
 
        from bzrlib.changeset import Changeset, ChangesetEntry, \
140
 
            PatchApply, ReplaceContents
141
 
        cset = Changeset()
142
 
        
143
 
        entry = ChangesetEntry(self.tree_root_id, 
144
 
                bzrlib.changeset.NULL_ID, './.')
145
 
        cset.add_entry(entry)
146
 
        for info, lines in self.actions:
147
 
            parts = info.split(' ')
148
 
            action = parts[0]
149
 
            kind = parts[1]
150
 
            extra = ' '.join(parts[2:])
151
 
            if action == 'renamed':
152
 
                old_path, new_path = extra.split(' => ')
153
 
                old_path = _unescape(old_path)
154
 
                new_path = _unescape(new_path)
155
 
 
156
 
                new_id = self.path2id[new_path]
157
 
                old_id = self.old_path2id[old_path]
158
 
                assert old_id == new_id
159
 
 
160
 
                new_parent = self.id2parent[new_id]
161
 
                old_parent = self.old_id2parent[old_id]
162
 
 
163
 
                entry = ChangesetEntry(old_id, old_parent, old_path)
164
 
                entry.new_path = new_path
165
 
                entry.new_parent = new_parent
166
 
                if lines:
167
 
                    entry.contents_change = PatchApply(''.join(lines))
168
 
            elif action == 'removed':
169
 
                old_path = _unescape(extra)
170
 
                old_id = self.old_path2id[old_path]
171
 
                old_parent = self.old_id2parent[old_id]
172
 
                entry = ChangesetEntry(old_id, old_parent, old_path)
173
 
                entry.new_path = None
174
 
                entry.new_parent = None
175
 
                if lines:
176
 
                    # Technically a removed should be a ReplaceContents()
177
 
                    # Where you need to have the old contents
178
 
                    # But at most we have a remove style patch.
179
 
                    #entry.contents_change = ReplaceContents()
180
 
                    pass
181
 
            elif action == 'added':
182
 
                new_path = _unescape(extra)
183
 
                new_id = self.path2id[new_path]
184
 
                new_parent = self.id2parent[new_id]
185
 
                entry = ChangesetEntry(new_id, new_parent, new_path)
186
 
                entry.path = None
187
 
                entry.parent = None
188
 
                if lines:
189
 
                    # Technically an added should be a ReplaceContents()
190
 
                    # Where you need to have the old contents
191
 
                    # But at most we have an add style patch.
192
 
                    #entry.contents_change = ReplaceContents()
193
 
                    entry.contents_change = PatchApply(''.join(lines))
194
 
            elif action == 'modified':
195
 
                new_path = _unescape(extra)
196
 
                new_id = self.path2id[new_path]
197
 
                new_parent = self.id2parent[new_id]
198
 
                entry = ChangesetEntry(new_id, new_parent, new_path)
199
 
                entry.path = None
200
 
                entry.parent = None
201
 
                if lines:
202
 
                    # Technically an added should be a ReplaceContents()
203
 
                    # Where you need to have the old contents
204
 
                    # But at most we have an add style patch.
205
 
                    #entry.contents_change = ReplaceContents()
206
 
                    entry.contents_change = PatchApply(''.join(lines))
207
 
            else:
208
 
                raise BadChangeset('Unrecognized action: %r' % action)
209
 
            cset.add_entry(entry)
210
 
        return cset
 
111
            if rev.timestamp is None and self.timestamp is not None:
 
112
                rev.timestamp = self.timestamp
 
113
                rev.timezone = self.timezone
 
114
            if rev.message is None and self.message:
 
115
                rev.message = self.message
 
116
            if rev.committer is None and self.committer:
 
117
                rev.committer = self.committer
211
118
 
212
119
class ChangesetReader(object):
213
120
    """This class reads in a changeset from a file, and returns
231
138
        self._read_patches()
232
139
        self._read_footer()
233
140
 
 
141
    def get_info_and_tree(self, branch):
 
142
        """Return the meta information, and a Changeset tree which can
 
143
        be used to populate the local stores and working tree, respectively.
 
144
        """
 
145
        self.info.complete_info()
 
146
        store_base_sha1 = branch.get_revision_sha1(self.info.base) 
 
147
        if store_base_sha1 != self.info.base_sha1:
 
148
            raise BzrError('Base revision sha1 hash in store'
 
149
                    ' does not match the one read in the changeset'
 
150
                    ' (%s != %s)' % (store_base_sha1, self.info.base_sha1))
 
151
        tree = ChangesetTree(branch.revision_tree(self.info.base))
 
152
        self._update_tree(tree)
 
153
 
 
154
        return self.info, self.tree
 
155
 
234
156
    def _next(self):
235
157
        """yield the next line, but secretly
236
158
        keep 1 extra line for peeking.
241
163
            if last is not None:
242
164
                yield last
243
165
 
244
 
    def get_info(self):
245
 
        """Create the actual changeset object.
246
 
        """
247
 
        self.info.complete_info()
248
 
        return self.info
249
 
 
250
166
    def _read_header(self):
251
167
        """Read the bzr header"""
252
168
        header = common.get_header()
404
320
                            can handle. That extra line is given here.
405
321
        """
406
322
        line = self._next().next()
407
 
        if line != '# BEGIN BZR FOOTER\n':
408
 
            raise MalformedFooter('Footer did not begin with BEGIN BZR FOOTER')
409
 
 
410
323
        for line in self._next():
411
 
            if line == '# END BZR FOOTER\n':
412
 
                return
413
324
            self._handle_next(line)
 
325
            if self._next_line[:1] != '#':
 
326
                break
 
327
 
 
328
    def _update_tree(self, tree):
 
329
        """This fills out a ChangesetTree based on the information
 
330
        that was read in.
 
331
 
 
332
        :param tree: A ChangesetTree to update with the new information.
 
333
        """
 
334
        from bzrlib.errors import BzrError
 
335
        from common import decode
 
336
 
 
337
        def get_text_id(info, file_id):
 
338
            if info is not None:
 
339
                if info[:8] != 'text-id:':
 
340
                    raise BzrError("Text ids should be prefixed with 'text-id:'"
 
341
                        ': %r' % info)
 
342
                text_id = decode(info[8:])
 
343
            elif self.info.text_ids.has_key(file_id):
 
344
                return self.info.text_ids[file_id]
 
345
            else:
 
346
                # If text_id was not explicitly supplied
 
347
                # then it should be whatever we would guess it to be
 
348
                # based on the base revision, and what we know about
 
349
                # the target revision
 
350
                text_id = common.guess_text_id(tree.base_tree, 
 
351
                        file_id, self.info.base, True)
 
352
            if (self.info.text_ids.has_key(file_id)
 
353
                    and self.info.text_ids[file_id] != text_id):
 
354
                raise BzrError('Mismatched text_ids for file_id {%s}'
 
355
                        ': %s != %s' % (file_id,
 
356
                                        self.info.text_ids[file_id],
 
357
                                        text_id))
 
358
            # The Info object makes more sense for where
 
359
            # to store something like text_id, since it is
 
360
            # what will be used to generate stored inventory
 
361
            # entries.
 
362
            # The problem is that we are parsing the
 
363
            # ChangesetTree right now, we really modifying
 
364
            # the ChangesetInfo object
 
365
            self.info.text_ids[file_id] = text_id
 
366
            return text_id
 
367
 
 
368
        def renamed(kind, extra, lines):
 
369
            info = extra.split('\t')
 
370
            if len(info) < 2:
 
371
                raise BzrError('renamed action lines need both a from and to'
 
372
                        ': %r' % extra)
 
373
            old_path = decode(info[0])
 
374
            if info[1][:3] == '=> ':
 
375
                new_path = decode(info[1][3:])
 
376
            else:
 
377
                new_path = decode(info[1][3:])
 
378
 
 
379
            file_id = tree.path2id(new_path)
 
380
            if len(info) > 2:
 
381
                text_id = get_text_id(info[2], file_id)
 
382
            else:
 
383
                text_id = get_text_id(None, file_id)
 
384
            tree.note_rename(old_path, new_path)
 
385
            if lines:
 
386
                tree.note_patch(new_path, lines)
 
387
 
 
388
        def removed(kind, extra, lines):
 
389
            info = extra.split('\t')
 
390
            if len(info) > 1:
 
391
                # TODO: in the future we might allow file ids to be
 
392
                # given for removed entries
 
393
                raise BzrError('removed action lines should only have the path'
 
394
                        ': %r' % extra)
 
395
            path = decode(info[0])
 
396
            tree.note_deletion(path)
 
397
 
 
398
        def added(kind, extra, lines):
 
399
            info = extra.split('\t')
 
400
            if len(info) <= 1:
 
401
                raise BzrError('add action lines require the path and file id'
 
402
                        ': %r' % extra)
 
403
            elif len(info) > 3:
 
404
                raise BzrError('add action lines have fewer than 3 entries.'
 
405
                        ': %r' % extra)
 
406
            path = decode(info[0])
 
407
            if info[1][:8] == 'file-id:':
 
408
                raise BzrError('The file-id should follow the path for an add'
 
409
                        ': %r' % extra)
 
410
            file_id = decode(info[1][8:])
 
411
 
 
412
            if len(info) > 2:
 
413
                text_id = get_text_id(info[2], file_id)
 
414
            else:
 
415
                text_id = get_text_id(None, file_id)
 
416
            tree.note_id(file_id, path)
 
417
            tree.note_patch(path, lines)
 
418
 
 
419
        def modified(kind, extra, lines):
 
420
            info = extra.split('\t')
 
421
            if len(info) < 1:
 
422
                raise BzrError('modified action lines have at least'
 
423
                        'the path in them: %r' % extra)
 
424
            path = decode(info[0])
 
425
 
 
426
            file_id = tree.path2id(path)
 
427
            if len(info) > 1:
 
428
                text_id = get_text_id(info[1], file_id)
 
429
            else:
 
430
                text_id = get_text_id(None, file_id)
 
431
            tree.note_patch(path, lines)
 
432
            
 
433
 
 
434
        valid_actions = {
 
435
            'renamed':renamed,
 
436
            'removed':removed,
 
437
            'added':added,
 
438
            'modified':modified
 
439
        }
 
440
        for action_line, lines in self.info.actions:
 
441
            first = action_line.find(' ')
 
442
            if first == -1:
 
443
                raise BzrError('Bogus action line'
 
444
                        ' (no opening space): %r' % action_line)
 
445
            second = action_line.find(' ', first)
 
446
            if second == -1:
 
447
                raise BzrError('Bogus action line'
 
448
                        ' (missing second space): %r' % action_line)
 
449
            action = action_line[:first]
 
450
            kind = action_line[first+1:second]
 
451
            if kind not in ('file', 'directory'):
 
452
                raise BzrError('Bogus action line'
 
453
                        ' (invalid object kind): %r' % action_line)
 
454
            extra = action_line[second+1:]
 
455
 
 
456
            if action not in valid_actions:
 
457
                raise BzrError('Bogus action line'
 
458
                        ' (unrecognized action): %r' % action_line)
 
459
            valid_actions[action](kind, extra, lines)
414
460
 
415
461
def read_changeset(from_file):
416
462
    """Read in a changeset from a filelike object (must have "readline" support), and
424
470
class ChangesetTree:
425
471
    def __init__(self, base_tree=None):
426
472
        self.base_tree = base_tree
427
 
        self._renamed = {}
428
 
        self._renamed_r = {}
429
 
        self._new_id = {}
430
 
        self._new_id_r = {}
 
473
        self._renamed = {} # Mapping from old_path => new_path
 
474
        self._renamed_r = {} # new_path => old_path
 
475
        self._new_id = {} # new_path => new_id
 
476
        self._new_id_r = {} # new_id => new_path
431
477
        self.patches = {}
432
478
        self.deleted = []
433
479
        self.contents_by_id = True
434
480
 
 
481
    def __str__(self):
 
482
        return pprint.pformat(self.__dict__)
 
483
 
435
484
    def note_rename(self, old_path, new_path):
 
485
        """A file/directory has been renamed from old_path => new_path"""
436
486
        assert not self._renamed.has_key(old_path)
437
487
        assert not self._renamed_r.has_key(new_path)
438
488
        self._renamed[new_path] = old_path
439
489
        self._renamed_r[old_path] = new_path
440
490
 
441
491
    def note_id(self, new_id, new_path):
 
492
        """Files that don't exist in base need a new id."""
442
493
        self._new_id[new_path] = new_id
443
494
        self._new_id_r[new_id] = new_path
444
495
 
445
496
    def note_patch(self, new_path, patch):
 
497
        """There is a patch for a given filename."""
446
498
        self.patches[new_path] = patch
447
499
 
448
500
    def note_deletion(self, old_path):
 
501
        """The file at old_path has been deleted."""
449
502
        self.deleted.append(old_path)
450
503
 
451
504
    def old_path(self, new_path):
 
505
        """Get the old_path (path in the base_tree) for the file at new_path"""
452
506
        import os.path
453
507
        old_path = self._renamed.get(new_path)
454
508
        if old_path is not None:
470
524
 
471
525
 
472
526
    def new_path(self, old_path):
 
527
        """Get the new_path (path in the target_tree) for the file at old_path
 
528
        in the base tree.
 
529
        """
473
530
        import os.path
474
531
        new_path = self._renamed_r.get(old_path)
475
532
        if new_path is not None:
492
549
        return new_path 
493
550
 
494
551
    def path2id(self, path):
 
552
        """Return the id of the file present at path in the target tree."""
495
553
        file_id = self._new_id.get(path)
496
554
        if file_id is not None:
497
555
            return file_id
503
561
        return self.base_tree.path2id(old_path)
504
562
 
505
563
    def id2path(self, file_id):
 
564
        """Return the new path in the target tree of the file with id file_id"""
506
565
        path = self._new_id_r.get(file_id)
507
566
        if path is not None:
508
567
            return path
514
573
        return self.new_path(old_path)
515
574
 
516
575
    def old_contents_id(self, file_id):
 
576
        """Return the id in the base_tree for the given file_id,
 
577
        or None if the file did not exist in base.
 
578
 
 
579
        FIXME:  Something doesn't seem right here. It seems like this function
 
580
                should always either return None or file_id. Even if
 
581
                you are doing the by-path lookup, you are doing a
 
582
                id2path lookup, just to do the reverse path2id lookup.
 
583
        """
517
584
        if self.contents_by_id:
518
585
            if self.base_tree.has_id(file_id):
519
586
                return file_id
523
590
        return self.base_tree.path2id(new_path)
524
591
        
525
592
    def get_file(self, file_id):
 
593
        """Return a file-like object containing the new contents of the
 
594
        file given by file_id.
 
595
 
 
596
        TODO:   It might be nice if this actually generated an entry
 
597
                in the text-store, so that the file contents would
 
598
                then be cached.
 
599
        """
526
600
        base_id = self.old_contents_id(file_id)
527
601
        if base_id is not None:
528
602
            patch_original = self.base_tree.get_file(base_id)