Patchwork [2,of,4,V2] destutil: allow to specify an explicit source for the merge

login
register
mail settings
Submitter Pierre-Yves David
Date Feb. 14, 2016, 3:38 p.m.
Message ID <6b38d0c01aeca0d3c2a2.1455464294@marginatus.alto.octopoid.net>
Download mbox | patch
Permalink /patch/13182/
State Accepted
Headers show

Comments

Pierre-Yves David - Feb. 14, 2016, 3:38 p.m.
# HG changeset patch
# User Pierre-Yves David <pierre-yves.david@fb.com>
# Date 1454956349 -3600
#      Mon Feb 08 19:32:29 2016 +0100
# Node ID 6b38d0c01aeca0d3c2a2bbf3a0eadd04e37803e8
# Parent  0ffdd65816fcde6f970e60830fad8aa0bc67b970
# EXP-Topic destination
# Available At http://hg.netv6.net/marmoute-wip/mercurial/
#              hg pull http://hg.netv6.net/marmoute-wip/mercurial/ -r 6b38d0c01aec
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'.
Martin von Zweigbergk - Feb. 14, 2016, 4:37 p.m.
On Sun, Feb 14, 2016, 07:38 Pierre-Yves David <
pierre-yves.david@ens-lyon.org> wrote:

> # HG changeset patch
> # User Pierre-Yves David <pierre-yves.david@fb.com>
> # Date 1454956349 -3600
> #      Mon Feb 08 19:32:29 2016 +0100
> # Node ID 6b38d0c01aeca0d3c2a2bbf3a0eadd04e37803e8
> # Parent  0ffdd65816fcde6f970e60830fad8aa0bc67b970
> # EXP-Topic destination
> # Available At http://hg.netv6.net/marmoute-wip/mercurial/
> #              hg pull http://hg.netv6.net/marmoute-wip/mercurial/ -r
> 6b38d0c01aec
> 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'.
>
> diff --git a/mercurial/destutil.py b/mercurial/destutil.py
> --- a/mercurial/destutil.py
> +++ b/mercurial/destutil.py
> @@ -183,13 +183,23 @@ msgdestmerge = {
>      'notatheads':
>          {'merge':
>              (_('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)
>      curhead = repo[repo._activebookmark].node()
>      if len(bmheads) == 2:
> @@ -204,30 +214,42 @@ def _destmergebook(repo, action='merge')
>          msg, hint = msgdestmerge['nootherbookmarks'][action]
>          raise error.Abort(msg, hint=hint)
>      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):
>

Can this ever be different from just
repo.set('roots(%ld)', sourceset)? Typo? If it really is different, perhaps
it deserves a comment about what it does?


+            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:
>              msg, hint = msgdestmerge['nootherheadsbehind'][action]
>          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]
> +    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
>          #
>          # This means that there will be more than 1 candidate. This is
>          # ambiguous. We abort asking the user to pick as explicit
> destination
> @@ -251,21 +273,21 @@ def _destmergebranch(repo, action='merge
>      else:
>          node = nbhs[0]
>      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)
>
>      :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())'
>
>  def desthistedit(ui, repo):
> diff --git a/mercurial/revset.py b/mercurial/revset.py
> --- a/mercurial/revset.py
> +++ b/mercurial/revset.py
> @@ -539,12 +539,14 @@ def _destupdate(repo, subset, x):
>      return subset & baseset([destutil.destupdate(repo, **args)[0]])
>
>  @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):
>      """Changesets that add a file matching pattern.
>
> diff --git a/tests/test-merge-default.t b/tests/test-merge-default.t
> --- a/tests/test-merge-default.t
> +++ b/tests/test-merge-default.t
> @@ -114,5 +114,38 @@ Test experimental destination revset
>    $ hg log -r '_destmerge()'
>    abort: branch 'foobranch' has one head - please merge with an explicit
> rev
>    (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]
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>
Pierre-Yves David - Feb. 14, 2016, 4:40 p.m.
On 02/14/2016 04:37 PM, Martin von Zweigbergk wrote:
>
>
> On Sun, Feb 14, 2016, 07:38 Pierre-Yves David
> <pierre-yves.david@ens-lyon.org <mailto:pierre-yves.david@ens-lyon.org>>
> wrote:
>
>     # HG changeset patch
>     # User Pierre-Yves David <pierre-yves.david@fb.com
>     <mailto:pierre-yves.david@fb.com>>
>     # Date 1454956349 -3600
>     #      Mon Feb 08 19:32:29 2016 +0100
>     # Node ID 6b38d0c01aeca0d3c2a2bbf3a0eadd04e37803e8
>     # Parent  0ffdd65816fcde6f970e60830fad8aa0bc67b970
>     # EXP-Topic destination
>     # Available At http://hg.netv6.net/marmoute-wip/mercurial/
>     #              hg pull http://hg.netv6.net/marmoute-wip/mercurial/
>     -r 6b38d0c01aec
>     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'.
>
>     diff --git a/mercurial/destutil.py b/mercurial/destutil.py
>     --- a/mercurial/destutil.py
>     +++ b/mercurial/destutil.py
>     @@ -183,13 +183,23 @@ msgdestmerge = {
>           'notatheads':
>               {'merge':
>                   (_('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)
>           curhead = repo[repo._activebookmark].node()
>           if len(bmheads) == 2:
>     @@ -204,30 +214,42 @@ def _destmergebook(repo, action='merge')
>               msg, hint = msgdestmerge['nootherbookmarks'][action]
>               raise error.Abort(msg, hint=hint)
>           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):
>
>
> Can this ever be different from just
> repo.set('roots(%ld)', sourceset)? Typo? If it really is different,
> perhaps it deserves a comment about what it does?

If the sourceset is non-continuous (some hole in an ancestry line) roots 
would not return the roots of the planned rebase.

Mostly being safe as the function "contract" does not requires it.
Martin von Zweigbergk - Feb. 15, 2016, 5:17 a.m.
On Sun, Feb 14, 2016 at 7:38 AM, Pierre-Yves David
<pierre-yves.david@ens-lyon.org> wrote:
> # HG changeset patch
> # User Pierre-Yves David <pierre-yves.david@fb.com>
> # Date 1454956349 -3600
> #      Mon Feb 08 19:32:29 2016 +0100
> # Node ID 6b38d0c01aeca0d3c2a2bbf3a0eadd04e37803e8
> # Parent  0ffdd65816fcde6f970e60830fad8aa0bc67b970
> # EXP-Topic destination
> # Available At http://hg.netv6.net/marmoute-wip/mercurial/
> #              hg pull http://hg.netv6.net/marmoute-wip/mercurial/ -r 6b38d0c01aec
> 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'.
>
> diff --git a/mercurial/destutil.py b/mercurial/destutil.py
> --- a/mercurial/destutil.py
> +++ b/mercurial/destutil.py
> @@ -183,13 +183,23 @@ msgdestmerge = {
>      'notatheads':
>          {'merge':
>              (_('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)
>      curhead = repo[repo._activebookmark].node()
>      if len(bmheads) == 2:
> @@ -204,30 +214,42 @@ def _destmergebook(repo, action='merge')
>          msg, hint = msgdestmerge['nootherbookmarks'][action]
>          raise error.Abort(msg, hint=hint)
>      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()

I was missing tests for these. Hopefully they will be tested later
when you make the rebase code call this method.

> +
>      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:
>              msg, hint = msgdestmerge['nootherheadsbehind'][action]
>          else:
>              msg, hint = msgdestmerge['notatheads'][action]
>          raise error.Abort(msg, hint=hint)
>      # remove current head from the set

This comment is longer quite accurate, but I'll leave it to you to fix
that in a followup if you care enough. Also, there are two "Case B".

> -    bheads = [bh for bh in bheads if bh != parent]
> +    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
>          #
>          # This means that there will be more than 1 candidate. This is
>          # ambiguous. We abort asking the user to pick as explicit destination
> @@ -251,21 +273,21 @@ def _destmergebranch(repo, action='merge
>      else:
>          node = nbhs[0]
>      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)
>
>      :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())'
>
>  def desthistedit(ui, repo):
> diff --git a/mercurial/revset.py b/mercurial/revset.py
> --- a/mercurial/revset.py
> +++ b/mercurial/revset.py
> @@ -539,12 +539,14 @@ def _destupdate(repo, subset, x):
>      return subset & baseset([destutil.destupdate(repo, **args)[0]])
>
>  @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):
>      """Changesets that add a file matching pattern.
>
> diff --git a/tests/test-merge-default.t b/tests/test-merge-default.t
> --- a/tests/test-merge-default.t
> +++ b/tests/test-merge-default.t
> @@ -114,5 +114,38 @@ Test experimental destination revset
>    $ hg log -r '_destmerge()'
>    abort: branch 'foobranch' has one head - please merge with an explicit rev
>    (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]
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel

