Patchwork [1,of,4,V4] update: accept --merge to allow merging across topo branches (issue5125)

login
register
mail settings
Submitter via Mercurial-devel
Date Feb. 16, 2017, 4:59 p.m.
Message ID <19f471c814809099b545.1487264352@martinvonz.mtv.corp.google.com>
Download mbox | patch
Permalink /patch/18557/
State Superseded
Headers show

Comments

via Mercurial-devel - Feb. 16, 2017, 4:59 p.m.
# HG changeset patch
# User Martin von Zweigbergk <martinvonz@google.com>
# Date 1487019517 28800
#      Mon Feb 13 12:58:37 2017 -0800
# Node ID 19f471c814809099b5452b1174a2ecb0699cb76a
# Parent  1ee685defe80117cf6aafea1ede6c33c478abceb
update: accept --merge to allow merging across topo branches (issue5125)
Jun Wu - Feb. 21, 2017, 10:17 p.m.
I like the behavior change (didn't check the implementation details
carefully).

Could you also update the table in the docstring of merge.update? I think
that's very helpful to explain the behavior cleanly. Thanks!

Excerpts from Martin von Zweigbergk's message of 2017-02-16 08:59:12 -0800:
> # HG changeset patch
> # User Martin von Zweigbergk <martinvonz@google.com>
> # Date 1487019517 28800
> #      Mon Feb 13 12:58:37 2017 -0800
> # Node ID 19f471c814809099b5452b1174a2ecb0699cb76a
> # Parent  1ee685defe80117cf6aafea1ede6c33c478abceb
> update: accept --merge to allow merging across topo branches (issue5125)
> 
> diff -r 1ee685defe80 -r 19f471c81480 mercurial/commands.py
> --- a/mercurial/commands.py    Wed Feb 15 16:29:58 2017 -0800
> +++ b/mercurial/commands.py    Mon Feb 13 12:58:37 2017 -0800
> @@ -5286,12 +5286,13 @@
>  @command('^update|up|checkout|co',
>      [('C', 'clean', None, _('discard uncommitted changes (no backup)')),
>      ('c', 'check', None, _('require clean working directory')),
> +    ('m', 'merge', None, _('merge local changes')),
>      ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
>      ('r', 'rev', '', _('revision'), _('REV'))
>       ] + mergetoolopts,
> -    _('[-C|-c] [-d DATE] [[-r] REV]'))
> +    _('[-C|-c|-m] [-d DATE] [[-r] REV]'))
>  def update(ui, repo, node=None, rev=None, clean=False, date=None, check=False,
> -           tool=None):
> +           merge=None, tool=None):
>      """update working directory (or switch revisions)
>  
>      Update the repository's working directory to the specified
> @@ -5310,8 +5311,8 @@
>  
>      .. container:: verbose
>  
> -      The -C/--clean and -c/--check options control what happens if the
> -      working directory contains uncommitted changes.
> +      The -C/--clean, -c/--check, and -m/--merge options control what
> +      happens if the working directory contains uncommitted changes.
>        At most of one of them can be specified.
>  
>        1. If no option is specified, and if
> @@ -5323,10 +5324,14 @@
>           branch), the update is aborted and the uncommitted changes
>           are preserved.
>  
> -      2. With the -c/--check option, the update is aborted and the
> +      2. With the -m/--merge option, the update is allowed even if the
> +         requested changeset is not an ancestor or descendant of
> +         the working directory's parent.
> +
> +      3. With the -c/--check option, the update is aborted and the
>           uncommitted changes are preserved.
>  
> -      3. With the -C/--clean option, uncommitted changes are discarded and
> +      4. With the -C/--clean option, uncommitted changes are discarded and
>           the working directory is updated to the requested changeset.
>  
>      To cancel an uncommitted merge (and lose your changes), use
> @@ -5351,8 +5356,15 @@
>      if date and rev is not None:
>          raise error.Abort(_("you can't specify a revision and a date"))
>  
> -    if check and clean:
> -        raise error.Abort(_("cannot specify both -c/--check and -C/--clean"))
> +    if len([x for x in (clean, check, merge) if x]) > 1:
> +        raise error.Abort(_("can only specify one of -C/--clean, -c/--check, "
> +                            "or -m/merge"))
> +
> +    updatecheck = None
> +    if check:
> +        updatecheck = 'abort'
> +    elif merge:
> +        updatecheck = 'none'
>  
>      with repo.wlock():
>          cmdutil.clearunfinished(repo)
> @@ -5366,7 +5378,8 @@
>  
>          repo.ui.setconfig('ui', 'forcemerge', tool, 'update')
>  
> -        return hg.updatetotally(ui, repo, rev, brev, clean=clean, check=check)
> +        return hg.updatetotally(ui, repo, rev, brev, clean=clean,
> +                                updatecheck=updatecheck)
>  
>  @command('verify', [])
>  def verify(ui, repo):
> diff -r 1ee685defe80 -r 19f471c81480 mercurial/hg.py
> --- a/mercurial/hg.py    Wed Feb 15 16:29:58 2017 -0800
> +++ b/mercurial/hg.py    Mon Feb 13 12:58:37 2017 -0800
> @@ -681,18 +681,19 @@
>      repo.ui.status(_("%d files updated, %d files merged, "
>                       "%d files removed, %d files unresolved\n") % stats)
>  
> -def updaterepo(repo, node, overwrite):
> +def updaterepo(repo, node, overwrite, updatecheck=None):
>      """Update the working directory to node.
>  
>      When overwrite is set, changes are clobbered, merged else
>  
>      returns stats (see pydoc mercurial.merge.applyupdates)"""
>      return mergemod.update(repo, node, False, overwrite,
> -                           labels=['working copy', 'destination'])
> +                           labels=['working copy', 'destination'],
> +                           updatecheck=updatecheck)
>  
> -def update(repo, node, quietempty=False):
> -    """update the working directory to node, merging linear changes"""
> -    stats = updaterepo(repo, node, False)
> +def update(repo, node, quietempty=False, updatecheck=None):
> +    """update the working directory to node"""
> +    stats = updaterepo(repo, node, False, updatecheck=updatecheck)
>      _showstats(repo, stats, quietempty)
>      if stats[3]:
>          repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
> @@ -712,7 +713,7 @@
>  # naming conflict in updatetotally()
>  _clean = clean
>  
> -def updatetotally(ui, repo, checkout, brev, clean=False, check=False):
> +def updatetotally(ui, repo, checkout, brev, clean=False, updatecheck=None):
>      """Update the working directory with extra care for non-file components
>  
>      This takes care of non-file components below:
> @@ -724,10 +725,19 @@
>      :checkout: to which revision the working directory is updated
>      :brev: a name, which might be a bookmark to be activated after updating
>      :clean: whether changes in the working directory can be discarded
> -    :check: whether changes in the working directory should be checked
> +    :updatecheck: how to deal with a dirty working directory
> +
> +    Valid values for updatecheck are (None => linear):
> +
> +     * abort: abort if the working directory is dirty
> +     * none: don't check (merge working directory changes into destination)
> +     * linear: check that update is linear before merging working directory
> +               changes into destination
>  
>      This returns whether conflict is detected at updating or not.
>      """
> +    if updatecheck is None:
> +        updatecheck = 'linear'
>      with repo.wlock():
>          movemarkfrom = None
>          warndest = False
> @@ -739,9 +749,10 @@
>          if clean:
>              ret = _clean(repo, checkout)
>          else:
> -            if check:
> +            if updatecheck == 'abort':
>                  cmdutil.bailifchanged(repo, merge=False)
> -            ret = _update(repo, checkout)
> +                updatecheck = 'none'
> +            ret = _update(repo, checkout, updatecheck=updatecheck)
>  
>          if not ret and movemarkfrom:
>              if movemarkfrom == repo['.'].node():
> diff -r 1ee685defe80 -r 19f471c81480 mercurial/merge.py
> --- a/mercurial/merge.py    Wed Feb 15 16:29:58 2017 -0800
> +++ b/mercurial/merge.py    Mon Feb 13 12:58:37 2017 -0800
> @@ -1444,7 +1444,8 @@
>              repo.dirstate.normal(f)
>  
>  def update(repo, node, branchmerge, force, ancestor=None,
> -           mergeancestor=False, labels=None, matcher=None, mergeforce=False):
> +           mergeancestor=False, labels=None, matcher=None, mergeforce=False,
> +           updatecheck=None):
>      """
>      Perform a merge between the working directory and the given node
>  
> @@ -1491,9 +1492,16 @@
>      Return the same tuple as applyupdates().
>      """
>  
> -    # This functon used to find the default destination if node was None, but
> +    # This function used to find the default destination if node was None, but
>      # that's now in destutil.py.
>      assert node is not None
> +    if not branchmerge and not force:
> +        # TODO: remove the default once all callers that pass branchmerge=False
> +        # and force=False pass a value for updatecheck. We may want to allow
> +        # updatecheck='abort' to better suppport some of these callers.
> +        if updatecheck is None:
> +            updatecheck = 'linear'
> +        assert updatecheck in ('none', 'linear')
>      # If we're doing a partial update, we need to skip updating
>      # the dirstate, so make a note of any partial-ness to the
>      # update here.
> @@ -1550,7 +1558,8 @@
>                  repo.hook('update', parent1=xp2, parent2='', error=0)
>                  return 0, 0, 0, 0
>  
> -            if pas not in ([p1], [p2]):  # nonlinear
> +            if (updatecheck == 'linear' and
> +                    pas not in ([p1], [p2])):  # nonlinear
>                  dirty = wc.dirty(missing=True)
>                  if dirty:
>                      # Branching is a bit strange to ensure we do the minimal
> diff -r 1ee685defe80 -r 19f471c81480 tests/test-completion.t
> --- a/tests/test-completion.t    Wed Feb 15 16:29:58 2017 -0800
> +++ b/tests/test-completion.t    Mon Feb 13 12:58:37 2017 -0800
> @@ -223,7 +223,7 @@
>    serve: accesslog, daemon, daemon-postexec, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates, style, ipv6, certificate
>    status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos, template
>    summary: remote
> -  update: clean, check, date, rev, tool
> +  update: clean, check, merge, date, rev, tool
>    addremove: similarity, subrepos, include, exclude, dry-run
>    archive: no-decode, prefix, rev, type, subrepos, include, exclude
>    backout: merge, commit, no-commit, parent, rev, edit, tool, include, exclude, message, logfile, date, user
> diff -r 1ee685defe80 -r 19f471c81480 tests/test-update-branches.t
> --- a/tests/test-update-branches.t    Wed Feb 15 16:29:58 2017 -0800
> +++ b/tests/test-update-branches.t    Mon Feb 13 12:58:37 2017 -0800
> @@ -160,6 +160,16 @@
>    parent=1
>    M foo
>  
> +  $ revtest '-m dirty linear'   dirty 1 2 -m
> +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
> +  parent=2
> +  M foo
> +
> +  $ revtest '-m dirty cross'  dirty 3 4 -m
> +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
> +  parent=4
> +  M foo
> +
>    $ revtest '-c dirtysub linear'   dirtysub 1 2 -c
>    abort: uncommitted changes in subrepository 'sub'
>    parent=1
> @@ -171,7 +181,17 @@
>    parent=2
>  
>    $ revtest '-cC dirty linear'  dirty 1 2 -cC
> -  abort: cannot specify both -c/--check and -C/--clean
> +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
> +  parent=1
> +  M foo
> +
> +  $ revtest '-mc dirty linear'  dirty 1 2 -mc
> +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
> +  parent=1
> +  M foo
> +
> +  $ revtest '-mC dirty linear'  dirty 1 2 -mC
> +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
>    parent=1
>    M foo
>
Martin von Zweigbergk - Feb. 21, 2017, 10:27 p.m.
Sure. I'm on vacation, so I can do that next week, or in a follow-up.
Whichever the person considering queuing prefers.

On Tue, Feb 21, 2017, 14:18 Jun Wu <quark@fb.com> wrote:

> I like the behavior change (didn't check the implementation details
> carefully).
>
> Could you also update the table in the docstring of merge.update? I think
> that's very helpful to explain the behavior cleanly. Thanks!
>
> Excerpts from Martin von Zweigbergk's message of 2017-02-16 08:59:12 -0800:
> > # HG changeset patch
> > # User Martin von Zweigbergk <martinvonz@google.com>
> > # Date 1487019517 28800
> > #      Mon Feb 13 12:58:37 2017 -0800
> > # Node ID 19f471c814809099b5452b1174a2ecb0699cb76a
> > # Parent  1ee685defe80117cf6aafea1ede6c33c478abceb
> > update: accept --merge to allow merging across topo branches (issue5125)
> >
> > diff -r 1ee685defe80 -r 19f471c81480 mercurial/commands.py
> > --- a/mercurial/commands.py    Wed Feb 15 16:29:58 2017 -0800
> > +++ b/mercurial/commands.py    Mon Feb 13 12:58:37 2017 -0800
> > @@ -5286,12 +5286,13 @@
> >  @command('^update|up|checkout|co',
> >      [('C', 'clean', None, _('discard uncommitted changes (no backup)')),
> >      ('c', 'check', None, _('require clean working directory')),
> > +    ('m', 'merge', None, _('merge local changes')),
> >      ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
> >      ('r', 'rev', '', _('revision'), _('REV'))
> >       ] + mergetoolopts,
> > -    _('[-C|-c] [-d DATE] [[-r] REV]'))
> > +    _('[-C|-c|-m] [-d DATE] [[-r] REV]'))
> >  def update(ui, repo, node=None, rev=None, clean=False, date=None,
> check=False,
> > -           tool=None):
> > +           merge=None, tool=None):
> >      """update working directory (or switch revisions)
> >
> >      Update the repository's working directory to the specified
> > @@ -5310,8 +5311,8 @@
> >
> >      .. container:: verbose
> >
> > -      The -C/--clean and -c/--check options control what happens if the
> > -      working directory contains uncommitted changes.
> > +      The -C/--clean, -c/--check, and -m/--merge options control what
> > +      happens if the working directory contains uncommitted changes.
> >        At most of one of them can be specified.
> >
> >        1. If no option is specified, and if
> > @@ -5323,10 +5324,14 @@
> >           branch), the update is aborted and the uncommitted changes
> >           are preserved.
> >
> > -      2. With the -c/--check option, the update is aborted and the
> > +      2. With the -m/--merge option, the update is allowed even if the
> > +         requested changeset is not an ancestor or descendant of
> > +         the working directory's parent.
> > +
> > +      3. With the -c/--check option, the update is aborted and the
> >           uncommitted changes are preserved.
> >
> > -      3. With the -C/--clean option, uncommitted changes are discarded
> and
> > +      4. With the -C/--clean option, uncommitted changes are discarded
> and
> >           the working directory is updated to the requested changeset.
> >
> >      To cancel an uncommitted merge (and lose your changes), use
> > @@ -5351,8 +5356,15 @@
> >      if date and rev is not None:
> >          raise error.Abort(_("you can't specify a revision and a date"))
> >
> > -    if check and clean:
> > -        raise error.Abort(_("cannot specify both -c/--check and
> -C/--clean"))
> > +    if len([x for x in (clean, check, merge) if x]) > 1:
> > +        raise error.Abort(_("can only specify one of -C/--clean,
> -c/--check, "
> > +                            "or -m/merge"))
> > +
> > +    updatecheck = None
> > +    if check:
> > +        updatecheck = 'abort'
> > +    elif merge:
> > +        updatecheck = 'none'
> >
> >      with repo.wlock():
> >          cmdutil.clearunfinished(repo)
> > @@ -5366,7 +5378,8 @@
> >
> >          repo.ui.setconfig('ui', 'forcemerge', tool, 'update')
> >
> > -        return hg.updatetotally(ui, repo, rev, brev, clean=clean,
> check=check)
> > +        return hg.updatetotally(ui, repo, rev, brev, clean=clean,
> > +                                updatecheck=updatecheck)
> >
> >  @command('verify', [])
> >  def verify(ui, repo):
> > diff -r 1ee685defe80 -r 19f471c81480 mercurial/hg.py
> > --- a/mercurial/hg.py    Wed Feb 15 16:29:58 2017 -0800
> > +++ b/mercurial/hg.py    Mon Feb 13 12:58:37 2017 -0800
> > @@ -681,18 +681,19 @@
> >      repo.ui.status(_("%d files updated, %d files merged, "
> >                       "%d files removed, %d files unresolved\n") % stats)
> >
> > -def updaterepo(repo, node, overwrite):
> > +def updaterepo(repo, node, overwrite, updatecheck=None):
> >      """Update the working directory to node.
> >
> >      When overwrite is set, changes are clobbered, merged else
> >
> >      returns stats (see pydoc mercurial.merge.applyupdates)"""
> >      return mergemod.update(repo, node, False, overwrite,
> > -                           labels=['working copy', 'destination'])
> > +                           labels=['working copy', 'destination'],
> > +                           updatecheck=updatecheck)
> >
> > -def update(repo, node, quietempty=False):
> > -    """update the working directory to node, merging linear changes"""
> > -    stats = updaterepo(repo, node, False)
> > +def update(repo, node, quietempty=False, updatecheck=None):
> > +    """update the working directory to node"""
> > +    stats = updaterepo(repo, node, False, updatecheck=updatecheck)
> >      _showstats(repo, stats, quietempty)
> >      if stats[3]:
> >          repo.ui.status(_("use 'hg resolve' to retry unresolved file
> merges\n"))
> > @@ -712,7 +713,7 @@
> >  # naming conflict in updatetotally()
> >  _clean = clean
> >
> > -def updatetotally(ui, repo, checkout, brev, clean=False, check=False):
> > +def updatetotally(ui, repo, checkout, brev, clean=False,
> updatecheck=None):
> >      """Update the working directory with extra care for non-file
> components
> >
> >      This takes care of non-file components below:
> > @@ -724,10 +725,19 @@
> >      :checkout: to which revision the working directory is updated
> >      :brev: a name, which might be a bookmark to be activated after
> updating
> >      :clean: whether changes in the working directory can be discarded
> > -    :check: whether changes in the working directory should be checked
> > +    :updatecheck: how to deal with a dirty working directory
> > +
> > +    Valid values for updatecheck are (None => linear):
> > +
> > +     * abort: abort if the working directory is dirty
> > +     * none: don't check (merge working directory changes into
> destination)
> > +     * linear: check that update is linear before merging working
> directory
> > +               changes into destination
> >
> >      This returns whether conflict is detected at updating or not.
> >      """
> > +    if updatecheck is None:
> > +        updatecheck = 'linear'
> >      with repo.wlock():
> >          movemarkfrom = None
> >          warndest = False
> > @@ -739,9 +749,10 @@
> >          if clean:
> >              ret = _clean(repo, checkout)
> >          else:
> > -            if check:
> > +            if updatecheck == 'abort':
> >                  cmdutil.bailifchanged(repo, merge=False)
> > -            ret = _update(repo, checkout)
> > +                updatecheck = 'none'
> > +            ret = _update(repo, checkout, updatecheck=updatecheck)
> >
> >          if not ret and movemarkfrom:
> >              if movemarkfrom == repo['.'].node():
> > diff -r 1ee685defe80 -r 19f471c81480 mercurial/merge.py
> > --- a/mercurial/merge.py    Wed Feb 15 16:29:58 2017 -0800
> > +++ b/mercurial/merge.py    Mon Feb 13 12:58:37 2017 -0800
> > @@ -1444,7 +1444,8 @@
> >              repo.dirstate.normal(f)
> >
> >  def update(repo, node, branchmerge, force, ancestor=None,
> > -           mergeancestor=False, labels=None, matcher=None,
> mergeforce=False):
> > +           mergeancestor=False, labels=None, matcher=None,
> mergeforce=False,
> > +           updatecheck=None):
> >      """
> >      Perform a merge between the working directory and the given node
> >
> > @@ -1491,9 +1492,16 @@
> >      Return the same tuple as applyupdates().
> >      """
> >
> > -    # This functon used to find the default destination if node was
> None, but
> > +    # This function used to find the default destination if node was
> None, but
> >      # that's now in destutil.py.
> >      assert node is not None
> > +    if not branchmerge and not force:
> > +        # TODO: remove the default once all callers that pass
> branchmerge=False
> > +        # and force=False pass a value for updatecheck. We may want to
> allow
> > +        # updatecheck='abort' to better suppport some of these callers.
> > +        if updatecheck is None:
> > +            updatecheck = 'linear'
> > +        assert updatecheck in ('none', 'linear')
> >      # If we're doing a partial update, we need to skip updating
> >      # the dirstate, so make a note of any partial-ness to the
> >      # update here.
> > @@ -1550,7 +1558,8 @@
> >                  repo.hook('update', parent1=xp2, parent2='', error=0)
> >                  return 0, 0, 0, 0
> >
> > -            if pas not in ([p1], [p2]):  # nonlinear
> > +            if (updatecheck == 'linear' and
> > +                    pas not in ([p1], [p2])):  # nonlinear
> >                  dirty = wc.dirty(missing=True)
> >                  if dirty:
> >                      # Branching is a bit strange to ensure we do the
> minimal
> > diff -r 1ee685defe80 -r 19f471c81480 tests/test-completion.t
> > --- a/tests/test-completion.t    Wed Feb 15 16:29:58 2017 -0800
> > +++ b/tests/test-completion.t    Mon Feb 13 12:58:37 2017 -0800
> > @@ -223,7 +223,7 @@
> >    serve: accesslog, daemon, daemon-postexec, errorlog, port, address,
> prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates,
> style, ipv6, certificate
> >    status: all, modified, added, removed, deleted, clean, unknown,
> ignored, no-status, copies, print0, rev, change, include, exclude,
> subrepos, template
> >    summary: remote
> > -  update: clean, check, date, rev, tool
> > +  update: clean, check, merge, date, rev, tool
> >    addremove: similarity, subrepos, include, exclude, dry-run
> >    archive: no-decode, prefix, rev, type, subrepos, include, exclude
> >    backout: merge, commit, no-commit, parent, rev, edit, tool, include,
> exclude, message, logfile, date, user
> > diff -r 1ee685defe80 -r 19f471c81480 tests/test-update-branches.t
> > --- a/tests/test-update-branches.t    Wed Feb 15 16:29:58 2017 -0800
> > +++ b/tests/test-update-branches.t    Mon Feb 13 12:58:37 2017 -0800
> > @@ -160,6 +160,16 @@
> >    parent=1
> >    M foo
> >
> > +  $ revtest '-m dirty linear'   dirty 1 2 -m
> > +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
> > +  parent=2
> > +  M foo
> > +
> > +  $ revtest '-m dirty cross'  dirty 3 4 -m
> > +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
> > +  parent=4
> > +  M foo
> > +
> >    $ revtest '-c dirtysub linear'   dirtysub 1 2 -c
> >    abort: uncommitted changes in subrepository 'sub'
> >    parent=1
> > @@ -171,7 +181,17 @@
> >    parent=2
> >
> >    $ revtest '-cC dirty linear'  dirty 1 2 -cC
> > -  abort: cannot specify both -c/--check and -C/--clean
> > +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
> > +  parent=1
> > +  M foo
> > +
> > +  $ revtest '-mc dirty linear'  dirty 1 2 -mc
> > +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
> > +  parent=1
> > +  M foo
> > +
> > +  $ revtest '-mC dirty linear'  dirty 1 2 -mC
> > +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
> >    parent=1
> >    M foo
> >
>
via Mercurial-devel - Feb. 27, 2017, 10:29 p.m.
On Tue, Feb 21, 2017 at 2:27 PM, Martin von Zweigbergk
<martinvonz@google.com> wrote:
> Sure. I'm on vacation, so I can do that next week, or in a follow-up.
> Whichever the person considering queuing prefers.

I'm back from vacation. It's unclear if the series was not queued
because of Jun's comment or something else (maybe lack of time), but
I'm working on a V5 now. I hope to send that quite soon. Let me know
if there was another problem that held the series back.

>
>
> On Tue, Feb 21, 2017, 14:18 Jun Wu <quark@fb.com> wrote:
>>
>> I like the behavior change (didn't check the implementation details
>> carefully).
>>
>> Could you also update the table in the docstring of merge.update? I think
>> that's very helpful to explain the behavior cleanly. Thanks!
>>
>> Excerpts from Martin von Zweigbergk's message of 2017-02-16 08:59:12
>> -0800:
>> > # HG changeset patch
>> > # User Martin von Zweigbergk <martinvonz@google.com>
>> > # Date 1487019517 28800
>> > #      Mon Feb 13 12:58:37 2017 -0800
>> > # Node ID 19f471c814809099b5452b1174a2ecb0699cb76a
>> > # Parent  1ee685defe80117cf6aafea1ede6c33c478abceb
>> > update: accept --merge to allow merging across topo branches (issue5125)
>> >
>> > diff -r 1ee685defe80 -r 19f471c81480 mercurial/commands.py
>> > --- a/mercurial/commands.py    Wed Feb 15 16:29:58 2017 -0800
>> > +++ b/mercurial/commands.py    Mon Feb 13 12:58:37 2017 -0800
>> > @@ -5286,12 +5286,13 @@
>> >  @command('^update|up|checkout|co',
>> >      [('C', 'clean', None, _('discard uncommitted changes (no
>> > backup)')),
>> >      ('c', 'check', None, _('require clean working directory')),
>> > +    ('m', 'merge', None, _('merge local changes')),
>> >      ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
>> >      ('r', 'rev', '', _('revision'), _('REV'))
>> >       ] + mergetoolopts,
>> > -    _('[-C|-c] [-d DATE] [[-r] REV]'))
>> > +    _('[-C|-c|-m] [-d DATE] [[-r] REV]'))
>> >  def update(ui, repo, node=None, rev=None, clean=False, date=None,
>> > check=False,
>> > -           tool=None):
>> > +           merge=None, tool=None):
>> >      """update working directory (or switch revisions)
>> >
>> >      Update the repository's working directory to the specified
>> > @@ -5310,8 +5311,8 @@
>> >
>> >      .. container:: verbose
>> >
>> > -      The -C/--clean and -c/--check options control what happens if the
>> > -      working directory contains uncommitted changes.
>> > +      The -C/--clean, -c/--check, and -m/--merge options control what
>> > +      happens if the working directory contains uncommitted changes.
>> >        At most of one of them can be specified.
>> >
>> >        1. If no option is specified, and if
>> > @@ -5323,10 +5324,14 @@
>> >           branch), the update is aborted and the uncommitted changes
>> >           are preserved.
>> >
>> > -      2. With the -c/--check option, the update is aborted and the
>> > +      2. With the -m/--merge option, the update is allowed even if the
>> > +         requested changeset is not an ancestor or descendant of
>> > +         the working directory's parent.
>> > +
>> > +      3. With the -c/--check option, the update is aborted and the
>> >           uncommitted changes are preserved.
>> >
>> > -      3. With the -C/--clean option, uncommitted changes are discarded
>> > and
>> > +      4. With the -C/--clean option, uncommitted changes are discarded
>> > and
>> >           the working directory is updated to the requested changeset.
>> >
>> >      To cancel an uncommitted merge (and lose your changes), use
>> > @@ -5351,8 +5356,15 @@
>> >      if date and rev is not None:
>> >          raise error.Abort(_("you can't specify a revision and a date"))
>> >
>> > -    if check and clean:
>> > -        raise error.Abort(_("cannot specify both -c/--check and
>> > -C/--clean"))
>> > +    if len([x for x in (clean, check, merge) if x]) > 1:
>> > +        raise error.Abort(_("can only specify one of -C/--clean,
>> > -c/--check, "
>> > +                            "or -m/merge"))
>> > +
>> > +    updatecheck = None
>> > +    if check:
>> > +        updatecheck = 'abort'
>> > +    elif merge:
>> > +        updatecheck = 'none'
>> >
>> >      with repo.wlock():
>> >          cmdutil.clearunfinished(repo)
>> > @@ -5366,7 +5378,8 @@
>> >
>> >          repo.ui.setconfig('ui', 'forcemerge', tool, 'update')
>> >
>> > -        return hg.updatetotally(ui, repo, rev, brev, clean=clean,
>> > check=check)
>> > +        return hg.updatetotally(ui, repo, rev, brev, clean=clean,
>> > +                                updatecheck=updatecheck)
>> >
>> >  @command('verify', [])
>> >  def verify(ui, repo):
>> > diff -r 1ee685defe80 -r 19f471c81480 mercurial/hg.py
>> > --- a/mercurial/hg.py    Wed Feb 15 16:29:58 2017 -0800
>> > +++ b/mercurial/hg.py    Mon Feb 13 12:58:37 2017 -0800
>> > @@ -681,18 +681,19 @@
>> >      repo.ui.status(_("%d files updated, %d files merged, "
>> >                       "%d files removed, %d files unresolved\n") %
>> > stats)
>> >
>> > -def updaterepo(repo, node, overwrite):
>> > +def updaterepo(repo, node, overwrite, updatecheck=None):
>> >      """Update the working directory to node.
>> >
>> >      When overwrite is set, changes are clobbered, merged else
>> >
>> >      returns stats (see pydoc mercurial.merge.applyupdates)"""
>> >      return mergemod.update(repo, node, False, overwrite,
>> > -                           labels=['working copy', 'destination'])
>> > +                           labels=['working copy', 'destination'],
>> > +                           updatecheck=updatecheck)
>> >
>> > -def update(repo, node, quietempty=False):
>> > -    """update the working directory to node, merging linear changes"""
>> > -    stats = updaterepo(repo, node, False)
>> > +def update(repo, node, quietempty=False, updatecheck=None):
>> > +    """update the working directory to node"""
>> > +    stats = updaterepo(repo, node, False, updatecheck=updatecheck)
>> >      _showstats(repo, stats, quietempty)
>> >      if stats[3]:
>> >          repo.ui.status(_("use 'hg resolve' to retry unresolved file
>> > merges\n"))
>> > @@ -712,7 +713,7 @@
>> >  # naming conflict in updatetotally()
>> >  _clean = clean
>> >
>> > -def updatetotally(ui, repo, checkout, brev, clean=False, check=False):
>> > +def updatetotally(ui, repo, checkout, brev, clean=False,
>> > updatecheck=None):
>> >      """Update the working directory with extra care for non-file
>> > components
>> >
>> >      This takes care of non-file components below:
>> > @@ -724,10 +725,19 @@
>> >      :checkout: to which revision the working directory is updated
>> >      :brev: a name, which might be a bookmark to be activated after
>> > updating
>> >      :clean: whether changes in the working directory can be discarded
>> > -    :check: whether changes in the working directory should be checked
>> > +    :updatecheck: how to deal with a dirty working directory
>> > +
>> > +    Valid values for updatecheck are (None => linear):
>> > +
>> > +     * abort: abort if the working directory is dirty
>> > +     * none: don't check (merge working directory changes into
>> > destination)
>> > +     * linear: check that update is linear before merging working
>> > directory
>> > +               changes into destination
>> >
>> >      This returns whether conflict is detected at updating or not.
>> >      """
>> > +    if updatecheck is None:
>> > +        updatecheck = 'linear'
>> >      with repo.wlock():
>> >          movemarkfrom = None
>> >          warndest = False
>> > @@ -739,9 +749,10 @@
>> >          if clean:
>> >              ret = _clean(repo, checkout)
>> >          else:
>> > -            if check:
>> > +            if updatecheck == 'abort':
>> >                  cmdutil.bailifchanged(repo, merge=False)
>> > -            ret = _update(repo, checkout)
>> > +                updatecheck = 'none'
>> > +            ret = _update(repo, checkout, updatecheck=updatecheck)
>> >
>> >          if not ret and movemarkfrom:
>> >              if movemarkfrom == repo['.'].node():
>> > diff -r 1ee685defe80 -r 19f471c81480 mercurial/merge.py
>> > --- a/mercurial/merge.py    Wed Feb 15 16:29:58 2017 -0800
>> > +++ b/mercurial/merge.py    Mon Feb 13 12:58:37 2017 -0800
>> > @@ -1444,7 +1444,8 @@
>> >              repo.dirstate.normal(f)
>> >
>> >  def update(repo, node, branchmerge, force, ancestor=None,
>> > -           mergeancestor=False, labels=None, matcher=None,
>> > mergeforce=False):
>> > +           mergeancestor=False, labels=None, matcher=None,
>> > mergeforce=False,
>> > +           updatecheck=None):
>> >      """
>> >      Perform a merge between the working directory and the given node
>> >
>> > @@ -1491,9 +1492,16 @@
>> >      Return the same tuple as applyupdates().
>> >      """
>> >
>> > -    # This functon used to find the default destination if node was
>> > None, but
>> > +    # This function used to find the default destination if node was
>> > None, but
>> >      # that's now in destutil.py.
>> >      assert node is not None
>> > +    if not branchmerge and not force:
>> > +        # TODO: remove the default once all callers that pass
>> > branchmerge=False
>> > +        # and force=False pass a value for updatecheck. We may want to
>> > allow
>> > +        # updatecheck='abort' to better suppport some of these callers.
>> > +        if updatecheck is None:
>> > +            updatecheck = 'linear'
>> > +        assert updatecheck in ('none', 'linear')
>> >      # If we're doing a partial update, we need to skip updating
>> >      # the dirstate, so make a note of any partial-ness to the
>> >      # update here.
>> > @@ -1550,7 +1558,8 @@
>> >                  repo.hook('update', parent1=xp2, parent2='', error=0)
>> >                  return 0, 0, 0, 0
>> >
>> > -            if pas not in ([p1], [p2]):  # nonlinear
>> > +            if (updatecheck == 'linear' and
>> > +                    pas not in ([p1], [p2])):  # nonlinear
>> >                  dirty = wc.dirty(missing=True)
>> >                  if dirty:
>> >                      # Branching is a bit strange to ensure we do the
>> > minimal
>> > diff -r 1ee685defe80 -r 19f471c81480 tests/test-completion.t
>> > --- a/tests/test-completion.t    Wed Feb 15 16:29:58 2017 -0800
>> > +++ b/tests/test-completion.t    Mon Feb 13 12:58:37 2017 -0800
>> > @@ -223,7 +223,7 @@
>> >    serve: accesslog, daemon, daemon-postexec, errorlog, port, address,
>> > prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates,
>> > style, ipv6, certificate
>> >    status: all, modified, added, removed, deleted, clean, unknown,
>> > ignored, no-status, copies, print0, rev, change, include, exclude, subrepos,
>> > template
>> >    summary: remote
>> > -  update: clean, check, date, rev, tool
>> > +  update: clean, check, merge, date, rev, tool
>> >    addremove: similarity, subrepos, include, exclude, dry-run
>> >    archive: no-decode, prefix, rev, type, subrepos, include, exclude
>> >    backout: merge, commit, no-commit, parent, rev, edit, tool, include,
>> > exclude, message, logfile, date, user
>> > diff -r 1ee685defe80 -r 19f471c81480 tests/test-update-branches.t
>> > --- a/tests/test-update-branches.t    Wed Feb 15 16:29:58 2017 -0800
>> > +++ b/tests/test-update-branches.t    Mon Feb 13 12:58:37 2017 -0800
>> > @@ -160,6 +160,16 @@
>> >    parent=1
>> >    M foo
>> >
>> > +  $ revtest '-m dirty linear'   dirty 1 2 -m
>> > +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
>> > +  parent=2
>> > +  M foo
>> > +
>> > +  $ revtest '-m dirty cross'  dirty 3 4 -m
>> > +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
>> > +  parent=4
>> > +  M foo
>> > +
>> >    $ revtest '-c dirtysub linear'   dirtysub 1 2 -c
>> >    abort: uncommitted changes in subrepository 'sub'
>> >    parent=1
>> > @@ -171,7 +181,17 @@
>> >    parent=2
>> >
>> >    $ revtest '-cC dirty linear'  dirty 1 2 -cC
>> > -  abort: cannot specify both -c/--check and -C/--clean
>> > +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
>> > +  parent=1
>> > +  M foo
>> > +
>> > +  $ revtest '-mc dirty linear'  dirty 1 2 -mc
>> > +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
>> > +  parent=1
>> > +  M foo
>> > +
>> > +  $ revtest '-mC dirty linear'  dirty 1 2 -mC
>> > +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
>> >    parent=1
>> >    M foo
>> >
Jun Wu - Feb. 27, 2017, 10:46 p.m.
Excerpts from Martin von Zweigbergk's message of 2017-02-27 14:29:08 -0800:
> On Tue, Feb 21, 2017 at 2:27 PM, Martin von Zweigbergk
> <martinvonz@google.com> wrote:
> > Sure. I'm on vacation, so I can do that next week, or in a follow-up.
> > Whichever the person considering queuing prefers.
> 
> I'm back from vacation. It's unclear if the series was not queued
> because of Jun's comment or something else (maybe lack of time), but
> I'm working on a V5 now. I hope to send that quite soon. Let me know
> if there was another problem that held the series back.

I guess it's lack of manpower...
I'll try to do a more thorough review later.

> 
> >
> >
> > On Tue, Feb 21, 2017, 14:18 Jun Wu <quark@fb.com> wrote:
> >>
> >> I like the behavior change (didn't check the implementation details
> >> carefully).
> >>
> >> Could you also update the table in the docstring of merge.update? I think
> >> that's very helpful to explain the behavior cleanly. Thanks!
> >>
> >> Excerpts from Martin von Zweigbergk's message of 2017-02-16 08:59:12
> >> -0800:
> >> > # HG changeset patch
> >> > # User Martin von Zweigbergk <martinvonz@google.com>
> >> > # Date 1487019517 28800
> >> > #      Mon Feb 13 12:58:37 2017 -0800
> >> > # Node ID 19f471c814809099b5452b1174a2ecb0699cb76a
> >> > # Parent  1ee685defe80117cf6aafea1ede6c33c478abceb
> >> > update: accept --merge to allow merging across topo branches (issue5125)
> >> >
> >> > diff -r 1ee685defe80 -r 19f471c81480 mercurial/commands.py
> >> > --- a/mercurial/commands.py    Wed Feb 15 16:29:58 2017 -0800
> >> > +++ b/mercurial/commands.py    Mon Feb 13 12:58:37 2017 -0800
> >> > @@ -5286,12 +5286,13 @@
> >> >  @command('^update|up|checkout|co',
> >> >      [('C', 'clean', None, _('discard uncommitted changes (no
> >> > backup)')),
> >> >      ('c', 'check', None, _('require clean working directory')),
> >> > +    ('m', 'merge', None, _('merge local changes')),
> >> >      ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
> >> >      ('r', 'rev', '', _('revision'), _('REV'))
> >> >       ] + mergetoolopts,
> >> > -    _('[-C|-c] [-d DATE] [[-r] REV]'))
> >> > +    _('[-C|-c|-m] [-d DATE] [[-r] REV]'))
> >> >  def update(ui, repo, node=None, rev=None, clean=False, date=None,
> >> > check=False,
> >> > -           tool=None):
> >> > +           merge=None, tool=None):
> >> >      """update working directory (or switch revisions)
> >> >
> >> >      Update the repository's working directory to the specified
> >> > @@ -5310,8 +5311,8 @@
> >> >
> >> >      .. container:: verbose
> >> >
> >> > -      The -C/--clean and -c/--check options control what happens if the
> >> > -      working directory contains uncommitted changes.
> >> > +      The -C/--clean, -c/--check, and -m/--merge options control what
> >> > +      happens if the working directory contains uncommitted changes.
> >> >        At most of one of them can be specified.
> >> >
> >> >        1. If no option is specified, and if
> >> > @@ -5323,10 +5324,14 @@
> >> >           branch), the update is aborted and the uncommitted changes
> >> >           are preserved.
> >> >
> >> > -      2. With the -c/--check option, the update is aborted and the
> >> > +      2. With the -m/--merge option, the update is allowed even if the
> >> > +         requested changeset is not an ancestor or descendant of
> >> > +         the working directory's parent.
> >> > +
> >> > +      3. With the -c/--check option, the update is aborted and the
> >> >           uncommitted changes are preserved.
> >> >
> >> > -      3. With the -C/--clean option, uncommitted changes are discarded
> >> > and
> >> > +      4. With the -C/--clean option, uncommitted changes are discarded
> >> > and
> >> >           the working directory is updated to the requested changeset.
> >> >
> >> >      To cancel an uncommitted merge (and lose your changes), use
> >> > @@ -5351,8 +5356,15 @@
> >> >      if date and rev is not None:
> >> >          raise error.Abort(_("you can't specify a revision and a date"))
> >> >
> >> > -    if check and clean:
> >> > -        raise error.Abort(_("cannot specify both -c/--check and
> >> > -C/--clean"))
> >> > +    if len([x for x in (clean, check, merge) if x]) > 1:
> >> > +        raise error.Abort(_("can only specify one of -C/--clean,
> >> > -c/--check, "
> >> > +                            "or -m/merge"))
> >> > +
> >> > +    updatecheck = None
> >> > +    if check:
> >> > +        updatecheck = 'abort'
> >> > +    elif merge:
> >> > +        updatecheck = 'none'
> >> >
> >> >      with repo.wlock():
> >> >          cmdutil.clearunfinished(repo)
> >> > @@ -5366,7 +5378,8 @@
> >> >
> >> >          repo.ui.setconfig('ui', 'forcemerge', tool, 'update')
> >> >
> >> > -        return hg.updatetotally(ui, repo, rev, brev, clean=clean,
> >> > check=check)
> >> > +        return hg.updatetotally(ui, repo, rev, brev, clean=clean,
> >> > +                                updatecheck=updatecheck)
> >> >
> >> >  @command('verify', [])
> >> >  def verify(ui, repo):
> >> > diff -r 1ee685defe80 -r 19f471c81480 mercurial/hg.py
> >> > --- a/mercurial/hg.py    Wed Feb 15 16:29:58 2017 -0800
> >> > +++ b/mercurial/hg.py    Mon Feb 13 12:58:37 2017 -0800
> >> > @@ -681,18 +681,19 @@
> >> >      repo.ui.status(_("%d files updated, %d files merged, "
> >> >                       "%d files removed, %d files unresolved\n") %
> >> > stats)
> >> >
> >> > -def updaterepo(repo, node, overwrite):
> >> > +def updaterepo(repo, node, overwrite, updatecheck=None):
> >> >      """Update the working directory to node.
> >> >
> >> >      When overwrite is set, changes are clobbered, merged else
> >> >
> >> >      returns stats (see pydoc mercurial.merge.applyupdates)"""
> >> >      return mergemod.update(repo, node, False, overwrite,
> >> > -                           labels=['working copy', 'destination'])
> >> > +                           labels=['working copy', 'destination'],
> >> > +                           updatecheck=updatecheck)
> >> >
> >> > -def update(repo, node, quietempty=False):
> >> > -    """update the working directory to node, merging linear changes"""
> >> > -    stats = updaterepo(repo, node, False)
> >> > +def update(repo, node, quietempty=False, updatecheck=None):
> >> > +    """update the working directory to node"""
> >> > +    stats = updaterepo(repo, node, False, updatecheck=updatecheck)
> >> >      _showstats(repo, stats, quietempty)
> >> >      if stats[3]:
> >> >          repo.ui.status(_("use 'hg resolve' to retry unresolved file
> >> > merges\n"))
> >> > @@ -712,7 +713,7 @@
> >> >  # naming conflict in updatetotally()
> >> >  _clean = clean
> >> >
> >> > -def updatetotally(ui, repo, checkout, brev, clean=False, check=False):
> >> > +def updatetotally(ui, repo, checkout, brev, clean=False,
> >> > updatecheck=None):
> >> >      """Update the working directory with extra care for non-file
> >> > components
> >> >
> >> >      This takes care of non-file components below:
> >> > @@ -724,10 +725,19 @@
> >> >      :checkout: to which revision the working directory is updated
> >> >      :brev: a name, which might be a bookmark to be activated after
> >> > updating
> >> >      :clean: whether changes in the working directory can be discarded
> >> > -    :check: whether changes in the working directory should be checked
> >> > +    :updatecheck: how to deal with a dirty working directory
> >> > +
> >> > +    Valid values for updatecheck are (None => linear):
> >> > +
> >> > +     * abort: abort if the working directory is dirty
> >> > +     * none: don't check (merge working directory changes into
> >> > destination)
> >> > +     * linear: check that update is linear before merging working
> >> > directory
> >> > +               changes into destination
> >> >
> >> >      This returns whether conflict is detected at updating or not.
> >> >      """
> >> > +    if updatecheck is None:
> >> > +        updatecheck = 'linear'
> >> >      with repo.wlock():
> >> >          movemarkfrom = None
> >> >          warndest = False
> >> > @@ -739,9 +749,10 @@
> >> >          if clean:
> >> >              ret = _clean(repo, checkout)
> >> >          else:
> >> > -            if check:
> >> > +            if updatecheck == 'abort':
> >> >                  cmdutil.bailifchanged(repo, merge=False)
> >> > -            ret = _update(repo, checkout)
> >> > +                updatecheck = 'none'
> >> > +            ret = _update(repo, checkout, updatecheck=updatecheck)
> >> >
> >> >          if not ret and movemarkfrom:
> >> >              if movemarkfrom == repo['.'].node():
> >> > diff -r 1ee685defe80 -r 19f471c81480 mercurial/merge.py
> >> > --- a/mercurial/merge.py    Wed Feb 15 16:29:58 2017 -0800
> >> > +++ b/mercurial/merge.py    Mon Feb 13 12:58:37 2017 -0800
> >> > @@ -1444,7 +1444,8 @@
> >> >              repo.dirstate.normal(f)
> >> >
> >> >  def update(repo, node, branchmerge, force, ancestor=None,
> >> > -           mergeancestor=False, labels=None, matcher=None,
> >> > mergeforce=False):
> >> > +           mergeancestor=False, labels=None, matcher=None,
> >> > mergeforce=False,
> >> > +           updatecheck=None):
> >> >      """
> >> >      Perform a merge between the working directory and the given node
> >> >
> >> > @@ -1491,9 +1492,16 @@
> >> >      Return the same tuple as applyupdates().
> >> >      """
> >> >
> >> > -    # This functon used to find the default destination if node was
> >> > None, but
> >> > +    # This function used to find the default destination if node was
> >> > None, but
> >> >      # that's now in destutil.py.
> >> >      assert node is not None
> >> > +    if not branchmerge and not force:
> >> > +        # TODO: remove the default once all callers that pass
> >> > branchmerge=False
> >> > +        # and force=False pass a value for updatecheck. We may want to
> >> > allow
> >> > +        # updatecheck='abort' to better suppport some of these callers.
> >> > +        if updatecheck is None:
> >> > +            updatecheck = 'linear'
> >> > +        assert updatecheck in ('none', 'linear')
> >> >      # If we're doing a partial update, we need to skip updating
> >> >      # the dirstate, so make a note of any partial-ness to the
> >> >      # update here.
> >> > @@ -1550,7 +1558,8 @@
> >> >                  repo.hook('update', parent1=xp2, parent2='', error=0)
> >> >                  return 0, 0, 0, 0
> >> >
> >> > -            if pas not in ([p1], [p2]):  # nonlinear
> >> > +            if (updatecheck == 'linear' and
> >> > +                    pas not in ([p1], [p2])):  # nonlinear
> >> >                  dirty = wc.dirty(missing=True)
> >> >                  if dirty:
> >> >                      # Branching is a bit strange to ensure we do the
> >> > minimal
> >> > diff -r 1ee685defe80 -r 19f471c81480 tests/test-completion.t
> >> > --- a/tests/test-completion.t    Wed Feb 15 16:29:58 2017 -0800
> >> > +++ b/tests/test-completion.t    Mon Feb 13 12:58:37 2017 -0800
> >> > @@ -223,7 +223,7 @@
> >> >    serve: accesslog, daemon, daemon-postexec, errorlog, port, address,
> >> > prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates,
> >> > style, ipv6, certificate
> >> >    status: all, modified, added, removed, deleted, clean, unknown,
> >> > ignored, no-status, copies, print0, rev, change, include, exclude, subrepos,
> >> > template
> >> >    summary: remote
> >> > -  update: clean, check, date, rev, tool
> >> > +  update: clean, check, merge, date, rev, tool
> >> >    addremove: similarity, subrepos, include, exclude, dry-run
> >> >    archive: no-decode, prefix, rev, type, subrepos, include, exclude
> >> >    backout: merge, commit, no-commit, parent, rev, edit, tool, include,
> >> > exclude, message, logfile, date, user
> >> > diff -r 1ee685defe80 -r 19f471c81480 tests/test-update-branches.t
> >> > --- a/tests/test-update-branches.t    Wed Feb 15 16:29:58 2017 -0800
> >> > +++ b/tests/test-update-branches.t    Mon Feb 13 12:58:37 2017 -0800
> >> > @@ -160,6 +160,16 @@
> >> >    parent=1
> >> >    M foo
> >> >
> >> > +  $ revtest '-m dirty linear'   dirty 1 2 -m
> >> > +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
> >> > +  parent=2
> >> > +  M foo
> >> > +
> >> > +  $ revtest '-m dirty cross'  dirty 3 4 -m
> >> > +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
> >> > +  parent=4
> >> > +  M foo
> >> > +
> >> >    $ revtest '-c dirtysub linear'   dirtysub 1 2 -c
> >> >    abort: uncommitted changes in subrepository 'sub'
> >> >    parent=1
> >> > @@ -171,7 +181,17 @@
> >> >    parent=2
> >> >
> >> >    $ revtest '-cC dirty linear'  dirty 1 2 -cC
> >> > -  abort: cannot specify both -c/--check and -C/--clean
> >> > +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
> >> > +  parent=1
> >> > +  M foo
> >> > +
> >> > +  $ revtest '-mc dirty linear'  dirty 1 2 -mc
> >> > +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
> >> > +  parent=1
> >> > +  M foo
> >> > +
> >> > +  $ revtest '-mC dirty linear'  dirty 1 2 -mC
> >> > +  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
> >> >    parent=1
> >> >    M foo
> >> >

Patch

diff -r 1ee685defe80 -r 19f471c81480 mercurial/commands.py
--- a/mercurial/commands.py	Wed Feb 15 16:29:58 2017 -0800
+++ b/mercurial/commands.py	Mon Feb 13 12:58:37 2017 -0800
@@ -5286,12 +5286,13 @@ 
 @command('^update|up|checkout|co',
     [('C', 'clean', None, _('discard uncommitted changes (no backup)')),
     ('c', 'check', None, _('require clean working directory')),
+    ('m', 'merge', None, _('merge local changes')),
     ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
     ('r', 'rev', '', _('revision'), _('REV'))
      ] + mergetoolopts,
-    _('[-C|-c] [-d DATE] [[-r] REV]'))
+    _('[-C|-c|-m] [-d DATE] [[-r] REV]'))
 def update(ui, repo, node=None, rev=None, clean=False, date=None, check=False,
-           tool=None):
+           merge=None, tool=None):
     """update working directory (or switch revisions)
 
     Update the repository's working directory to the specified
@@ -5310,8 +5311,8 @@ 
 
     .. container:: verbose
 
-      The -C/--clean and -c/--check options control what happens if the
-      working directory contains uncommitted changes.
+      The -C/--clean, -c/--check, and -m/--merge options control what
+      happens if the working directory contains uncommitted changes.
       At most of one of them can be specified.
 
       1. If no option is specified, and if
@@ -5323,10 +5324,14 @@ 
          branch), the update is aborted and the uncommitted changes
          are preserved.
 
-      2. With the -c/--check option, the update is aborted and the
+      2. With the -m/--merge option, the update is allowed even if the
+         requested changeset is not an ancestor or descendant of
+         the working directory's parent.
+
+      3. With the -c/--check option, the update is aborted and the
          uncommitted changes are preserved.
 
-      3. With the -C/--clean option, uncommitted changes are discarded and
+      4. With the -C/--clean option, uncommitted changes are discarded and
          the working directory is updated to the requested changeset.
 
     To cancel an uncommitted merge (and lose your changes), use
@@ -5351,8 +5356,15 @@ 
     if date and rev is not None:
         raise error.Abort(_("you can't specify a revision and a date"))
 
-    if check and clean:
-        raise error.Abort(_("cannot specify both -c/--check and -C/--clean"))
+    if len([x for x in (clean, check, merge) if x]) > 1:
+        raise error.Abort(_("can only specify one of -C/--clean, -c/--check, "
+                            "or -m/merge"))
+
+    updatecheck = None
+    if check:
+        updatecheck = 'abort'
+    elif merge:
+        updatecheck = 'none'
 
     with repo.wlock():
         cmdutil.clearunfinished(repo)
@@ -5366,7 +5378,8 @@ 
 
         repo.ui.setconfig('ui', 'forcemerge', tool, 'update')
 
-        return hg.updatetotally(ui, repo, rev, brev, clean=clean, check=check)
+        return hg.updatetotally(ui, repo, rev, brev, clean=clean,
+                                updatecheck=updatecheck)
 
 @command('verify', [])
 def verify(ui, repo):
diff -r 1ee685defe80 -r 19f471c81480 mercurial/hg.py
--- a/mercurial/hg.py	Wed Feb 15 16:29:58 2017 -0800
+++ b/mercurial/hg.py	Mon Feb 13 12:58:37 2017 -0800
@@ -681,18 +681,19 @@ 
     repo.ui.status(_("%d files updated, %d files merged, "
                      "%d files removed, %d files unresolved\n") % stats)
 
-def updaterepo(repo, node, overwrite):
+def updaterepo(repo, node, overwrite, updatecheck=None):
     """Update the working directory to node.
 
     When overwrite is set, changes are clobbered, merged else
 
     returns stats (see pydoc mercurial.merge.applyupdates)"""
     return mergemod.update(repo, node, False, overwrite,
-                           labels=['working copy', 'destination'])
+                           labels=['working copy', 'destination'],
+                           updatecheck=updatecheck)
 
-def update(repo, node, quietempty=False):
-    """update the working directory to node, merging linear changes"""
-    stats = updaterepo(repo, node, False)
+def update(repo, node, quietempty=False, updatecheck=None):
+    """update the working directory to node"""
+    stats = updaterepo(repo, node, False, updatecheck=updatecheck)
     _showstats(repo, stats, quietempty)
     if stats[3]:
         repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
@@ -712,7 +713,7 @@ 
 # naming conflict in updatetotally()
 _clean = clean
 
-def updatetotally(ui, repo, checkout, brev, clean=False, check=False):
+def updatetotally(ui, repo, checkout, brev, clean=False, updatecheck=None):
     """Update the working directory with extra care for non-file components
 
     This takes care of non-file components below:
