/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/plugins/rewrite/rebase.py

  • Committer: Jelmer Vernooij
  • Date: 2020-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2007 by Jelmer Vernooij
 
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
"""Rebase."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
import os
 
22
 
 
23
from ... import (
 
24
    config as _mod_config,
 
25
    osutils,
 
26
    )
 
27
from ...errors import (
 
28
    BzrError,
 
29
    NoSuchFile,
 
30
    UnknownFormatError,
 
31
    NoCommonAncestor,
 
32
    UnrelatedBranches,
 
33
    )
 
34
from ...bzr.generate_ids import gen_revision_id
 
35
from ...graph import FrozenHeadsCache
 
36
from ...merge import Merger
 
37
from ...revision import NULL_REVISION
 
38
from ...trace import mutter
 
39
from ...tsort import topo_sort
 
40
from ...tree import TreeChange
 
41
from ... import ui
 
42
 
 
43
from .maptree import (
 
44
    MapTree,
 
45
    map_file_ids,
 
46
    )
 
47
 
 
48
REBASE_PLAN_FILENAME = 'rebase-plan'
 
49
REBASE_CURRENT_REVID_FILENAME = 'rebase-current'
 
50
REBASE_PLAN_VERSION = 1
 
51
REVPROP_REBASE_OF = 'rebase-of'
 
52
 
 
53
class RebaseState(object):
 
54
 
 
55
    def has_plan(self):
 
56
        """Check whether there is a rebase plan present.
 
57
 
 
58
        :return: boolean
 
59
        """
 
60
        raise NotImplementedError(self.has_plan)
 
61
 
 
62
    def read_plan(self):
 
63
        """Read a rebase plan file.
 
64
 
 
65
        :return: Tuple with last revision info and replace map.
 
66
        """
 
67
        raise NotImplementedError(self.read_plan)
 
68
 
 
69
    def write_plan(self, replace_map):
 
70
        """Write a rebase plan file.
 
71
 
 
72
        :param replace_map: Replace map (old revid -> (new revid, new parents))
 
73
        """
 
74
        raise NotImplementedError(self.write_plan)
 
75
 
 
76
    def remove_plan(self):
 
77
        """Remove a rebase plan file.
 
78
        """
 
79
        raise NotImplementedError(self.remove_plan)
 
80
 
 
81
    def write_active_revid(self, revid):
 
82
        """Write the id of the revision that is currently being rebased.
 
83
 
 
84
        :param revid: Revision id to write
 
85
        """
 
86
        raise NotImplementedError(self.write_active_revid)
 
87
 
 
88
    def read_active_revid(self):
 
89
        """Read the id of the revision that is currently being rebased.
 
90
 
 
91
        :return: Id of the revision that is being rebased.
 
92
        """
 
93
        raise NotImplementedError(self.read_active_revid)
 
94
 
 
95
 
 
96
class RebaseState1(RebaseState):
 
97
 
 
98
    def __init__(self, wt):
 
99
        self.wt = wt
 
100
        self.transport = wt._transport
 
101
 
 
102
    def has_plan(self):
 
103
        """See `RebaseState`."""
 
104
        try:
 
105
            return self.transport.get_bytes(REBASE_PLAN_FILENAME) != b''
 
106
        except NoSuchFile:
 
107
            return False
 
108
 
 
109
    def read_plan(self):
 
110
        """See `RebaseState`."""
 
111
        text = self.transport.get_bytes(REBASE_PLAN_FILENAME)
 
112
        if text == b'':
 
113
            raise NoSuchFile(REBASE_PLAN_FILENAME)
 
114
        return unmarshall_rebase_plan(text)
 
115
 
 
116
    def write_plan(self, replace_map):
 
117
        """See `RebaseState`."""
 
118
        self.wt.update_feature_flags({b"rebase-v1": b"write-required"})
 
119
        content = marshall_rebase_plan(
 
120
            self.wt.branch.last_revision_info(), replace_map)
 
121
        assert isinstance(content, bytes)
 
122
        self.transport.put_bytes(REBASE_PLAN_FILENAME, content)
 
123
 
 
124
    def remove_plan(self):
 
125
        """See `RebaseState`."""
 
126
        self.wt.update_feature_flags({b"rebase-v1": None})
 
127
        self.transport.put_bytes(REBASE_PLAN_FILENAME, b'')
 
128
 
 
129
    def write_active_revid(self, revid):
 
130
        """See `RebaseState`."""
 
131
        if revid is None:
 
132
            revid = NULL_REVISION
 
133
        assert isinstance(revid, bytes)
 
134
        self.transport.put_bytes(REBASE_CURRENT_REVID_FILENAME, revid)
 
135
 
 
136
    def read_active_revid(self):
 
137
        """See `RebaseState`."""
 
138
        try:
 
139
            text = self.transport.get_bytes(REBASE_CURRENT_REVID_FILENAME).rstrip(b"\n")
 
140
            if text == NULL_REVISION:
 
141
                return None
 
142
            return text
 
143
        except NoSuchFile:
 
144
            return None
 
145
 
 
146
 
 
147
def marshall_rebase_plan(last_rev_info, replace_map):
 
148
    """Marshall a rebase plan.
 
