# HG changeset patch # User Stefano Tortarolo # Date 2010-02-06 09:51:50 # Node ID 66d954e76ffb621a4a54aad3705bb28528b71da9 # Parent 38fe86fb16e3d8f67e94f58e682df35edec53105 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. diff --git a/hgext/rebase.py b/hgext/rebase.py --- a/hgext/rebase.py +++ b/hgext/rebase.py @@ -22,6 +22,8 @@ from mercurial.lock import release 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 @@ def rebase(ui, repo, **opts): 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 @@ def rebase(ui, repo, **opts): 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 @@ def rebase(ui, repo, **opts): 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 @@ def rebase(ui, repo, **opts): 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 @@ def rebase(ui, repo, **opts): 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 @@ def defineparents(repo, rev, target, sta 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 @@ def abort(repo, originalwd, target, stat 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 @@ def buildstate(repo, dest, src, base): 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 @@ def buildstate(repo, dest, src, base): 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 @@ cmdtable = { ('', '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]')), } 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,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 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,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 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 @@ options: --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 "hg -v help rebase" to show global o % 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 @@ options: --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 "hg -v help rebase" to show global o % 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 @@ options: --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 "hg -v help rebase" to show global o % 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 @@ options: --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