1
# Copyright (C) 2006-2007 by Jelmer Vernooij
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.
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.
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
22
config as _mod_config,
25
from ...errors import (
32
from ...bzr.generate_ids import gen_revision_id
33
from ...graph import FrozenHeadsCache
34
from ...merge import Merger
35
from ...revision import NULL_REVISION
36
from ...trace import mutter
37
from ...tsort import topo_sort
38
from ...tree import TreeChange
41
from .maptree import (
46
REBASE_PLAN_FILENAME = 'rebase-plan'
47
REBASE_CURRENT_REVID_FILENAME = 'rebase-current'
48
REBASE_PLAN_VERSION = 1
49
REVPROP_REBASE_OF = 'rebase-of'
51
class RebaseState(object):
54
"""Check whether there is a rebase plan present.
58
raise NotImplementedError(self.has_plan)
61
"""Read a rebase plan file.
63
:return: Tuple with last revision info and replace map.
65
raise NotImplementedError(self.read_plan)
67
def write_plan(self, replace_map):
68
"""Write a rebase plan file.
70
:param replace_map: Replace map (old revid -> (new revid, new parents))
72
raise NotImplementedError(self.write_plan)
74
def remove_plan(self):
75
"""Remove a rebase plan file.
77
raise NotImplementedError(self.remove_plan)
79
def write_active_revid(self, revid):
80
"""Write the id of the revision that is currently being rebased.
82
:param revid: Revision id to write
84
raise NotImplementedError(self.write_active_revid)
86
def read_active_revid(self):
87
"""Read the id of the revision that is currently being rebased.
89
:return: Id of the revision that is being rebased.
91
raise NotImplementedError(self.read_active_revid)
94
class RebaseState1(RebaseState):
96
def __init__(self, wt):
98
self.transport = wt._transport
101
"""See `RebaseState`."""
103
return self.transport.get_bytes(REBASE_PLAN_FILENAME) != b''
108
"""See `RebaseState`."""
109
text = self.transport.get_bytes(REBASE_PLAN_FILENAME)
111
raise NoSuchFile(REBASE_PLAN_FILENAME)
112
return unmarshall_rebase_plan(text)
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)
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'')
127
def write_active_revid(self, revid):
128
"""See `RebaseState`."""
130
revid = NULL_REVISION
131
assert isinstance(revid, bytes)
132
self.transport.put_bytes(REBASE_CURRENT_REVID_FILENAME, revid)
134
def read_active_revid(self):
135
"""See `RebaseState`."""
137
text = self.transport.get_bytes(REBASE_CURRENT_REVID_FILENAME).rstrip(b"\n")
138
if text == NULL_REVISION:
145
def marshall_rebase_plan(last_rev_info, replace_map):
146
"""Marshall a rebase plan.
148
:param last_rev_info: Last revision info tuple.
149
:param replace_map: Replace map (old revid -> (new revid, new parents))
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"
161
def unmarshall_rebase_plan(text):
162
"""Unmarshall a rebase plan.
164
:param text: Text to parse
165
:return: Tuple with last revision info, replace map.
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])
172
pts = lines[1].split(b" ", 1)
173
last_revision_info = (int(pts[0]), pts[1])
180
replace_map[pts[0]] = (pts[1], tuple(pts[2:]))
181
return (last_revision_info, replace_map)
184
def regenerate_default_revid(repository, revid):
185
"""Generate a revision id for the rebase of an existing revision.
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:
192
rev = repository.get_revision(revid)
193
return gen_revision_id(rev.committer, rev.timestamp)
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.
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
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
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"
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
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])
243
parents.append(onto_revid)
244
parents.append(oldparents[0])
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,)):
252
elif oldparent in replace_map:
253
newparent = replace_map[oldparent][0]
254
if parents[0] == onto_revid:
255
parents[0] = newparent
257
parents.append(newparent)
259
parents.append(oldparent)
260
if len(parents) == 1 and skip_full_merged:
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)
270
def generate_transpose_plan(ancestry, renames, graph, generate_revid):
271
"""Create a rebase plan that replaces a bunch of revisions
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
283
for r, ps in ancestry:
284
if r not in children:
286
if ps is None: # Ghost
289
if r not in children:
292
if p not in children:
294
children[p].append(r)
296
parent_map.update(graph.get_parent_map(filter(lambda x: x not in parent_map, renames.values())))
298
# todo contains a list of revisions that need to
300
for r, v in renames.items():
301
replace_map[r] = (v, parent_map[v])
307
pb = ui.ui_factory.nested_progress_bar()
313
pb.update('determining dependencies', i, total)
314
# Add entry for them in replace_map
315
for c in children[r]:
319
parents = replace_map[c][1]
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)),
331
if replace_map[c][0] == c:
333
elif c not in processed:
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]
346
def rebase_todo(repository, replace_map):
347
"""Figure out what revisions still need to be rebased.
349
:param repository: Repository that contains the revisions
350
:param replace_map: Replace map
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]):
358
def rebase(repository, replace_map, revision_rewriter):
359
"""Rebase a working tree according to the specified map.
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
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()
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
377
revision_rewriter(revid, newrevid, newparents)
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])
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])
391
new_parent = change.parent_id[1]
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,
399
class CommitBuilderRevisionRewriter(object):
400
"""Revision rewriter that use commit builder.
402
:ivar repository: Repository in which the revision is present.
405
def __init__(self, repository, map_ids=True):
406
self.repository = repository
407
self.map_ids = map_ids
409
def _get_present_revisions(self, revids):
410
return tuple([p for p in revids if self.repository.has_revision(p)])
412
def __call__(self, oldrevid, newrevid, new_parents):
413
"""Replay a commit by simply commiting the same snapshot with different
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.
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)
425
revprops = dict(oldrev.properties)
426
revprops[REVPROP_REBASE_OF] = oldrevid.decode('utf-8')
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)
434
fileid_map = map_file_ids(
435
self.repository, nonghost_oldparents,
437
mappedtree = MapTree(oldtree, fileid_map)
442
old_base = nonghost_oldparents[0]
444
old_base = NULL_REVISION
446
new_base = new_parents[0]
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())
458
for (relpath, fs_hash) in builder.record_iter_changes(
459
mappedtree, new_base, iter_changes):
461
builder.finish_inventory()
462
return builder.commit(oldrev.message)
468
class WorkingTreeRevisionRewriter(object):
470
def __init__(self, wt, state, merge_type=None):
472
:param wt: Working tree in which to do the replays.
475
self.graph = self.wt.branch.repository.get_graph()
477
self.merge_type = merge_type
479
def __call__(self, oldrevid, newrevid, newparents):
480
"""Replay a commit in a working tree, with a different base.
482
:param oldrevid: Old revision id
483
:param newrevid: New revision id
484
:param newparents: New parent revision ids
486
repository = self.wt.branch.repository
487
if self.merge_type is None:
488
from ...merge import Merge3Merger
489
merge_type = Merge3Merger
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"
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
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)
514
def determine_base(self, oldrevid, oldparents, newrevid, newparents):
515
"""Determine the base for replaying a revision using merge.
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.
523
# If this was the first commit, no base is needed
524
if len(oldparents) == 0:
527
# In the case of a "simple" revision with just one parent,
528
# that parent should be the base
529
if len(oldparents) == 1:
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
535
if len(newparents) == 1:
536
# FIXME: Find oldparents entry that matches newparents[0]
541
return self.graph.find_unique_lca(*[oldparents[0], newparents[1]])
542
except NoCommonAncestor:
545
def commit_rebase(self, oldrev, newrevid):
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:
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']
568
message=oldrev.message, timestamp=oldrev.timestamp,
569
timezone=oldrev.timezone, revprops=revprops, rev_id=newrevid,
570
committer=committer, authors=authors)
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.
577
:param wt: Working tree to use for rebase
578
:param newparents: New parents of the working tree
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)
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])
596
class ReplaySnapshotError(BzrError):
597
"""Raised when replaying a snapshot failed."""
598
_fmt = """Replaying the snapshot failed: %(msg)s."""
600
def __init__(self, msg):
601
BzrError.__init__(self)