[PATCH] rebase: do not add second parent to rebased changeset (drop detach option) (BC)

pierre-yves.david at logilab.fr pierre-yves.david at logilab.fr
Thu Jun 7 06:58:22 CDT 2012


# HG changeset patch
# User Pierre-Yves David <pierre-yves.david at ens-lyon.org>
# Date 1339069890 -7200
# Node ID 146a5994a0dbe3f6dd5dcb05bacfdff6f92a8c97
# Parent  2255950e1f7663a9faa6b57040cc5c0debe7d4dd
rebase: do not add second parent to rebased changeset (drop detach option) (BC)

Rebase now always keeps the number of parents constant. merge are rebased as
merge and non-merge are rebased with single parent. This mean that hg rebase
always "detach" the rebase set from it's original branch if the root changeset
only have a single parent.

This only alter the behavior of rebase when using the --source option. The
detach option itself is dropped.

See in comment in code for details on the Why and How of the new behavior.

All test changes have been manually validated with care.

diff --git a/hgext/rebase.py b/hgext/rebase.py
--- a/hgext/rebase.py
+++ b/hgext/rebase.py
@@ -46,12 +46,11 @@ testedwith = 'internal'
     ('e', 'edit', False, _('invoke editor on commit messages')),
     ('l', 'logfile', '',
      _('read collapse commit message from file'), _('FILE')),
     ('', 'keep', False, _('keep original changesets')),
     ('', 'keepbranches', False, _('keep original branch names')),
