[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