/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: Breezy landing bot
  • Author(s): Colin Watson
  • Date: 2020-11-16 21:47:08 UTC
  • mfrom: (7521.1.1 remove-lp-workaround)
  • Revision ID: breezy.the.bot@gmail.com-20201116214708-jos209mgxi41oy15
Remove breezy.git workaround for bazaar.launchpad.net.

Merged from https://code.launchpad.net/~cjwatson/brz/remove-lp-workaround/+merge/393710

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