149
 
 
150
    :param last_rev_info: Last revision info tuple.
 
151
    :param replace_map: Replace map (old revid -> (new revid, new parents))
 
152
    :return: string
 
153
    """
 
154
    ret = b"# Bazaar rebase plan %d\n" % REBASE_PLAN_VERSION
 
155
    ret += b"%d %s\n" % last_rev_info
 
156
    for oldrev in replace_map:
 
157
        (newrev, newparents) = replace_map[oldrev]
 
158
        ret += b"%s %s" % (oldrev, newrev) + \
 
159
            b"".join([b" %s" % p for p in newparents]) + b"\n"
 
160
    return ret
 
161
 
 
162
 
 
163
def unmarshall_rebase_plan(text):
 
164
    """Unmarshall a rebase plan.
 
165
 
 
166
    :param text: Text to parse
 
167
    :return: Tuple with last revision info, replace map.
 
168
    """
 
169
    lines = text.split(b'\n')
 
170
    # Make sure header is there
 
171
    if lines[0] != b"# Bazaar rebase plan %d" % REBASE_PLAN_VERSION:
 
172
        raise UnknownFormatError(lines[0])
 
173
 
 
174
    pts = lines[1].split(b" ", 1)
 
175
    last_revision_info = (int(pts[0]), pts[1])
 
176
    replace_map = {}
 
177
    for l in lines[2:]:
 
178
        if l == b"":
 
179
            # Skip empty lines
 
180
            continue
 
181
        pts = l.split(b" ")
 
182
        replace_map[pts[0]] = (pts[1], tuple(pts[2:]))
 
183
    return (last_revision_info, replace_map)
 
184
 
 
185
 
 
186
def regenerate_default_revid(repository, revid):
 
187
    """Generate a revision id for the rebase of an existing revision.
 
188
 
 
189
    :param repository: Repository in which the revision is present.
 
190
    :param revid: Revision id of the revision that is being rebased.
 
191
    :return: new revision id."""
 
192
    if revid == NULL_REVISION:
 
193
        return NULL_REVISION
 
194
    rev = repository.get_revision(revid)
 
195
    return gen_revision_id(rev.committer, rev.timestamp)
 
196
 
 
197
 
 
198
def generate_simple_plan(todo_set, start_revid, stop_revid, onto_revid, graph,
 
199
                         generate_revid, skip_full_merged=False):
 
200
    """Create a simple rebase plan that replays history based
 
201
    on one revision being replayed on top of another.
 
202
 
 
203
    :param todo_set: A set of revisions to rebase. Only the revisions
 
204
        topologically between stop_revid and start_revid (inclusive) are
 
205
        rebased; other revisions are ignored (and references to them are
 
206
        preserved).
 
207
    :param start_revid: Id of revision at which to start replaying
 
208
    :param stop_revid: Id of revision until which to stop replaying
 
209
    :param onto_revid: Id of revision on top of which to replay
 
210
    :param graph: Graph object
 
211
    :param generate_revid: Function for generating new revision ids
 
212
    :param skip_full_merged: Skip revisions that merge already merged
 
213
                             revisions.
 
214
 
 
215
    :return: replace map
 