@@ -724,10 +725,19 @@ 
     :checkout: to which revision the working directory is updated
     :brev: a name, which might be a bookmark to be activated after updating
     :clean: whether changes in the working directory can be discarded
-    :check: whether changes in the working directory should be checked
+    :updatecheck: how to deal with a dirty working directory
+
+    Valid values for updatecheck are (None => linear):
+
+     * abort: abort if the working directory is dirty
+     * none: don't check (merge working directory changes into destination)
+     * linear: check that update is linear before merging working directory
+               changes into destination
 
     This returns whether conflict is detected at updating or not.
     """
+    if updatecheck is None:
+        updatecheck = 'linear'
     with repo.wlock():
         movemarkfrom = None
         warndest = False
@@ -739,9 +749,10 @@ 
         if clean:
             ret = _clean(repo, checkout)
         else:
-            if check:
+            if updatecheck == 'abort':
                 cmdutil.bailifchanged(repo, merge=False)
-            ret = _update(repo, checkout)
+                updatecheck = 'none'
+            ret = _update(repo, checkout, updatecheck=updatecheck)
 
         if not ret and movemarkfrom:
             if movemarkfrom == repo['.'].node():
diff -r 1ee685defe80 -r 19f471c81480 mercurial/merge.py
--- a/mercurial/merge.py	Wed Feb 15 16:29:58 2017 -0800
+++ b/mercurial/merge.py	Mon Feb 13 12:58:37 2017 -0800
@@ -1444,7 +1444,8 @@ 
             repo.dirstate.normal(f)
 
 def update(repo, node, branchmerge, force, ancestor=None,
-           mergeancestor=False, labels=None, matcher=None, mergeforce=False):
+           mergeancestor=False, labels=None, matcher=None, mergeforce=False,
+           updatecheck=None):
     """
     Perform a merge between the working directory and the given node
 