Patch

diff --git a/mercurial/destutil.py b/mercurial/destutil.py
--- a/mercurial/destutil.py
+++ b/mercurial/destutil.py
@@ -183,13 +183,23 @@  msgdestmerge = {
     'notatheads':
         {'merge':
             (_('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)
     curhead = repo[repo._activebookmark].node()
     if len(bmheads) == 2:
@@ -204,30 +214,42 @@  def _destmergebook(repo, action='merge')
         msg, hint = msgdestmerge['nootherbookmarks'][action]
         raise error.Abort(msg, hint=hint)
     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:
             msg, hint = msgdestmerge['nootherheadsbehind'][action]
         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]
+    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
         #
         # This means that there will be more than 1 candidate. This is
         # ambiguous. We abort asking the user to pick as explicit destination
@@ -251,21 +273,21 @@  def _destmergebranch(repo, action='merge
     else:
         node = nbhs[0]
     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)
 
     :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())'
 
 def desthistedit(ui, repo):
diff --git a/mercurial/revset.py b/mercurial/revset.py
--- a/mercurial/revset.py
+++ b/mercurial/revset.py
@@ -539,12 +539,14 @@  def _destupdate(repo, subset, x):
     return subset & baseset([destutil.destupdate(repo, **args)[0]])
 
 @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):
     """Changesets that add a file matching pattern.
 
diff --git a/tests/test-merge-default.t b/tests/test-merge-default.t
--- a/tests/test-merge-default.t
+++ b/tests/test-merge-default.t
@@ -114,5 +114,38 @@  Test experimental destination revset
   $ hg log -r '_destmerge()'
   abort: branch 'foobranch' has one head - please merge with an explicit rev
   (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]