rebase: support multiple roots for rebaseset

We have all the necessary mechanism to rebase a set with multiple roots, we only
needed a proper handling of this case we preparing and concluding the rebase.
This changeset des that.

Rebase set with multiple root allows some awesome usage of rebase like:

- rebase all your draft on lastest upstream

  hg rebase --dest @ --rev 'draft()'

- exclusion of specific changeset during rebase

  hg rebase --rev '42:: - author(Babar)'

-  rebase a set of revision were multiple roots are later merged

  hg rebase --rev '(18+42)::'
This commit is contained in:
Pierre-Yves David 2013-01-17 00:35:01 +01:00
parent 6c68029a60
commit 7e632ae757
3 changed files with 196 additions and 67 deletions

View File

@ -574,9 +574,9 @@ def abort(repo, originalwd, target, state):
merge.update(repo, repo[originalwd].rev(), False, True, False)
rebased = filter(lambda x: x > -1 and x != target, state.values())
if rebased:
strippoint = min(rebased)
strippoints = [c.node() for c in repo.set('roots(%ld)', rebased)]
# no backup of rebased cset versions needed
repair.strip(repo.ui, repo, repo[strippoint].node())
repair.strip(repo.ui, repo, strippoints)
clearstatus(repo)
repo.ui.warn(_('rebase aborted\n'))
return 0
@ -599,65 +599,65 @@ def buildstate(repo, dest, rebaseset, collapse):
roots = list(repo.set('roots(%ld)', rebaseset))
if not roots:
raise util.Abort(_('no matching revisions'))
if len(roots) > 1:
raise util.Abort(_("can't rebase multiple roots"))
root = roots[0]
roots.sort()
state = {}
detachset = set()
for root in roots:
commonbase = root.ancestor(dest)
if commonbase == root:
raise util.Abort(_('source is ancestor of destination'))
if commonbase == dest:
samebranch = root.branch() == dest.branch()
if not collapse and samebranch and root in dest.children():
repo.ui.debug('source is a child of destination\n')
return None
commonbase = root.ancestor(dest)
if commonbase == root:
raise util.Abort(_('source is ancestor of destination'))
if commonbase == dest:
samebranch = root.branch() == dest.branch()
if not collapse and samebranch and root in dest.children():
repo.ui.debug('source is a child of destination\n')
return None
repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root))
state = dict.fromkeys(rebaseset, nullrev)
# Rebase tries to turn <dest> into a parent of <root> while
# preserving the number of parents of rebased changesets:
#
# - A changeset with a single parent will always be rebased as a
# changeset with a single parent.
#
# - A merge will be rebased as merge unless its parents are both
# ancestors of <dest> or are themselves in the rebased set and
# pruned while rebased.
#
# If one parent of <root> is an ancestor of <dest>, the rebased
# version of this parent will be <dest>. This is always true with
# --base option.
#
# Otherwise, we need to *replace* the original parents with
# <dest>. This "detaches" the rebased set from its former location
# and rebases it onto <dest>. Changes introduced by ancestors of
# <root> not common with <dest> (the detachset, marked as
# nullmerge) are "removed" from the rebased changesets.
#
# - If <root> has a single parent, set it to <dest>.
#
# - If <root> is a merge, we cannot decide which parent to
# replace, the rebase operation is not clearly defined.
#
# The table below sums up this behavior:
#
# +--------------------+----------------------+-------------------------+
# | | one parent | merge |
# +--------------------+----------------------+-------------------------+
# | parent in ::<dest> | new parent is <dest> | parents in ::<dest> are |
# | | | remapped to <dest> |
# +--------------------+----------------------+-------------------------+
# | unrelated source | new parent is <dest> | ambiguous, abort |
# +--------------------+----------------------+-------------------------+
#
# The actual abort is handled by `defineparents`
if len(root.parents()) <= 1:
# ancestors of <root> not ancestors of <dest>
detachset = repo.changelog.findmissingrevs([commonbase.rev()],
[root.rev()])
state.update(dict.fromkeys(detachset, nullmerge))
# detachset can have root, and we definitely want to rebase that
state[root.rev()] = nullrev
repo.ui.debug('rebase onto %d starting from %s\n' % (dest, roots))
state.update(dict.fromkeys(rebaseset, nullrev))
# Rebase tries to turn <dest> into a parent of <root> while
# preserving the number of parents of rebased changesets:
#
# - A changeset with a single parent will always be rebased as a
# changeset with a single parent.
#
# - A merge will be rebased as merge unless its parents are both
# ancestors of <dest> or are themselves in the rebased set and
# pruned while rebased.
#
# If one parent of <root> is an ancestor of <dest>, the rebased
# version of this parent will be <dest>. This is always true with
# --base option.
#
# Otherwise, we need to *replace* the original parents with
# <dest>. This "detaches" the rebased set from its former location
# and rebases it onto <dest>. Changes introduced by ancestors of
# <root> not common with <dest> (the detachset, marked as
# nullmerge) are "removed" from the rebased changesets.
#
# - If <root> has a single parent, set it to <dest>.
#
# - If <root> is a merge, we cannot decide which parent to
# replace, the rebase operation is not clearly defined.
#
# The table below sums up this behavior:
#
# +------------------+----------------------+-------------------------+
# | | one parent | merge |
# +------------------+----------------------+-------------------------+
# | parent in | new parent is <dest> | parents in ::<dest> are |
# | ::<dest> | | remapped to <dest> |
# +------------------+----------------------+-------------------------+
# | unrelated source | new parent is <dest> | ambiguous, abort |
# +------------------+----------------------+-------------------------+
#
# The actual abort is handled by `defineparents`
if len(root.parents()) <= 1:
# ancestors of <root> not ancestors of <dest>
detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
[root.rev()]))
for r in detachset:
if r not in state:
state[r] = nullmerge
return repo['.'].rev(), dest.rev(), state
def clearrebased(ui, repo, state, collapsedas=None):
@ -677,12 +677,16 @@ def clearrebased(ui, repo, state, collapsedas=None):
else:
rebased = [rev for rev in state if state[rev] != nullmerge]
if rebased:
if set(repo.changelog.descendants([min(rebased)])) - set(state):
ui.warn(_("warning: new changesets detected "
"on source branch, not stripping\n"))
else:
stripped = []
for root in repo.set('roots(%ld)', rebased):
if set(repo.changelog.descendants([root.rev()])) - set(state):
ui.warn(_("warning: new changesets detected "
"on source branch, not stripping\n"))
else:
stripped.append(root.node())
if stripped:
# backup the old csets by default
repair.strip(ui, repo, repo[min(rebased)].node(), "all")
repair.strip(ui, repo, stripped, "all")
def pullrebase(orig, ui, repo, *args, **opts):

