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
19
from __future__ import absolute_import
24
config as _mod_config,
27
from ...errors import (
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
43
from .maptree import (
48
REBASE_PLAN_FILENAME = 'rebase-plan'
49
REBASE_CURRENT_REVID_FILENAME = 'rebase-current'
50
REBASE_PLAN_VERSION = 1
51
REVPROP_REBASE_OF = 'rebase-of'
53
class RebaseState(object):
56
"""Check whether there is a rebase plan present.
60
raise NotImplementedError(self.has_plan)
63
"""Read a rebase plan file.
65
:return: Tuple with last revision info and replace map.
67
raise NotImplementedError(self.read_plan)
69
def write_plan(self, replace_map):
70
"""Write a rebase plan file.
72
:param replace_map: Replace map (old revid -> (new revid, new parents))
74
raise NotImplementedError(self.write_plan)
76
def remove_plan(self):
77
"""Remove a rebase plan file.
79
raise NotImplementedError(self.remove_plan)
81
def write_active_revid(self, revid):
82
"""Write the id of the revision that is currently being rebased.
84
:param revid: Revision id to write
86
raise NotImplementedError(self.write_active_revid)
88
def read_active_revid(self):
89
"""Read the id of the revision that is currently being rebased.
91
:return: Id of the revision that is being rebased.
93
raise NotImplementedError(self.read_active_revid)
96
class RebaseState1(RebaseState):
98
def __init__(self, wt):
100
self.transport = wt._transport
103
"""See `RebaseState`."""
105
return self.transport.get_bytes(REBASE_PLAN_FILENAME) != b''
110
"""See `RebaseState`."""
111
text = self.transport.get_bytes(REBASE_PLAN_FILENAME)
113
raise NoSuchFile(REBASE_PLAN_FILENAME)
114
return unmarshall_rebase_plan(text)
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)
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'')
129
def write_active_revid(self, revid):
130
"""See `RebaseState`."""
132
revid = NULL_REVISION
133
assert isinstance(revid, bytes)
134
self.transport.put_bytes(REBASE_CURRENT_REVID_FILENAME, revid)
136
def read_active_revid(self):
137
"""See `RebaseState`."""
139
text = self.transport.get_bytes(REBASE_CURRENT_REVID_FILENAME).rstrip(b"\n")
140
if text == NULL_REVISION:
147
def marshall_rebase_plan(last_rev_info, replace_map):
148
"""Marshall a rebase plan.
150
:param last_rev_info: Last revision info tuple.
151
:param replace_map: Replace map (old revid -> (new revid, new parents))
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"
163
def unmarshall_rebase_plan(text):
164
"""Unmarshall a rebase plan.
166
:param text: Text to parse
167
:return: Tuple with last revision info, replace map.
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])
174
pts = lines[1].split(b" ", 1)
175
last_revision_info = (int(pts[0]), pts[1])
182
replace_map[pts[0]] = (pts[1], tuple(pts[2:]))
183
return (last_revision_info, replace_map)
186
def regenerate_default_revid(repository, revid):
187
"""Generate a revision id for the rebase of an existing revision.
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:
194
rev = repository.get_revision(revid)
195
return gen_revision_id(rev.committer, rev.timestamp)
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.
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
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
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"
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
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])
245
parents.append(onto_revid)
246
parents.append(oldparents[0])
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,)):
254
elif oldparent in replace_map:
255
newparent = replace_map[oldparent][0]
256
if parents[0] == onto_revid:
257
parents[0] = newparent
259
parents.append(newparent)
261
parents.append(oldparent)
262
if len(parents) == 1 and skip_full_merged:
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)
272
def generate_transpose_plan(ancestry, renames, graph, generate_revid):
273
"""Create a rebase plan that replaces a bunch of revisions
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
285
for r, ps in ancestry:
286
if r not in children:
288
if ps is None: # Ghost
291
if r not in children:
294
if p not in children:
296
children[p].append(r)
298
parent_map.update(graph.get_parent_map(filter(lambda x: x not in parent_map, renames.values())))
300
# todo contains a list of revisions that need to
302
for r, v in renames.items():
303
replace_map[r] = (v, parent_map[v])
309
pb = ui.ui_factory.nested_progress_bar()
315
pb.update('determining dependencies', i, total)
316
# Add entry for them in replace_map
317
for c in children[r]:
321
parents = replace_map[c][1]
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)),
333
if replace_map[c][0] == c:
335
elif c not in processed:
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]
348
def rebase_todo(repository, replace_map):
349
"""Figure out what revisions still need to be rebased.
351
:param repository: Repository that contains the revisions
352
:param replace_map: Replace map
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]):
360
def rebase(repository, replace_map, revision_rewriter):
361
"""Rebase a working tree according to the specified map.
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
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()
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
379
revision_rewriter(revid, newrevid, newparents)
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])
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])
393
new_parent = change.parent_id[1]
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,
401
class CommitBuilderRevisionRewriter(object):
402
"""Revision rewriter that use commit builder.
404
:ivar repository: Repository in which the revision is present.
407
def __init__(self, repository, map_ids=True):
408
self.repository = repository
409
self.map_ids = map_ids
411
def _get_present_revisions(self, revids):
412
return tuple([p for p in revids if self.repository.has_revision(p)])
414
def __call__(self, oldrevid, newrevid, new_parents):
415
"""Replay a commit by simply commiting the same snapshot with different
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.
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)
427
revprops = dict(oldrev.properties)
428
revprops[REVPROP_REBASE_OF] = oldrevid.decode('utf-8')
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)
436
fileid_map = map_file_ids(
437
self.repository, nonghost_oldparents,
439
mappedtree = MapTree(oldtree, fileid_map)
444
old_base = nonghost_oldparents[0]
446
old_base = NULL_REVISION
448
new_base = new_parents[0]
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())
460
for (relpath, fs_hash) in builder.record_iter_changes(
461
mappedtree, new_base, iter_changes):
463
builder.finish_inventory()
464
return builder.commit(oldrev.message)
470
class WorkingTreeRevisionRewriter(object):
472
def __init__(self, wt, state, merge_type=None):
474
:param wt: Working tree in which to do the replays.
477
self.graph = self.wt.branch.repository.get_graph()
479
self.merge_type = merge_type
481
def __call__(self, oldrevid, newrevid, newparents):
482
"""Replay a commit in a working tree, with a different base.
484
:param oldrevid: Old revision id
485
:param newrevid: New revision id
486
:param newparents: New parent revision ids
488
repository = self.wt.branch.repository
489
if self.merge_type is None:
490
from ...merge import Merge3Merger
491
merge_type = Merge3Merger
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"
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
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)
516
def determine_base(self, oldrevid, oldparents, newrevid, newparents):
517
"""Determine the base for replaying a revision using merge.
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.
525
# If this was the first commit, no base is needed
526
if len(oldparents) == 0:
529
# In the case of a "simple" revision with just one parent,
530
# that parent should be the base
531
if len(oldparents) == 1:
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
537
if len(newparents) == 1:
538
# FIXME: Find oldparents entry that matches newparents[0]
543
return self.graph.find_unique_lca(*[oldparents[0], newparents[1]])
544
except NoCommonAncestor:
547
def commit_rebase(self, oldrev, newrevid):
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:
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']
570
message=oldrev.message, timestamp=oldrev.timestamp,
571
timezone=oldrev.timezone, revprops=revprops, rev_id=newrevid,
572
committer=committer, authors=authors)
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.
579
:param wt: Working tree to use for rebase
580
:param newparents: New parents of the working tree
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)
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])
598
class ReplaySnapshotError(BzrError):
599
"""Raised when replaying a snapshot failed."""
600
_fmt = """Replaying the snapshot failed: %(msg)s."""
602
def __init__(self, msg):
603
BzrError.__init__(self)