216
    """
 
217
    assert start_revid is None or start_revid in todo_set, \
 
218
        "invalid start revid(%r), todo_set(%r)" % (start_revid, todo_set)
 
219
    assert stop_revid is None or stop_revid in todo_set, "invalid stop_revid"
 
220
    replace_map = {}
 
221
    parent_map = graph.get_parent_map(todo_set)
 
222
    order = topo_sort(parent_map)
 
223
    if stop_revid is None:
 
224
        stop_revid = order[-1]
 
225
    if start_revid is None:
 
226
        # We need a common base.
 
227
        lca = graph.find_lca(stop_revid, onto_revid)
 
228
        if lca == set([NULL_REVISION]):
 
229
            raise UnrelatedBranches()
 
230
        start_revid = order[0]
 
231
    todo = order[order.index(start_revid):order.index(stop_revid) + 1]
 
232
    heads_cache = FrozenHeadsCache(graph)
 
233
    # XXX: The output replacemap'd parents should get looked up in some manner
 
234
    # by the heads cache? RBC 20080719
 
235
    for oldrevid in todo:
 
236
        oldparents = parent_map[oldrevid]
 
237
        assert isinstance(oldparents, tuple), "not tuple: %r" % oldparents
 
238
        parents = []
 
239
        # Left parent:
 
240
        if heads_cache.heads((oldparents[0], onto_revid)) == set((onto_revid,)):
 
241
            parents.append(onto_revid)
 
242
        elif oldparents[0] in replace_map:
 
243
            parents.append(replace_map[oldparents[0]][0])
 
244
        else:
 
245
            parents.append(onto_revid)
 
246
            parents.append(oldparents[0])
 
247
        # Other parents:
 
248
        if len(oldparents) > 1:
 
249
            additional_parents = heads_cache.heads(oldparents[1:])
 
250
            for oldparent in oldparents[1:]:
 
251
                if oldparent in additional_parents:
 
252
                    if heads_cache.heads((oldparent, onto_revid)) == set((onto_revid,)):
 
253
                        pass
 
254
                    elif oldparent in replace_map:
 
255
                        newparent = replace_map[oldparent][0]
 
256
                        if parents[0] == onto_revid:
 
257
                            parents[0] = newparent
 
258
                        else:
 
259
                            parents.append(newparent)
 
260
                    else:
 
261
                        parents.append(oldparent)
 
262
            if len(parents) == 1 and skip_full_merged:
 
263
                continue
 
264
        parents = tuple(parents)
 
265
        newrevid = generate_revid(oldrevid, parents)
 
266
        assert newrevid != oldrevid, "old and newrevid equal (%r)" % newrevid
 
267
        assert isinstance(parents, tuple), "parents not tuple: %r" % parents
 
268
        replace_map[oldrevid] = (newrevid, parents)
 
269
    return replace_map
 
270
 
 
271
 
 
272
def generate_transpose_plan(ancestry, renames, graph, generate_revid):
 
273
    """Create a rebase plan that replaces a bunch of revisions
 
274
    in a revision graph.
 
275
 
 
276
    :param ancestry: Ancestry to consider
 
277
    :param renames: Renames of revision
 
278
    :param graph: Graph object
 
279
    :param generate_revid: Function for creating new revision ids
 
280
    """
 
281
    replace_map = {}
 
282
    todo = []
 
283
    children = {}
 
284
    parent_map = {}
 
285
    for r, ps in ancestry:
 
286
        if r not in children:
 
287
            children[r] = []
 
288
        if ps is None: # Ghost
 
289
            continue
 
290
        parent_map[r] = ps
 
291
        if r not in children:
 
292
            children[r] = []
 
293
        for p in ps:
 
294
            if p not in children:
 
295
                children[p] = []
 
296
            children[p].append(r)
 
297
 
 
298
    parent_map.update(graph.get_parent_map(filter(lambda x: x not in parent_map, renames.values())))
 
299
 
 
300
    # todo contains a list of revisions that need to
 
301
    # be rewritten
 
302
    for r, v in renames.items():
 
303
        replace_map[r] = (v, parent_map[v])
 
304
        todo.append(r)
 
305
 
 
306
    total = len(todo)
 
307
    processed = set()
 
308
    i = 0
 
309
    pb = ui.ui_factory.nested_progress_bar()
 
310
    try:
 
311
        while len(todo) > 0:
 
312
            r = todo.pop()
 
313
            processed.add(r)
 
314
            i += 1
 
315
            pb.update('determining dependencies', i, total)
 
316
            # Add entry for them in replace_map
 
317
            for c in children[r]:
 
318
                if c in renames:
 
319
                    continue
 
320
                if c in replace_map:
 
321
                    parents = replace_map[c][1]
 
322
                else:
 
323
                    parents = parent_map[c]
 
324
                assert isinstance(parents, tuple), \
 
325
                    "Expected tuple of parents, got: %r" % parents
 
326
                # replace r in parents with replace_map[r][0]
 
327
                if not replace_map[r][0] in parents:
 
328
                    parents = list(parents)
 
329
                    parents[parents.index(r)] = replace_map[r][0]
 
330
                    parents = tuple(parents)
 
331
                replace_map[c] = (generate_revid(c, tuple(parents)),
 
332
                                  tuple(parents))
 
333
                if replace_map[c][0] == c:
 
334
                    del replace_map[c]
 
335
                elif c not in processed:
 
336
                    todo.append(c)
 
337
    finally:
 
338
        pb.finished()
 
339
 
 
340
    # Remove items from the map that already exist
 
341
    for revid in renames:
 
342
        if revid in replace_map:
 
343
            del replace_map[revid]
 
344
 
 
345
    return replace_map
 
346
 
 
347
 
 
348
def rebase_todo(repository, replace_map):
 
349
    """Figure out what revisions still need to be rebased.
 
