[PATCH 1 of 2] rebase: add --detach to force detaching from the original branch (issue1950)
Stefano Tortarolo
stefano.tortarolo at gmail.com
Thu Dec 31 01:50:16 UTC 2009
# HG changeset patch
# User Stefano Tortarolo <stefano.tortarolo at gmail.com>
# Date 1262220625 -3600
# Node ID fcedd4d4282f33be5f86cd3a311bf937fe07528e
# Parent 513c89a60f203a8387d8dfa6f2dc67b21725790a
rebase: add --detach to force detaching from the original branch (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 performs 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.
diff --git a/hgext/rebase.py b/hgext/rebase.py
--- a/hgext/rebase.py
+++ b/hgext/rebase.py
@@ -22,7 +22,7 @@
from mercurial.i18n import _
import os, errno
-def rebasemerge(repo, rev, first=False):
+def rebasemerge(repo, rev, first=False, nullmerge=False):
'return the correct ancestor'
oldancestor = ancestor.ancestor
@@ -36,7 +36,10 @@
else:
repo.ui.debug("first revision, do not change ancestor\n")
try:
- stats = merge.update(repo, rev, True, True, False)
+ if nullmerge:
+ stats = merge.update(repo, rev, False, False, False)
+ else:
+ stats = merge.update(repo, rev, True, True, False)
return stats
finally:
ancestor.ancestor = oldancestor
@@ -55,6 +58,7 @@
external = nullrev
state = {}
skipped = set()
+ detach = nullrev
lock = wlock = None
try:
@@ -71,6 +75,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:
@@ -80,12 +85,16 @@
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'))
(originalwd, target, state, collapsef, keepf,
- keepbranchesf, external) = restorestatus(repo)
+ keepbranchesf, detach, external) = restorestatus(repo)
if abortf:
abort(repo, originalwd, target, state)
return
@@ -93,10 +102,21 @@
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 collapsef:
+ raise error.ParseError(
+ 'rebase', _('cannot use collapse with detach'))
+ if basef:
+ raise error.ParseError(
+ 'rebase', _('cannot specify a base with detach'))
+
cmdutil.bail_if_changed(repo)
- result = buildstate(repo, destf, srcf, basef, collapsef)
+ result = buildstate(repo, destf, srcf, basef, collapsef, detachf)
if result:
- originalwd, target, state, external = result
+ originalwd, target, state, detach, external = result
else: # Empty state built, nothing to rebase
ui.status(_('nothing to rebase\n'))
return
@@ -115,15 +135,15 @@
for rev in sorted(state):
if state[rev] == -1:
storestatus(repo, originalwd, target, state, collapsef, keepf,
- keepbranchesf, external)
+ keepbranchesf, detach, external)
rebasenode(repo, rev, target, state, skipped, targetancestors,
- collapsef, extrafn)
+ collapsef, detach, extrafn)
ui.note(_('rebase merging completed\n'))
if collapsef:
p1, p2 = defineparents(repo, min(state), target,
state, targetancestors)
- concludenode(repo, rev, p1, external, state, collapsef,
+ concludenode(repo, rev, p1, external, state, collapsef, detach=None,
last=True, skipped=skipped, extrafn=extrafn)
if 'qtip' in repo.tags():
@@ -135,7 +155,11 @@
ui.warn(_("warning: new changesets detected on source branch, "
"not stripping\n"))
else:
- repair.strip(ui, repo, repo[min(state)].node(), "strip")
+ if detach != nullrev:
+ strippoint = repo[detach].node()
+ else:
+ strippoint = repo[min(state)].node()
+ repair.strip(ui, repo, strippoint, "strip")
clearstatus(repo)
ui.status(_("rebase completed\n"))
@@ -146,13 +170,13 @@
finally:
release(lock, wlock)
-def concludenode(repo, rev, p1, p2, state, collapse, last=False, skipped=None,
+def concludenode(repo, rev, p1, p2, state, collapse, detach, last=False, skipped=None,
extrafn=None):
"""Skip commit if collapsing has been required and rev is not the last
revision, commit otherwise
"""
repo.ui.debug(" set parents\n")
- if collapse and not last:
+ if (collapse and not last) or (detach and rev < detach):
repo.dirstate.setparents(repo[p1].node())
return None
@@ -187,12 +211,15 @@
raise
def rebasenode(repo, rev, target, state, skipped, targetancestors, collapse,
- extrafn):
+ detach, extrafn):
'Rebase a single revision'
repo.ui.debug("rebasing %d:%s\n" % (rev, repo[rev]))
p1, p2 = defineparents(repo, rev, target, state, targetancestors)
-
+ nullmerge = detach and rev < detach
+ if nullmerge:
+ repo.ui.debug(" ignoring parent %d\n" % repo[p2].rev())
+ p2 = nullrev
repo.ui.debug(" future parents are %d and %d\n" % (repo[p1].rev(),
repo[p2].rev()))
@@ -207,7 +234,7 @@
repo.dirstate.write()
repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev]))
first = repo[rev].rev() == repo[min(state)].rev()
- stats = rebasemerge(repo, rev, first)
+ stats = rebasemerge(repo, rev, first, nullmerge)
if stats[3] > 0:
raise util.Abort(_('fix unresolved conflicts with hg resolve then '
@@ -227,7 +254,7 @@
if v in m2 and v not in m1:
repo.dirstate.remove(v)
- newrev = concludenode(repo, rev, p1, p2, state, collapse,
+ newrev = concludenode(repo, rev, p1, p2, state, collapse, detach,
extrafn=extrafn)
# Update the state
@@ -299,12 +326,13 @@
repo.mq.save_dirty()
def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches,
- external):
+ detach, external):
'Store the current status to allow recovery'
f = repo.opener("rebasestate", "w")
f.write(repo[originalwd].hex() + '\n')
f.write(repo[target].hex() + '\n')
f.write(repo[external].hex() + '\n')
+ f.write(repo[detach].hex() + '\n')
f.write('%d\n' % int(collapse))
f.write('%d\n' % int(keep))
f.write('%d\n' % int(keepbranches))
@@ -336,16 +364,19 @@
elif i == 2:
external = repo[l].rev()
elif i == 3:
+ detach = repo[l].rev()
+ elif i == 4:
collapse = bool(int(l))
- elif i == 4:
+ elif i == 5:
keep = bool(int(l))
- elif i == 5:
+ elif i == 6:
keepbranches = bool(int(l))
else:
oldrev, newrev = l.split(':')
state[repo[oldrev].rev()] = repo[newrev].rev()
repo.ui.debug('rebase status resumed\n')
- return originalwd, target, state, collapse, keep, keepbranches, external
+ return (originalwd, target, state, collapse, keep, keepbranches,
+ detach, external)
except IOError, err:
if err.errno != errno.ENOENT:
raise
@@ -366,9 +397,10 @@
clearstatus(repo)
repo.ui.status(_('rebase aborted\n'))
-def buildstate(repo, dest, src, base, collapse):
+def buildstate(repo, dest, src, base, collapse, detach):
'Define which revisions are going to be rebased and where'
targetancestors = set()
+ detachrev = nullrev
if not dest:
# Destination defaults to the latest revision in the current branch
@@ -387,6 +419,16 @@
if commonbase == repo[dest]:
raise util.Abort(_('source is descendant of destination'))
source = repo[src].rev()
+ if detach:
+ # If detach is required 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())
+ detachrev = source
+ if detachset:
+ source = min(detachset)
else:
if base:
cwd = repo[base].rev()
@@ -411,7 +453,10 @@
rebasingbranch = cwdancestors - targetancestors
source = min(rebasingbranch)
- repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
+ if detachrev:
+ repo.ui.debug('rebase onto %d starting from %d\n' % (dest, detachrev))
+ else:
+ repo.ui.debug('rebase onto %d starting from %d\n' % (dest, source))
state = dict.fromkeys(repo.changelog.descendants(source), nullrev)
external = nullrev
if collapse:
@@ -428,7 +473,7 @@
external = p.rev()
state[source] = nullrev
- return repo['.'].rev(), repo[dest].rev(), state, external
+ return repo['.'].rev(), repo[dest].rev(), state, detachrev, external
def pullrebase(orig, ui, repo, *args, **opts):
'Call rebase after pull if the latter has been invoked with --rebase'
@@ -469,9 +514,10 @@
('', '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] '
+ _('hg rebase [-s REV | -b REV] [-d REV] [--collapse | --detach] [--keep] '
'[--keepbranches] | [-c] | [-a]')),
}
diff --git a/tests/test-rebase-detach b/tests/test-rebase-detach
new file mode 100755
--- /dev/null
+++ b/tests/test-rebase-detach
@@ -0,0 +1,59 @@
+#!/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
+
+exit 0
diff --git a/tests/test-rebase-detach.out b/tests/test-rebase-detach.out
new file mode 100644
--- /dev/null
+++ b/tests/test-rebase-detach.out
@@ -0,0 +1,102 @@
+@ 4: E
+|
+| o 3: D
+| |
+| o 2: C
+| |
+| o 1: B
+|/
+o 0: A
+
+% Rebasing D onto E detaching from C
+not in dirstate: E
+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
diff --git a/tests/test-rebase-parameters b/tests/test-rebase-parameters
--- a/tests/test-rebase-parameters
+++ b/tests/test-rebase-parameters
@@ -57,6 +57,14 @@
hg update 6
hg rebase
+echo
+echo "% Specify detach and not source"
+hg rebase --detach 2>&1 | sed 's/\(saving bundle to \).*/\1/'
+
+echo
+echo "% Specify detach and collapse"
+hg rebase --detach --source 2 --collapse 2>&1 | sed 's/\(saving bundle to \).*/\1/'
+
echo "% ----------"
echo "% These work"
echo
diff --git a/tests/test-rebase-parameters.out b/tests/test-rebase-parameters.out
--- a/tests/test-rebase-parameters.out
+++ b/tests/test-rebase-parameters.out
@@ -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
@@ -118,6 +122,64 @@
% Rebase with no arguments - from the current branch
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
nothing to rebase
+
+% Specify detach and not source
+hg rebase: detach requires a revision to be specified
+hg rebase [-s REV | -b REV] [-d REV] [--collapse | --detach] [--keep] [--keepbranches] | [-c] | [-a]
+
+move changeset (and descendants) to a different branch
+
+ Rebase uses repeated merging to graft changesets from one part of history
+ onto another. This can be useful for linearizing local changes relative to
+ a master development tree.
+
+ If a rebase is interrupted to manually resolve a merge, it can be
+ continued with --continue/-c or aborted with --abort/-a.
+
+options:
+
+ -s --source rebase from a given revision
+ -b --base rebase from the base of a given revision
+ -d --dest rebase onto a given revision
+ --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
+ --template display with template
+
+use "hg -v help rebase" to show global options
+
+% Specify detach and collapse
+hg rebase: cannot use collapse with detach
+hg rebase [-s REV | -b REV] [-d REV] [--collapse | --detach] [--keep] [--keepbranches] | [-c] | [-a]
+
+move changeset (and descendants) to a different branch
+
+ Rebase uses repeated merging to graft changesets from one part of history
+ onto another. This can be useful for linearizing local changes relative to
+ a master development tree.
+
+ If a rebase is interrupted to manually resolve a merge, it can be
+ continued with --continue/-c or aborted with --abort/-a.
+
+options:
+
+ -s --source rebase from a given revision
+ -b --base rebase from the base of a given revision
+ -d --dest rebase onto a given revision
+ --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
+ --template display with template
+
+use "hg -v help rebase" to show global options
% ----------
% These work
More information about the Mercurial-devel
mailing list