rebase: add --detach option to detach intermediate revisions (issue1950)
authorStefano Tortarolo <stefano.tortarolo@gmail.com>
Sat, 06 Feb 2010 10:51:50 +0100
changeset 10352 66d954e76ffb
parent 10351 38fe86fb16e3
child 10353 36b6b5ef7820
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.
hgext/rebase.py
tests/test-rebase-detach
tests/test-rebase-detach.out
tests/test-rebase-parameters.out
--- 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