-    ('D', 'detach', False, _('force detaching of source from its original '
-                            'branch')),
+    ('D', 'detach', False, _('do nothing (DEPRECATED)')),
     ('t', 'tool', '', _('specify merge tool')),
     ('c', 'continue', False, _('continue an interrupted rebase')),
     ('a', 'abort', False, _('abort an interrupted rebase'))] +
      templateopts,
     _('hg rebase [-s REV | -b REV] [-d REV] [options]\n'
@@ -129,11 +128,10 @@ def rebase(ui, repo, **opts):
         collapsef = opts.get('collapse', False)
         collapsemsg = cmdutil.logmessage(ui, opts)
         extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion
         keepf = opts.get('keep', False)
         keepbranchesf = opts.get('keepbranches', False)
-        detachf = opts.get('detach', False)
         # keepopen is not meant for use on the command line, but by
         # other extensions
         keepopen = opts.get('keepopen', False)
 
         if collapsemsg and not collapsef:
@@ -144,12 +142,10 @@ def rebase(ui, repo, **opts):
             if contf and abortf:
                 raise util.Abort(_('cannot use both abort and continue'))
             if collapsef:
                 raise util.Abort(
                     _('cannot use collapse with continue or abort'))
-            if detachf:
-                raise util.Abort(_('cannot use detach with continue or abort'))
             if srcf or basef or destf:
                 raise util.Abort(
                     _('abort and continue do not allow specifying revisions'))
             if opts.get('tool', False):
                 ui.warn(_('tool option will be ignored\n'))
@@ -166,16 +162,10 @@ def rebase(ui, repo, **opts):
                 raise util.Abort(_('cannot specify both a '
                                    'revision and a base'))
             if revf and srcf:
                 raise util.Abort(_('cannot specify both a '
                                    'revision and a source'))
-            if detachf:
-                if not (srcf or revf):
-                    raise util.Abort(
-                        _('detach requires a revision to be specified'))
-                if basef:
-                    raise util.Abort(_('cannot specify a base with detach'))
 
             cmdutil.bailifchanged(repo)
 
             if not destf:
                 # Destination defaults to the latest revision in the
@@ -213,11 +203,11 @@ def rebase(ui, repo, **opts):
             elif not keepf and not repo[root].mutable():
                 raise util.Abort(_("can't rebase immutable changeset %s")
                                  % repo[root],
                                  hint=_('see hg help phases for details'))
             else:
-                result = buildstate(repo, dest, rebaseset, detachf, collapsef)
+                result = buildstate(repo, dest, rebaseset, collapsef)
 
             if not result:
                 # Empty state built, nothing to rebase
                 ui.status(_('nothing to rebase\n'))
                 return 1
@@ -590,26 +580,25 @@ def abort(repo, originalwd, target, stat
             repair.strip(repo.ui, repo, repo[strippoint].node())
         clearstatus(repo)
         repo.ui.warn(_('rebase aborted\n'))
         return 0
 
-def buildstate(repo, dest, rebaseset, detach, collapse):
+def buildstate(repo, dest, rebaseset, collapse):
     '''Define which revisions are going to be rebased and where
 
     repo: repo
     dest: context
     rebaseset: set of rev
-    detach: boolean'''
+    '''
 
     # This check isn't strictly necessary, since mq detects commits over an
     # applied patch. But it prevents messing up the working directory when
     # a partially completed rebase is blocked by mq.
     if 'qtip' in repo.tags() and (dest.node() in
                             [s.node for s in repo.mq.applied]):
         raise util.Abort(_('cannot rebase onto an applied mq patch'))
 
-    detachset = set()
     roots = list(repo.set('roots(%ld)', rebaseset))
     if not roots:
         raise util.Abort(_('no matching revisions'))
     if len(roots) > 1:
         raise util.Abort(_("can't rebase multiple roots"))
@@ -621,18 +610,64 @@ def buildstate(repo, dest, rebaseset, de
     if commonbase == dest:
         samebranch = root.branch() == dest.branch()
         if not collapse and samebranch and root in dest.children():
             repo.ui.debug('source is a child of destination\n')
             return None
-        # rebase on ancestor, force detach
-        detach = True
-    if detach:
-        detachset = repo.revs('::%d - ::%d - %d', root, commonbase, root)
 
     repo.ui.debug('rebase onto %d starting from %d\n' % (dest, root))
     state = dict.fromkeys(rebaseset, nullrev)
-    state.update(dict.fromkeys(detachset, nullmerge))
+    # Detach root from it's former parent
+    # If root is a merge we try to preserve it.
+    # (possible if at least one parent is ancestor of destination)
+    #
+    # Full Explanation
+    # -----------------
+    #
+    # Rebase try to preserve the number of parents of rebased changesets:
+    #
+    # - A changeset with a single parent will always be rebased as a changeset
+    #   with a single parent.
+    #
+    # - A merge changeset with two parent will likely be rebased as a changeset
+    #   with two parents. (It can have fewer parents in some special cases
+    #   ignored here)
+    #
+    # The goal of rebase is to make <dest> a parent of <root>. It's is
+    # very simple if at least one parent of <root> is an ancestor of
+    # <dest>. In this case the "rebased" version of this parent is
+    # obviously <dest>. This is always the case when <root> where
+    # specified using the --base option, .
+    #
+    # If no parent of <root> is an ancestor of <dest> this is more
+    # complex. To keep the same number of parents for the rebased
+    # version of <root> we need to *replace* the original parent by
+    # <dest>. This "detach" the rebased set from it's former
+    # localisation and rebase it onto <dest>. When "detaching" the
+    # changeset, changes introduced by ancestors of <root> which are
+    # not common with <dest> are "removed" from the rebased changeset.
+    #
+    # - If <root> has a single parent, we just need to drop it.
+    #
+    # - If <root> is a merge, we can't decide which parent to drop. Dropping
+    #   both parents does not make sense.
+    #
+    # The table below sum up this behavior:
+    #
+    # +--------------------+----------------------+-------------------------+
+    # |                    |     one parent       |  merge                  |
+    # +--------------------+----------------------+-------------------------+
+    # | parent in ::<dest> | new parent is <dest> | parents in ::<dest> are |
+    # |                    |                      | remapped to <dest>      |
+    # +--------------------+----------------------+-------------------------+
+    # | unrelated source   | new parent is <dest> | Ambiguous rebase abort  |
+    # +--------------------+----------------------+-------------------------+
+    #
+    # The actual abort is handled by `defineparents`
+    if len(root.parents()) <= 1:
+        # (strict) ancestors of <root> not ancestors of <dest>
+        detachset = repo.revs('::%d - ::%d - %d', root, commonbase, root)
+        state.update(dict.fromkeys(detachset, nullmerge))
     return repo['.'].rev(), dest.rev(), state
 
 def pullrebase(orig, ui, repo, *args, **opts):
     'Call rebase after pull if the latter has been invoked with --rebase'
     if opts.get('rebase'):
diff --git a/tests/test-bookmarks-rebase.t b/tests/test-bookmarks-rebase.t
--- a/tests/test-bookmarks-rebase.t
+++ b/tests/test-bookmarks-rebase.t
@@ -37,15 +37,14 @@ rebase
 
   $ hg rebase -s two -d one
   saved backup bundle to $TESTTMP/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg log
-  changeset:   3:9163974d1cb5
+  changeset:   3:42e5ed2cdcf4
   bookmark:    two
   tag:         tip
   parent:      1:925d80f479bb
-  parent:      2:db815d6d32e6
   user:        test
   date:        Thu Jan 01 00:00:00 1970 +0000
   summary:     3
   
   changeset:   2:db815d6d32e6
diff --git a/tests/test-rebase-bookmarks.t b/tests/test-rebase-bookmarks.t
--- a/tests/test-rebase-bookmarks.t
+++ b/tests/test-rebase-bookmarks.t
@@ -52,11 +52,11 @@ Move only rebased bookmarks
   $ hg clone -q a a1
 
   $ cd a1
   $ hg up -q Z
 
-  $ hg rebase --detach -s Y -d 3
+  $ hg rebase -s Y -d 3
   saved backup bundle to $TESTTMP/a1/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog 
   @  3: 'C' bookmarks: Y Z
   |
diff --git a/tests/test-rebase-cache.t b/tests/test-rebase-cache.t
--- a/tests/test-rebase-cache.t
+++ b/tests/test-rebase-cache.t
@@ -102,11 +102,11 @@ Rebase part of branch2 (5-6) onto branch
   6: 'E' branch2
   4: 'C' branch2
   2: 'B' branch1
   0: 'A' 
 
-  $ hg rebase --detach -s 5 -d 8
+  $ hg rebase -s 5 -d 8
   saved backup bundle to $TESTTMP/a1/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg branches
   branch3                        8:466cdfb14b62
   branch2                        4:e4fdb121d036
@@ -163,11 +163,11 @@ Rebase head of branch3 (8) onto branch2 
   | |
   | o  1: 'branch1' branch1
   |/
   o  0: 'A'
   
-  $ hg rebase --detach -s 8 -d 6
+  $ hg rebase -s 8 -d 6
   saved backup bundle to $TESTTMP/a2/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg branches
   branch2                        8:6b4bdc1b5ac0
   branch3                        7:653b9feb4616
@@ -227,11 +227,11 @@ Rebase entire branch3 (7-8) onto branch2
   | |
   | o  1: 'branch1' branch1
   |/
   o  0: 'A'
   
-  $ hg rebase --detach -s 7 -d 6
+  $ hg rebase -s 7 -d 6
   saved backup bundle to $TESTTMP/a3/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg branches
   branch2                        7:6b4bdc1b5ac0
   branch1                        2:0a03079c47fd (inactive)
diff --git a/tests/test-rebase-collapse.t b/tests/test-rebase-collapse.t
--- a/tests/test-rebase-collapse.t
+++ b/tests/test-rebase-collapse.t
@@ -228,11 +228,11 @@ Rebase and collapse - more than one exte
   abort: unable to collapse, there is more than one external parent
   [255]
 
 Rebase and collapse - E onto H:
 
-  $ hg rebase -s 4 --collapse
+  $ hg rebase -s 4 --collapse # root (4) is not a merge
   saved backup bundle to $TESTTMP/b1/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
   @    5: 'Collapsed revision
   |\   * E
@@ -248,11 +248,10 @@ Rebase and collapse - E onto H:
   |/
   o  0: 'A'
   
   $ hg manifest
   A
-  B
   C
   D
   E
   F
   H
@@ -338,11 +337,11 @@ Create repo c:
 Rebase and collapse - E onto I:
 
   $ hg clone -q -u . c c1
   $ cd c1
 
-  $ hg rebase -s 4 --collapse
+  $ hg rebase -s 4 --collapse # root (4) is not a merge
   merging E
   saved backup bundle to $TESTTMP/c1/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
   @    5: 'Collapsed revision
@@ -360,11 +359,10 @@ Rebase and collapse - E onto I:
   |/
   o  0: 'A'
   
   $ hg manifest
   A
-  B
   C
   D
   E
   G
   I
diff --git a/tests/test-rebase-detach.t b/tests/test-rebase-detach.t
--- a/tests/test-rebase-detach.t
+++ b/tests/test-rebase-detach.t
@@ -46,11 +46,11 @@ Rebasing D onto H detaching from C:
   | o  1: 'B'
   |/
   o  0: 'A'
   
   $ hg phase --force --secret 3
-  $ hg rebase --detach -s 3 -d 7
+  $ hg rebase -s 3 -d 7
   saved backup bundle to $TESTTMP/a1/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg log -G --template "{rev}:{phase} '{desc}' {branches}\n"
   @  7:secret 'D'
   |
@@ -97,11 +97,11 @@ Rebasing C onto H detaching from B:
   | |
   | o  1: 'B'
   |/
   o  0: 'A'
   
-  $ hg rebase --detach -s 2 -d 7
+  $ hg rebase -s 2 -d 7
   saved backup bundle to $TESTTMP/a2/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
   @  7: 'D'
   |
@@ -149,11 +149,11 @@ Rebasing B onto H using detach (same as 
   | |
   | o  1: 'B'
   |/
   o  0: 'A'
   
-  $ hg rebase --detach -s 1 -d 7
+  $ hg rebase -s 1 -d 7
   saved backup bundle to $TESTTMP/a3/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
   @  7: 'D'
   |
@@ -203,11 +203,11 @@ Rebasing C onto H detaching from B and c
   | |
   | o  1: 'B'
   |/
   o  0: 'A'
   
-  $ hg rebase --detach --collapse -s 2 -d 7
+  $ hg rebase --collapse -s 2 -d 7
   saved backup bundle to $TESTTMP/a4/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg  log -G --template "{rev}:{phase} '{desc}' {branches}\n"
   @  6:secret 'Collapsed revision
   |  * C
@@ -262,11 +262,11 @@ Rebasing across null as ancestor
   | |
   | o  1: 'B'
   |/
   o  0: 'A'
   
-  $ hg rebase --detach -s 1 -d tip
+  $ hg rebase -s 1 -d tip
   saved backup bundle to $TESTTMP/a5/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
   @  8: 'D'
   |
@@ -323,11 +323,11 @@ Verify that target is not selected as ex
   (branch merge, don't forget to commit)
   $ hg ci -m "Merge"
   $ echo "J" >> F
   $ hg ci -m "J"
 
-  $ hg rebase -s 8 -d 7 --collapse --detach --config ui.merge=internal:other
+  $ hg rebase -s 8 -d 7 --collapse --config ui.merge=internal:other
   remote changed E which local deleted
   use (c)hanged version or leave (d)eleted? c
   saved backup bundle to $TESTTMP/a6/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
@@ -368,11 +368,11 @@ Ensure --continue restores a correct sta
   $ hg up -q 3
   $ echo 'H2' > H
   $ hg ci -A -m 'H2'
   adding H
   $ hg phase --force --secret 8
-  $ hg rebase -s 8 -d 7 --detach --config ui.merge=internal:fail
+  $ hg rebase -s 8 -d 7 --config ui.merge=internal:fail
   merging H
   warning: conflicts during merge.
   merging H incomplete! (edit conflicts, then use 'hg resolve --mark')
   abort: unresolved conflicts (see hg resolve, then hg rebase --continue)
   [255]
diff --git a/tests/test-rebase-parameters.t b/tests/test-rebase-parameters.t
--- a/tests/test-rebase-parameters.t
+++ b/tests/test-rebase-parameters.t
@@ -197,23 +197,23 @@ Specify only source (from 2 onto 8):
   saved backup bundle to $TESTTMP/a4/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
   @  8: 'D'
   |
-  o    7: 'C'
-  |\
-  | o  6: 'I'
+  o  7: 'C'
+  |
+  o  6: 'I'
+  |
+  o  5: 'H'
+  |
+  | o  4: 'G'
+  |/|
+  o |  3: 'F'
   | |
-  | o  5: 'H'
-  | |
-  | | o  4: 'G'
-  | |/|
-  | o |  3: 'F'
-  | | |
-  | | o  2: 'E'
-  | |/
-  o |  1: 'B'
+  | o  2: 'E'
+  |/
+  | o  1: 'B'
   |/
   o  0: 'A'
   
   $ cd ..
 
@@ -281,11 +281,11 @@ Specify only base (from 1 onto 8):
 Specify source and dest (from 2 onto 7):
 
   $ hg clone -q -u . a a7
   $ cd a7
 
-  $ hg rebase --detach --source 2 --dest 7
+  $ hg rebase --source 2 --dest 7
   saved backup bundle to $TESTTMP/a7/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
   @  8: 'D'
   |
@@ -347,23 +347,23 @@ Specify only revs (from 2 onto 8)
   saved backup bundle to $TESTTMP/a9/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
   @  8: 'D'
   |
-  o    7: 'C'
-  |\
-  | o  6: 'I'
+  o  7: 'C'
+  |
+  o  6: 'I'
+  |
+  o  5: 'H'
+  |
+  | o  4: 'G'
+  |/|
+  o |  3: 'F'
   | |
-  | o  5: 'H'
-  | |
-  | | o  4: 'G'
-  | |/|
-  | o |  3: 'F'
-  | | |
-  | | o  2: 'E'
-  | |/
-  o |  1: 'B'
+  | o  2: 'E'
+  |/
+  | o  1: 'B'
   |/
   o  0: 'A'
   
   $ cd ..
 
diff --git a/tests/test-rebase-scenario-global.t b/tests/test-rebase-scenario-global.t
--- a/tests/test-rebase-scenario-global.t
+++ b/tests/test-rebase-scenario-global.t
@@ -50,23 +50,23 @@ D onto H - simple rebase:
 
   $ hg rebase -s 3 -d 7
   saved backup bundle to $TESTTMP/a1/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
-  @    7: 'D'
-  |\
-  | o  6: 'H'
+  @  7: 'D'
+  |
+  o  6: 'H'
+  |
+  | o  5: 'G'
+  |/|
+  o |  4: 'F'
   | |
-  | | o  5: 'G'
-  | |/|
-  | o |  4: 'F'
-  | | |
-  | | o  3: 'E'
-  | |/
-  o |  2: 'C'
+  | o  3: 'E'
+  |/
+  | o  2: 'C'
   | |
-  o |  1: 'B'
+  | o  1: 'B'
   |/
   o  0: 'A'
   
   $ cd ..
 
@@ -78,23 +78,23 @@ D onto F - intermediate point:
 
   $ hg rebase -s 3 -d 5
   saved backup bundle to $TESTTMP/a2/.hg/strip-backup/*-backup.hg (glob)
 
   $ hg tglog
-  @    7: 'D'
-  |\
-  | | o  6: 'H'
-  | |/
-  | | o  5: 'G'
-  | |/|
-  | o |  4: 'F'
-  | | |
-  | | o  3: 'E'
-  | |/
-  o |  2: 'C'
+  @  7: 'D'
+  |
+  | o  6: 'H'
+  |/
+  | o  5: 'G'
+  |/|
+  o |  4: 'F'
   | |
-  o |  1: 'B'
+  | o  3: 'E'
+  |/
+  | o  2: 'C'
+  | |
+  | o  1: 'B'
   |/
   o  0: 'A'
   
   $ cd ..
 
@@ -304,11 +304,11 @@ Source phase greater or equal to destina
   secret
 Source phase lower than destination phase: new changeset get the phase of destination:
   $ hg rebase -s7 -d9
   saved backup bundle to $TESTTMP/a7/.hg/strip-backup/c9659aac0000-backup.hg (glob)
   $ hg log --template "{phase}\n" -r 9
-  secret
+  draft
 
   $ cd ..
 
 Test for revset
 
@@ -402,24 +402,24 @@ Base on have one descendant heads we ask
   |
   o  11: 'H'
   |
   o  10: 'G'
   |
-  o    9: 'D'
-  |\
-  | | o  8: 'I'
+  o  9: 'D'
+  |
+  | o  8: 'I'
+  | |
+  | o  7: 'H'
+  | |
+  | o  6: 'G'
+  | |
+  | | o  5: 'F'
   | | |
-  | | o  7: 'H'
-  | | |
-  | | o  6: 'G'
-  | | |
-  | | | o  5: 'F'
-  | | | |
-  | | | o  4: 'E'
-  | | |/
-  | | o  3: 'D'
+  | | o  4: 'E'
   | |/
+  | o  3: 'D'
+  | |
   | o  2: 'C'
   | |
   o |  1: 'B'
   |/
   o  0: 'A'
@@ -439,24 +439,24 @@ rebase subset
   $ hg tglog
   @  11: 'H'
   |
   o  10: 'G'
   |
-  o    9: 'D'
-  |\
-  | | o  8: 'I'
+  o  9: 'D'
+  |
+  | o  8: 'I'
+  | |
+  | o  7: 'H'
+  | |
+  | o  6: 'G'
+  | |
+  | | o  5: 'F'
   | | |
-  | | o  7: 'H'
-  | | |
-  | | o  6: 'G'
-  | | |
-  | | | o  5: 'F'
-  | | | |
-  | | | o  4: 'E'
-  | | |/
-  | | o  3: 'D'
+  | | o  4: 'E'
   | |/
+  | o  3: 'D'
+  | |
   | o  2: 'C'
   | |
   o |  1: 'B'
   |/
   o  0: 'A'
@@ -480,24 +480,24 @@ rebase subset with multiple head
   |
   | o  11: 'F'
   | |
   | o  10: 'E'
   |/
-  o    9: 'D'
-  |\
-  | | o  8: 'I'
+  o  9: 'D'
+  |
+  | o  8: 'I'
+  | |
+  | o  7: 'H'
+  | |
+  | o  6: 'G'
+  | |
+  | | o  5: 'F'
   | | |
-  | | o  7: 'H'
-  | | |
-  | | o  6: 'G'
-  | | |
-  | | | o  5: 'F'
-  | | | |
-  | | | o  4: 'E'
-  | | |/
-  | | o  3: 'D'
+  | | o  4: 'E'
   | |/
+  | o  3: 'D'
+  | |
   | o  2: 'C'
   | |
   o |  1: 'B'
   |/
   o  0: 'A'


More information about the Mercurial-devel mailing list