rebase: add --detach option to detach intermediate revisions (issue1950)
When rebasing an intermediate revision, rebase keeps a parent relationship
with the original parent. This option forces the removal of this relationship.
In more depth, it 'fakes' null merges between the target revision and the
ancestors of source, dropping every change from the ancestors.
The result is that every change in source and its descendants will be rebased,
ignoring the changes in its ancestors.
--- a/hgext/rebase.py Sun Jan 31 13:30:17 2010 +0100
+++ b/hgext/rebase.py Sat Feb 06 10:51:50 2010 +0100
@@ -22,6 +22,8 @@
from mercurial.i18n import _
import os, errno
+nullmerge = -2
+
def rebase(ui, repo, **opts):
"""move changeset (and descendants) to a different branch
@@ -53,6 +55,7 @@
extrafn = opts.get('extrafn')
keepf = opts.get('keep', False)
keepbranchesf = opts.get('keepbranches', False)
+ detachf = opts.get('detach', False)
if contf or abortf:
if contf and abortf:
@@ -62,6 +65,10 @@
raise error.ParseError(
'rebase', _('cannot use collapse with continue or abort'))
+ if detachf:
+ raise error.ParseError(
+ 'rebase', _('cannot use detach with continue or abort'))
+
if srcf or basef or destf:
raise error.ParseError('rebase',
_('abort and continue do not allow specifying revisions'))
@@ -75,8 +82,16 @@
if srcf and basef:
raise error.ParseError('rebase', _('cannot specify both a '
'revision and a base'))
+ if detachf:
+ if not srcf:
+ raise error.ParseError(
+ 'rebase', _('detach requires a revision to be specified'))
+ if basef:
+ raise error.ParseError(
+ 'rebase', _('cannot specify a base with detach'))
+
cmdutil.bail_if_changed(repo)
- result = buildstate(repo, destf, srcf, basef)
+ result = buildstate(repo, destf, srcf, basef, detachf)
if not result:
# Empty state built, nothing to rebase
ui.status(_('nothing to rebase\n'))
@@ -140,10 +155,10 @@
state, targetancestors)
commitmsg = 'Collapsed revision'
for rebased in state:
- if rebased not in skipped:
+ if rebased not in skipped and state[rebased] != nullmerge:
commitmsg += '\n* %s' % repo[rebased].description()
commitmsg = ui.edit(commitmsg, repo.ui.username())
- concludenode(repo, rev, p1, external, commitmsg=commitmsg,
+ newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg,
extra=extrafn)
if 'qtip' in repo.tags():
@@ -151,11 +166,13 @@
if not keepf:
# Remove no more useful revisions
- if set(repo.changelog.descendants(min(state))) - set(state):
- ui.warn(_("warning: new changesets detected on source branch, "
- "not stripping\n"))
- else:
- repair.strip(ui, repo, repo[min(state)].node(), "strip")
+ 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:
+ repair.strip(ui, repo, repo[min(rebased)].node(), "strip")
clearstatus(repo)
ui.status(_("rebase completed\n"))
@@ -260,7 +277,10 @@
if P1n in targetancestors:
p1 = target
elif P1n in state:
- p1 = state[P1n]
+ if state[P1n] == nullmerge:
+ p1 = target
+ else:
+ p1 = state[P1n]
else: # P1n external
p1 = target
p2 = P1n
@@ -379,9 +399,10 @@
clearstatus(repo)
repo.ui.status(_('rebase aborted\n'))
-def buildstate(repo, dest, src, base):
+def buildstate(repo, dest, src, base, detach):
'Define which revisions are going to be rebased and where'
targetancestors = set()
+ detachset = set()
if not dest:
# Destination defaults to the latest revision in the current branch
@@ -400,6 +421,12 @@
if commonbase == repo[dest]:
raise util.Abort(_('source is descendant of destination'))
source = repo[src].rev()
+ if detach:
+ # We need to keep track of source's ancestors up to the common base
+ srcancestors = set(repo.changelog.ancestors(source))
+ baseancestors = set(repo.changelog.ancestors(commonbase.rev()))
+ detachset = srcancestors - baseancestors
+ detachset.remove(commonbase.rev())
else:
if base:
cwd = repo[base].rev()
@@ -426,6 +453,7 @@
repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
+ state.update(dict.fromkeys(detachset, nullmerge))
state[source] = nullrev
return repo['.'].rev(), repo[dest].rev(), state
@@ -468,9 +496,11 @@
('', 'collapse', False, _('collapse the rebased changesets')),
('', 'keep', False, _('keep original changesets')),
('', 'keepbranches', False, _('keep original branch names')),
+ ('', 'detach', False, _('force detaching of source from its original '
+ 'branch')),
('c', 'continue', False, _('continue an interrupted rebase')),
('a', 'abort', False, _('abort an interrupted rebase')),] +
templateopts,
- _('hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--keep] '
- '[--keepbranches] | [-c] | [-a]')),
+ _('hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--detach] '
+ '[--keep] [--keepbranches] | [-c] | [-a]')),
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-rebase-detach Sat Feb 06 10:51:50 2010 +0100
@@ -0,0 +1,68 @@
+#!/bin/sh
+
+echo "[extensions]" >> $HGRCPATH
+echo "graphlog=" >> $HGRCPATH
+echo "rebase=" >> $HGRCPATH
+
+BASE=`pwd`
+
+addcommit () {
+ echo $1 > $1
+ hg add $1
+ hg commit -d "${2} 0" -m $1
+}
+
+commit () {
+ hg commit -d "${2} 0" -m $1
+}
+
+createrepo () {
+ cd $BASE
+ rm -rf a
+ hg init a
+ cd a
+ addcommit "A" 0
+ addcommit "B" 1
+ addcommit "C" 2
+ addcommit "D" 3
+
+ hg update -C 0
+ addcommit "E" 4
+}
+
+createrepo > /dev/null 2>&1
+hg glog --template '{rev}: {desc}\n'
+echo '% Rebasing D onto E detaching from C'
+hg rebase --detach -s 3 -d 4 2>&1 | sed 's/\(saving bundle to \).*/\1/'
+hg glog --template '{rev}: {desc}\n'
+echo "Expected A, D, E"
+hg manifest
+
+echo
+createrepo > /dev/null 2>&1
+hg glog --template '{rev}: {desc}\n'
+echo '% Rebasing C onto E detaching from B'
+hg rebase --detach -s 2 -d 4 2>&1 | sed 's/\(saving bundle to \).*/\1/'
+hg glog --template '{rev}: {desc}\n'
+echo "Expected A, C, D, E"
+hg manifest
+
+echo
+createrepo > /dev/null 2>&1
+hg glog --template '{rev}: {desc}\n'
+echo '% Rebasing B onto E using detach (same as not using it)'
+hg rebase --detach -s 1 -d 4 2>&1 | sed 's/\(saving bundle to \).*/\1/'
+hg glog --template '{rev}: {desc}\n'
+echo "Expected A, B, C, D, E"
+hg manifest
+
+echo
+createrepo > /dev/null 2>&1
+hg glog --template '{rev}: {desc}\n'
+echo '% Rebasing C onto E detaching from B and collapsing'
+hg rebase --detach --collapse -s 2 -d 4 2>&1 | sed 's/\(saving bundle to \).*/\1/'
+hg glog --template '{rev}: {desc}\n'
+echo "Expected A, C, D, E"
+hg manifest
+
+exit 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-rebase-detach.out Sat Feb 06 10:51:50 2010 +0100
@@ -0,0 +1,134 @@
+@ 4: E
+|
+| o 3: D
+| |
+| o 2: C
+| |
+| o 1: B
+|/
+o 0: A
+
+% Rebasing D onto E detaching from C
+saving bundle to
+adding branch
+adding changesets
+adding manifests
+adding file changes
+added 2 changesets with 2 changes to 2 files (+1 heads)
+rebase completed
+@ 4: D
+|
+o 3: E
+|
+| o 2: C
+| |
+| o 1: B
+|/
+o 0: A
+
+Expected A, D, E
+A
+D
+E
+
+@ 4: E
+|
+| o 3: D
+| |
+| o 2: C
+| |
+| o 1: B
+|/
+o 0: A
+
+% Rebasing C onto E detaching from B
+saving bundle to
+adding branch
+adding changesets
+adding manifests
+adding file changes
+added 3 changesets with 3 changes to 3 files (+1 heads)
+rebase completed
+@ 4: D
+|
+o 3: C
+|
+o 2: E
+|
+| o 1: B
+|/
+o 0: A
+
+Expected A, C, D, E
+A
+C
+D
+E
+
+@ 4: E
+|
+| o 3: D
+| |
+| o 2: C
+| |
+| o 1: B
+|/
+o 0: A
+
+% Rebasing B onto E using detach (same as not using it)
+saving bundle to
+adding branch
+adding changesets
+adding manifests
+adding file changes
+added 4 changesets with 4 changes to 4 files
+rebase completed
+@ 4: D
+|
+o 3: C
+|
+o 2: B
+|
+o 1: E
+|
+o 0: A
+
+Expected A, B, C, D, E
+A
+B
+C
+D
+E
+
+@ 4: E
+|
+| o 3: D
+| |
+| o 2: C
+| |
+| o 1: B
+|/
+o 0: A
+
+% Rebasing C onto E detaching from B and collapsing
+saving bundle to
+adding branch
+adding changesets
+adding manifests
+adding file changes
+added 2 changesets with 3 changes to 3 files (+1 heads)
+rebase completed
+@ 3: Collapsed revision
+| * C
+| * D
+o 2: E
+|
+| o 1: B
+|/
+o 0: A
+
+Expected A, C, D, E
+A
+C
+D
+E
--- a/tests/test-rebase-parameters.out Sun Jan 31 13:30:17 2010 +0100
+++ b/tests/test-rebase-parameters.out Sat Feb 06 10:51:50 2010 +0100
@@ -2,7 +2,7 @@
% Use continue and abort
hg rebase: cannot use both abort and continue
-hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--keep] [--keepbranches] | [-c] | [-a]
+hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--detach] [--keep] [--keepbranches] | [-c] | [-a]
move changeset (and descendants) to a different branch
@@ -21,6 +21,7 @@
--collapse collapse the rebased changesets
--keep keep original changesets
--keepbranches keep original branch names
+ --detach force detaching of source from its original branch
-c --continue continue an interrupted rebase
-a --abort abort an interrupted rebase
--style display using template map file
@@ -30,7 +31,7 @@
% Use continue and collapse
hg rebase: cannot use collapse with continue or abort
-hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--keep] [--keepbranches] | [-c] | [-a]
+hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--detach] [--keep] [--keepbranches] | [-c] | [-a]
move changeset (and descendants) to a different branch
@@ -49,6 +50,7 @@
--collapse collapse the rebased changesets
--keep keep original changesets
--keepbranches keep original branch names
+ --detach force detaching of source from its original branch
-c --continue continue an interrupted rebase
-a --abort abort an interrupted rebase
--style display using template map file
@@ -58,7 +60,7 @@
% Use continue/abort and dest/source
hg rebase: abort and continue do not allow specifying revisions
-hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--keep] [--keepbranches] | [-c] | [-a]
+hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--detach] [--keep] [--keepbranches] | [-c] | [-a]
move changeset (and descendants) to a different branch
@@ -77,6 +79,7 @@
--collapse collapse the rebased changesets
--keep keep original changesets
--keepbranches keep original branch names
+ --detach force detaching of source from its original branch
-c --continue continue an interrupted rebase
-a --abort abort an interrupted rebase
--style display using template map file
@@ -86,7 +89,7 @@
% Use source and base
hg rebase: cannot specify both a revision and a base
-hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--keep] [--keepbranches] | [-c] | [-a]
+hg rebase [-s REV | -b REV] [-d REV] [--collapse] [--detach] [--keep] [--keepbranches] | [-c] | [-a]
move changeset (and descendants) to a different branch
@@ -105,6 +108,7 @@
--collapse collapse the rebased changesets
--keep keep original changesets
--keepbranches keep original branch names
+ --detach force detaching of source from its original branch
-c --continue continue an interrupted rebase
-a --abort abort an interrupted rebase
--style display using template map file