rebase: allow rebase even if some revisions need no rebase (BC) (issue5422)
authorMartin von Zweigbergk <martinvonz@google.com>
Thu, 11 May 2017 11:37:18 -0700
changeset 32312 78496ac30025
parent 32311 6096d27dc119
child 32313 2e455cbeac50
rebase: allow rebase even if some revisions need no rebase (BC) (issue5422) This allows you to do e.g. "hg rebase -d @ -r 'draft()'" even if some drafts are already based off of @. You'd still need to exclude obsolete and troubled revisions, though. We will deal with those cases later. Implemented by treating state[rev]==rev as "no need to rebase". I considered adding another fake revision number like revdone=-6. That would make the code clearer in a few places, but would add extra code in other places. I moved the existing test out of test-rebase-base.t and into a new file and added more tests there, since not all are using --base.
hgext/rebase.py
tests/test-rebase-base.t
tests/test-rebase-partial.t
--- a/hgext/rebase.py	Wed May 10 11:55:22 2017 -0700
+++ b/hgext/rebase.py	Thu May 11 11:37:18 2017 -0700
@@ -384,7 +384,9 @@
             names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
             if names:
                 desc += ' (%s)' % ' '.join(names)
-            if self.state[rev] == revtodo:
+            if self.state[rev] == rev:
+                ui.status(_('already rebased %s\n') % desc)
+            elif self.state[rev] == revtodo:
                 pos += 1
                 ui.status(_('rebasing %s\n') % desc)
                 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
@@ -508,7 +510,7 @@
             # Nodeids are needed to reset bookmarks
             nstate = {}
             for k, v in self.state.iteritems():
-                if v > nullmerge:
+                if v > nullmerge and v != k:
                     nstate[repo[k].node()] = repo[v].node()
                 elif v == revprecursor:
                     succ = self.obsoletenotrebased[k]
@@ -1248,6 +1250,7 @@
     roots.sort()
     state = dict.fromkeys(rebaseset, revtodo)
     detachset = set()
+    emptyrebase = True
     for root in roots:
         commonbase = root.ancestor(dest)
         if commonbase == root:
@@ -1260,9 +1263,13 @@
             else:
                 samebranch = root.branch() == dest.branch()
             if not collapse and samebranch and root in dest.children():
+                # mark the revision as done by setting its new revision
+                # equal to its old (current) revisions
+                state[root.rev()] = root.rev()
                 repo.ui.debug('source is a child of destination\n')
-                return None
+                continue
 
+        emptyrebase = False
         repo.ui.debug('rebase onto %s starting from %s\n' % (dest, root))
         # Rebase tries to turn <dest> into a parent of <root> while
         # preserving the number of parents of rebased changesets:
@@ -1305,6 +1312,13 @@
             # ancestors of <root> not ancestors of <dest>
             detachset.update(repo.changelog.findmissingrevs([commonbase.rev()],
                                                             [root.rev()]))
+    if emptyrebase:
+        return None
+    for rev in sorted(state):
+        parents = [p for p in repo.changelog.parentrevs(rev) if p != nullrev]
+        # if all parents of this revision are done, then so is this revision
+        if parents and all((state.get(p) == p for p in parents)):
+            state[rev] = rev
     for r in detachset:
         if r not in state:
             state[r] = nullmerge
@@ -1332,7 +1346,7 @@
     if obsolete.isenabled(repo, obsolete.createmarkersopt):
         markers = []
         for rev, newrev in sorted(state.items()):
-            if newrev >= 0:
+            if newrev >= 0 and newrev != rev:
                 if rev in skipped:
                     succs = ()
                 elif collapsedas is not None:
@@ -1343,7 +1357,8 @@
         if markers:
             obsolete.createmarkers(repo, markers)
     else:
-        rebased = [rev for rev in state if state[rev] > nullmerge]
+        rebased = [rev for rev in state
+                   if state[rev] > nullmerge and state[rev] != rev]
         if rebased:
             stripped = []
             for root in repo.set('roots(%ld)', rebased):
--- a/tests/test-rebase-base.t	Wed May 10 11:55:22 2017 -0700
+++ b/tests/test-rebase-base.t	Thu May 11 11:37:18 2017 -0700
@@ -298,18 +298,6 @@
   |
   o  0: M0
   
-Mixed rebasable and non-rebasable bases (unresolved, issue5422):
-
-  $ rebasewithdag -b C+D -d B <<'EOS'
-  >   D
-  >  /
-  > B C
-  > |/
-  > A
-  > EOS
-  nothing to rebase
-  [1]
-
 Disconnected graph:
 
   $ rebasewithdag -b B -d Z <<'EOS'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-rebase-partial.t	Thu May 11 11:37:18 2017 -0700
@@ -0,0 +1,95 @@
+Tests rebasing with part of the rebase set already in the
+destination (issue5422)
+
+  $ cat >> $HGRCPATH <<EOF
+  > [extensions]
+  > rebase=
+  > drawdag=$TESTDIR/drawdag.py
+  > 
+  > [experimental]
+  > evolution=createmarkers,allowunstable
+  > 
+  > [alias]
+  > tglog = log -G --template "{rev}: {desc}"
+  > EOF
+
+  $ rebasewithdag() {
+  >   N=`$PYTHON -c "print($N+1)"`
+  >   hg init repo$N && cd repo$N
+  >   hg debugdrawdag
+  >   hg rebase "$@" > _rebasetmp
+  >   r=$?
+  >   grep -v 'saved backup bundle' _rebasetmp
+  >   [ $r -eq 0 ] && hg tglog
+  >   cd ..
+  >   return $r
+  > }
+
+Rebase two commits, of which one is already in the right place
+
+  $ rebasewithdag -r C+D -d B <<EOF
+  > C
+  > |
+  > B D
+  > |/
+  > A
+  > EOF
+  rebasing 2:b18e25de2cf5 "D" (D)
+  already rebased 3:26805aba1e60 "C" (C tip)
+  o  4: D
+  |
+  | o  3: C
+  |/
+  | x  2: D
+  | |
+  o |  1: B
+  |/
+  o  0: A
+  
+Can collapse commits even if one is already in the right place
+
+  $ rebasewithdag --collapse -r C+D -d B <<EOF
+  > C
+  > |
+  > B D
+  > |/
+  > A
+  > EOF
+  rebasing 2:b18e25de2cf5 "D" (D)
+  rebasing 3:26805aba1e60 "C" (C tip)
+  o  4: Collapsed revision
+  |  * D
+  |  * C
+  | x  3: C
+  |/
+  | x  2: D
+  | |
+  o |  1: B
+  |/
+  o  0: A
+  
+Rebase with "holes". The commits after the hole should end up on the parent of
+the hole (B below), not on top of the destination (A).
+
+  $ rebasewithdag -r B+D -d A <<EOF
+  > D
+  > |
+  > C
+  > |
+  > B
+  > |
+  > A
+  > EOF
+  already rebased 1:112478962961 "B" (B)
+  not rebasing ignored 2:26805aba1e60 "C" (C)
+  rebasing 3:f585351a92f8 "D" (D tip)
+  o  4: D
+  |
+  | x  3: D
+  | |
+  | o  2: C
+  |/
+  o  1: B
+  |
+  o  0: A
+