350
 
 
351
    :param repository: Repository that contains the revisions
 
352
    :param replace_map: Replace map
 
353
    """
 
354
    for revid, parent_ids in replace_map.items():
 
355
        assert isinstance(parent_ids, tuple), "replace map parents not tuple"
 
356
        if not repository.has_revision(parent_ids[0]):
 
357
            yield revid
 
358
 
 
359
 
 
360
def rebase(repository, replace_map, revision_rewriter):
 
361
    """Rebase a working tree according to the specified map.
 
362
 
 
363
    :param repository: Repository that contains the revisions
 
364
    :param replace_map: Dictionary with revisions to (optionally) rewrite
 
365
    :param merge_fn: Function for replaying a revision
 
366
    """
 
367
    # Figure out the dependencies
 
368
    graph = repository.get_graph()
 
369
    todo = list(graph.iter_topo_order(replace_map.keys()))
 
370
    pb = ui.ui_factory.nested_progress_bar()
 
371
    try:
 
372
        for i, revid in enumerate(todo):
 
373
            pb.update('rebase revisions', i, len(todo))
 
374
            (newrevid, newparents) = replace_map[revid]
 
375
            assert isinstance(newparents, tuple), "Expected tuple for %r" % newparents
 
376
            if repository.has_revision(newrevid):
 
377
                # Was already converted, no need to worry about it again
 
378
                continue
 
379
            revision_rewriter(revid, newrevid, newparents)
 
380
    finally:
 
381
        pb.finished()
 
382
 
 
383
 
 
384
def wrap_iter_changes(old_iter_changes, map_tree):
 
385
    for change in old_iter_changes:
 
386
        if change.parent_id[0] is not None:
 
387
            old_parent = map_tree.new_id(change.parent_id[0])
 
388
        else:
 
389
            old_parent = change.parent_id[0]
 
390
        if change.parent_id[1] is not None:
 
391
            new_parent = map_tree.new_id(change.parent_id[1])
 
392
        else:
 
393
            new_parent = change.parent_id[1]
 
394
        yield TreeChange(
 
395
            map_tree.new_id(change.file_id), change.path,
 
396
            change.changed_content, change.versioned,
 
397
            (old_parent, new_parent), change.name, change.kind,
 
398
            change.executable)
 
399
 
 
400
 
 
401
class CommitBuilderRevisionRewriter(object):
 
402
    """Revision rewriter that use commit builder.
 
403
 
 
404
    :ivar repository: Repository in which the revision is present.
 
405
    """
 
406
 
 
407
    def __init__(self, repository, map_ids=True):
 
408
        self.repository = repository
 
409
        self.map_ids = map_ids
 
410
 
 
411
    def _get_present_revisions(self, revids):
 
412
        return tuple([p for p in revids if self.repository.has_revision(p)])
 
413
 
 
414
    def __call__(self, oldrevid, newrevid, new_parents):
 
415
        """Replay a commit by simply commiting the same snapshot with different
 
416
        parents.
 
417
 
 
418
        :param oldrevid: Revision id of the revision to copy.
 
419
        :param newrevid: Revision id of the revision to create.
 
420
        :param new_parents: Revision ids of the new parent revisions.
 