@@ -1491,9 +1492,16 @@ 
     Return the same tuple as applyupdates().
     """
 
-    # This functon used to find the default destination if node was None, but
+    # This function used to find the default destination if node was None, but
     # that's now in destutil.py.
     assert node is not None
+    if not branchmerge and not force:
+        # TODO: remove the default once all callers that pass branchmerge=False
+        # and force=False pass a value for updatecheck. We may want to allow
+        # updatecheck='abort' to better suppport some of these callers.
+        if updatecheck is None:
+            updatecheck = 'linear'
+        assert updatecheck in ('none', 'linear')
     # If we're doing a partial update, we need to skip updating
     # the dirstate, so make a note of any partial-ness to the
     # update here.
@@ -1550,7 +1558,8 @@ 
                 repo.hook('update', parent1=xp2, parent2='', error=0)
                 return 0, 0, 0, 0
 
-            if pas not in ([p1], [p2]):  # nonlinear
+            if (updatecheck == 'linear' and
+                    pas not in ([p1], [p2])):  # nonlinear
                 dirty = wc.dirty(missing=True)
                 if dirty:
                     # Branching is a bit strange to ensure we do the minimal
diff -r 1ee685defe80 -r 19f471c81480 tests/test-completion.t
--- a/tests/test-completion.t	Wed Feb 15 16:29:58 2017 -0800
+++ b/tests/test-completion.t	Mon Feb 13 12:58:37 2017 -0800
@@ -223,7 +223,7 @@ 
   serve: accesslog, daemon, daemon-postexec, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates, style, ipv6, certificate
   status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos, template
   summary: remote
-  update: clean, check, date, rev, tool
+  update: clean, check, merge, date, rev, tool
   addremove: similarity, subrepos, include, exclude, dry-run
   archive: no-decode, prefix, rev, type, subrepos, include, exclude
   backout: merge, commit, no-commit, parent, rev, edit, tool, include, exclude, message, logfile, date, user
diff -r 1ee685defe80 -r 19f471c81480 tests/test-update-branches.t
--- a/tests/test-update-branches.t	Wed Feb 15 16:29:58 2017 -0800
+++ b/tests/test-update-branches.t	Mon Feb 13 12:58:37 2017 -0800
@@ -160,6 +160,16 @@ 
   parent=1
   M foo
 
+  $ revtest '-m dirty linear'   dirty 1 2 -m
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  parent=2
+  M foo
+
+  $ revtest '-m dirty cross'  dirty 3 4 -m
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  parent=4
+  M foo
+
   $ revtest '-c dirtysub linear'   dirtysub 1 2 -c
   abort: uncommitted changes in subrepository 'sub'
   parent=1
@@ -171,7 +181,17 @@ 
   parent=2
 
   $ revtest '-cC dirty linear'  dirty 1 2 -cC
-  abort: cannot specify both -c/--check and -C/--clean
+  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
+  parent=1
+  M foo
+
+  $ revtest '-mc dirty linear'  dirty 1 2 -mc
+  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
+  parent=1
+  M foo
+
+  $ revtest '-mC dirty linear'  dirty 1 2 -mC
+  abort: can only specify one of -C/--clean, -c/--check, or -m/merge
   parent=1
   M foo