View File

@ -306,3 +306,26 @@ Test that rewriting leaving instability behind is allowed
Test multiple root handling
------------------------------------
$ hg rebase --dest 4 --rev '7+11+9'
$ hg log -G
@ 14:00891d85fcfc C
|
| o 13:102b4c1d889b D
|/
| o 12:bfe264faf697 H
|/
| o 10:7c6027df6a99 B
| |
| x 7:02de42196ebe H
| |
+---o 6:eea13746799a G
| |/
| o 5:24b6387c8c8c F
| |
o | 4:9520eea781bc E
|/
o 0:cd010b8cd998 A

View File

@ -542,6 +542,108 @@ We would expect heads are I, F if it was supported
$ hg clone -q -u . ah ah6
$ cd ah6
$ hg rebase -r '(4+6)::' -d 1
abort: can't rebase multiple roots
[255]
saved backup bundle to $TESTTMP/ah6/.hg/strip-backup/3d8a618087a7-backup.hg (glob)
$ hg tglog
@ 8: 'I'
|
o 7: 'H'
|
o 6: 'G'
|
| o 5: 'F'
| |
| o 4: 'E'
|/
| o 3: 'D'
| |
| o 2: 'C'
| |
o | 1: 'B'
|/
o 0: 'A'
$ cd ..
More complexe rebase with multiple roots
each root have a different common ancestor with the destination and this is a detach
(setup)
$ hg clone -q -u . a a8
$ cd a8
$ echo I > I
$ hg add I
$ hg commit -m I
$ hg up 4
1 files updated, 0 files merged, 3 files removed, 0 files unresolved
$ echo I > J
$ hg add J
$ hg commit -m J
created new head
$ echo I > K
$ hg add K
$ hg commit -m K
$ hg tglog
@ 10: 'K'
|
o 9: 'J'
|
| o 8: 'I'
| |
| o 7: 'H'
| |
+---o 6: 'G'
| |/
| o 5: 'F'
| |
o | 4: 'E'
|/
| o 3: 'D'
| |
| o 2: 'C'
| |
| o 1: 'B'
|/
o 0: 'A'
(actual test)
$ hg rebase --dest 'desc(G)' --rev 'desc(K) + desc(I)'
saved backup bundle to $TESTTMP/a8/.hg/strip-backup/23a4ace37988-backup.hg (glob)
$ hg log --rev 'children(desc(G))'
changeset: 9:adb617877056
parent: 6:eea13746799a
user: test
date: Thu Jan 01 00:00:00 1970 +0000
summary: I
changeset: 10:882431a34a0e
tag: tip
parent: 6:eea13746799a
user: test
date: Thu Jan 01 00:00:00 1970 +0000
summary: K
$ hg tglog
@ 10: 'K'
|
| o 9: 'I'
|/
| o 8: 'J'
| |
| | o 7: 'H'
| | |
o---+ 6: 'G'
|/ /
| o 5: 'F'
| |
o | 4: 'E'
|/
| o 3: 'D'
| |
| o 2: 'C'
| |
| o 1: 'B'
|/
o 0: 'A'