421
        """
 
422
        assert isinstance(new_parents, tuple), "CommitBuilderRevisionRewriter: Expected tuple for %r" % new_parents
 
423
        mutter('creating copy %r of %r with new parents %r',
 
424
               newrevid, oldrevid, new_parents)
 
425
        oldrev = self.repository.get_revision(oldrevid)
 
426
 
 
427
        revprops = dict(oldrev.properties)
 
428
        revprops[REVPROP_REBASE_OF] = oldrevid.decode('utf-8')
 
429
 
 
430
        # Check what new_ie.file_id should be
 
431
        # use old and new parent trees to generate new_id map
 
432
        nonghost_oldparents = self._get_present_revisions(oldrev.parent_ids)
 
433
        nonghost_newparents = self._get_present_revisions(new_parents)
 
434
        oldtree = self.repository.revision_tree(oldrevid)
 
435
        if self.map_ids:
 
436
            fileid_map = map_file_ids(
 
437
                self.repository, nonghost_oldparents,
 
438
                nonghost_newparents)
 
439
            mappedtree = MapTree(oldtree, fileid_map)
 
440
        else:
 
441
            mappedtree = oldtree
 
442
 
 
443
        try:
 
444
            old_base = nonghost_oldparents[0]
 
445
        except IndexError:
 
446
            old_base = NULL_REVISION
 
447
        try:
 
448
            new_base = new_parents[0]
 
449
        except IndexError:
 
450
            new_base = NULL_REVISION
 
451
        old_base_tree = self.repository.revision_tree(old_base)
 
452
        old_iter_changes = oldtree.iter_changes(old_base_tree)
 
453
        iter_changes = wrap_iter_changes(old_iter_changes, mappedtree)
 
454
        builder = self.repository.get_commit_builder(
 
455
            branch=None, parents=new_parents, committer=oldrev.committer,
 
456
            timestamp=oldrev.timestamp, timezone=oldrev.timezone,
 
457
            revprops=revprops, revision_id=newrevid,
 
458
            config_stack=_mod_config.GlobalStack())
 
459
        try:
 
460
            for (relpath, fs_hash) in builder.record_iter_changes(
 
461
                    mappedtree, new_base, iter_changes):
 
462
                pass
 
463
            builder.finish_inventory()
 
464
            return builder.commit(oldrev.message)
 
465
        except:
 
466
            builder.abort()
 
467
            raise
 
468
 
 
469
 
 
470
class WorkingTreeRevisionRewriter(object):
 
471
 
 
472
    def __init__(self, wt, state, merge_type=None):
 
473
        """
 
474
        :param wt: Working tree in which to do the replays.
 
475
        """
 
476
        self.wt = wt
 
477
        self.graph = self.wt.branch.repository.get_graph()
 
478
        self.state = state
 
479
        self.merge_type = merge_type
 
480
 
 
481
    def __call__(self, oldrevid, newrevid, newparents):
 
482
        """Replay a commit in a working tree, with a different base.
 
483
 
 
484
        :param oldrevid: Old revision id
 
485
        :param newrevid: New revision id
 
486
        :param newparents: New parent revision ids
 
487
        """
 
488
        repository = self.wt.branch.repository
 
489
        if self.merge_type is None:
 
490
            from ...merge import Merge3Merger
 
491
            merge_type = Merge3Merger
 
492
        else:
 
493
            merge_type = self.merge_type
 
494
        oldrev = self.wt.branch.repository.get_revision(oldrevid)
 
495
        # Make sure there are no conflicts or pending merges/changes
 
496
        # in the working tree
 
497
        complete_revert(self.wt, [newparents[0]])
 
498
        assert not self.wt.changes_from(self.wt.basis_tree()).has_changed(), "Changes in rev"
 
499
 
 
500
        oldtree = repository.revision_tree(oldrevid)
 
501
        self.state.write_active_revid(oldrevid)
 
502
        merger = Merger(self.wt.branch, this_tree=self.wt)
 
503
        merger.set_other_revision(oldrevid, self.wt.branch)
 
504
        base_revid = self.determine_base(
 
505
            oldrevid, oldrev.parent_ids, newrevid, newparents)
 
506
        mutter('replaying %r as %r with base %r and new parents %r' %
 
507
               (oldrevid, newrevid, base_revid, newparents))
 
508
        merger.set_base_revision(base_revid, self.wt.branch)
 
509
        merger.merge_type = merge_type
 
510
        merger.do_merge()
 
511
        for newparent in newparents[1:]:
 
512
            self.wt.add_pending_merge(newparent)
 
513
        self.commit_rebase(oldrev, newrevid)
 
514
        self.state.write_active_revid(None)
 
515
 
 
516
    def determine_base(self, oldrevid, oldparents, newrevid, newparents):
 
517
        """Determine the base for replaying a revision using merge.
 
