changeset 28139:5476a7a039c0

destutil: allow to specify an explicit source for the merge We can now specify from where the merge is performed. The experimental revset is updated to take revisions as argument, allowing to test the feature. This will become very useful for pick the 'rebase' default destination. For this reason, we also exclude all descendants from the rebased set from the candidate destinations. This descendants exclusion was not necessary for merge as default destination would not be picked from anything else than a head. I'm not super excited with the current error messages, but I would prefer to delay an overall messages rework once 'hg rebase' is done getting a default destination aligned with 'hg merge'.
author Pierre-Yves David <pierre-yves.david@fb.com>
date Mon, 08 Feb 2016 19:32:29 +0100
parents 5ad2017454ee
children 276644ae9e8d
files mercurial/destutil.py mercurial/revset.py tests/test-merge-default.t
diffstat 3 files changed, 72 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/destutil.py	Mon Feb 08 18:12:06 2016 +0100
+++ b/mercurial/destutil.py	Mon Feb 08 19:32:29 2016 +0100
@@ -185,9 +185,19 @@
             (_('working directory not at a head revision'),
              _("use 'hg update' or merge with an explicit revision"))
         },
+    'emptysourceset':
+        {'merge':
+            (_('source set is empty'),
+             None)
+        },
+    'multiplebranchessourceset':
+        {'merge':
+            (_('source set is rooted in multiple branches'),
+             None)
+        },
     }
 
-def _destmergebook(repo, action='merge'):
+def _destmergebook(repo, action='merge', sourceset=None):
     """find merge destination in the active bookmark case"""
     node = None
     bmheads = repo.bookmarkheads(repo._activebookmark)
@@ -206,15 +216,27 @@
     assert node is not None
     return node
 
-def _destmergebranch(repo, action='merge'):
+def _destmergebranch(repo, action='merge', sourceset=None):
     """find merge destination based on branch heads"""
     node = None
-    parent = repo.dirstate.p1()
-    branch = repo.dirstate.branch()
+
+    if sourceset is None:
+        sourceset = [repo[repo.dirstate.p1()].rev()]
+        branch = repo.dirstate.branch()
+    elif not sourceset:
+        msg, hint = msgdestmerge['emptysourceset'][action]
+        raise error.Abort(msg, hint=hint)
+    else:
+        branch = None
+        for ctx in repo.set('roots(%ld::%ld)', sourceset, sourceset):
+            if branch is not None and ctx.branch() != branch:
+                msg, hint = msgdestmerge['multiplebranchessourceset'][action]
+                raise error.Abort(msg, hint=hint)
+            branch = ctx.branch()
+
     bheads = repo.branchheads(branch)
-
-    if parent not in bheads:
-        # Case A: working copy if not on a head.
+    if not repo.revs('%ld and %ln', sourceset, bheads):
+        # Case A: working copy if not on a head. (merge only)
         #
         # This is probably a user mistake We bailout pointing at 'hg update'
         if len(repo.heads()) <= 1:
@@ -222,10 +244,10 @@
         else:
             msg, hint = msgdestmerge['notatheads'][action]
         raise error.Abort(msg, hint=hint)
-    # remove current head from the set
-    bheads = [bh for bh in bheads if bh != parent]
+    # remove heads descendants of source from the set
+    bheads = list(repo.revs('%ln - (%ld::)', bheads, sourceset))
     # filters out bookmarked heads
-    nbhs = [bh for bh in bheads if not repo[bh].bookmarks()]
+    nbhs = list(repo.revs('%ld - bookmark()', bheads))
     if len(nbhs) > 1:
         # Case B: There is more than 1 other anonymous heads
         #
@@ -253,7 +275,7 @@
     assert node is not None
     return node
 
-def destmerge(repo, action='merge'):
+def destmerge(repo, action='merge', sourceset=None):
     """return the default destination for a merge
 
     (or raise exception about why it can't pick one)
@@ -261,9 +283,9 @@
     :action: the action being performed, controls emitted error message
     """
     if repo._activebookmark:
-        node = _destmergebook(repo, action=action)
+        node = _destmergebook(repo, action=action, sourceset=sourceset)
     else:
-        node = _destmergebranch(repo, action=action)
+        node = _destmergebranch(repo, action=action, sourceset=sourceset)
     return repo[node].rev()
 
 histeditdefaultrevset = 'reverse(only(.) and not public() and not ::merge())'
--- a/mercurial/revset.py	Mon Feb 08 18:12:06 2016 +0100
+++ b/mercurial/revset.py	Mon Feb 08 19:32:29 2016 +0100
@@ -541,8 +541,10 @@
 @predicate('_destmerge')
 def _destmerge(repo, subset, x):
     # experimental revset for merge destination
-    getargs(x, 0, 0, _("_mergedefaultdest takes no arguments"))
-    return subset & baseset([destutil.destmerge(repo)])
+    sourceset = None
+    if x is not None:
+        sourceset = getset(repo, fullreposet(repo), x)
+    return subset & baseset([destutil.destmerge(repo, sourceset=sourceset)])
 
 @predicate('adds(pattern)', safe=True)
 def adds(repo, subset, x):
--- a/tests/test-merge-default.t	Mon Feb 08 18:12:06 2016 +0100
+++ b/tests/test-merge-default.t	Mon Feb 08 19:32:29 2016 +0100
@@ -116,3 +116,36 @@
   (run 'hg heads' to see all heads)
   [255]
 
+(on a branch with a two heads)
+
+  $ hg up 5
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo f >> a
+  $ hg commit -mf
+  created new head
+  $ hg log -r '_destmerge()'
+  changeset:   6:e88e33f3bf62
+  parent:      5:a431fabd6039
+  parent:      3:ea9ff125ff88
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     m2
+  
+
+(from the other head)
+
+  $ hg log -r '_destmerge(e88e33f3bf62)'
+  changeset:   8:b613918999e2
+  tag:         tip
+  parent:      5:a431fabd6039
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     f
+  
+
+(from unrelated branch)
+
+  $ hg log -r '_destmerge(foobranch)'
+  abort: branch 'foobranch' has one head - please merge with an explicit rev
+  (run 'hg heads' to see all heads)
+  [255]