518
 
 
519
        :param oldrevid: Revid of old revision.
 
520
        :param oldparents: List of old parents revids.
 
521
        :param newrevid: Revid of new revision.
 
522
        :param newparents: List of new parents revids.
 
523
        :return: Revision id of the new new revision.
 
524
        """
 
525
        # If this was the first commit, no base is needed
 
526
        if len(oldparents) == 0:
 
527
            return NULL_REVISION
 
528
 
 
529
        # In the case of a "simple" revision with just one parent,
 
530
        # that parent should be the base
 
531
        if len(oldparents) == 1:
 
532
            return oldparents[0]
 
533
 
 
534
        # In case the rhs parent(s) of the origin revision has already been
 
535
        # merged in the new branch, use diff between rhs parent and diff from
 
536
        # original revision
 
537
        if len(newparents) == 1:
 
538
            # FIXME: Find oldparents entry that matches newparents[0]
 
539
            # and return it
 
540
            return oldparents[1]
 
541
 
 
542
        try:
 
543
            return self.graph.find_unique_lca(*[oldparents[0], newparents[1]])
 
544
        except NoCommonAncestor:
 
545
            return oldparents[0]
 
546
 
 
547
    def commit_rebase(self, oldrev, newrevid):
 
548
        """Commit a rebase.
 
549
 
 
550
        :param oldrev: Revision info of new revision to commit.
 
551
        :param newrevid: New revision id."""
 
552
        assert oldrev.revision_id != newrevid, "Invalid revid %r" % newrevid
 
553
        revprops = dict(oldrev.properties)
 
554
        revprops[REVPROP_REBASE_OF] = oldrev.revision_id.decode('utf-8')
 
555
        committer = self.wt.branch.get_config().username()
 
556
        authors = oldrev.get_apparent_authors()
 
557
        if oldrev.committer == committer:
 
558
            # No need to explicitly record the authors if the original
 
559
            # committer is rebasing.
 
560
            if [oldrev.committer] == authors:
 
561
                authors = None
 
562
        else:
 
563
            if oldrev.committer not in authors:
 
564
                authors.append(oldrev.committer)
 
565
        if 'author' in revprops:
 
566
            del revprops['author']
 
567
        if 'authors' in revprops:
 
568
            del revprops['authors']
 
569
        self.wt.commit(
 
570
            message=oldrev.message, timestamp=oldrev.timestamp,
 
571
            timezone=oldrev.timezone, revprops=revprops, rev_id=newrevid,
 
572
            committer=committer, authors=authors)
 
573
 
 
574
 
 
575
def complete_revert(wt, newparents):
 
576
    """Simple helper that reverts to specified new parents and makes sure none
 
577
    of the extra files are left around.
 
578
 
 
579
    :param wt: Working tree to use for rebase
 
580
    :param newparents: New parents of the working tree
 
581
    """
 
582
    newtree = wt.branch.repository.revision_tree(newparents[0])
 
583
    delta = wt.changes_from(newtree)
 
584
    wt.branch.generate_revision_history(newparents[0])
 
585
    wt.set_parent_ids([r for r in newparents[:1] if r != NULL_REVISION])
 
586
    for change in delta.added:
 
587
        abs_path = wt.abspath(change.path[1])
 
588
        if osutils.lexists(abs_path):
 
589
            if osutils.isdir(abs_path):
 
590
                osutils.rmtree(abs_path)
 
591
            else:
 
592
                os.unlink(abs_path)
 
593
    wt.revert(None, old_tree=newtree, backups=False)
 
594
    assert not wt.changes_from(wt.basis_tree()).has_changed(), "Rev changed"
 
595
    wt.set_parent_ids([r for r in newparents if r != NULL_REVISION])
 
596
 
 
597
 
 
598
class ReplaySnapshotError(BzrError):
 
599
    """Raised when replaying a snapshot failed."""
 
600
    _fmt = """Replaying the snapshot failed: %(msg)s."""
 
601
 
 
602
    def __init__(self, msg):
 
603
        BzrError.__init__(self)
 
604
        self.msg = msg