update: accept --merge to allow merging across topo branches (issue5125)
authorMartin von Zweigbergk <martinvonz@google.com>
Mon, 13 Feb 2017 12:58:37 -0800
changeset 31166 fad5e299cfc7
parent 31165 81250d377611
child 31167 696e321b304d
update: accept --merge to allow merging across topo branches (issue5125)
mercurial/commands.py
mercurial/hg.py
mercurial/merge.py
tests/test-completion.t
tests/test-update-branches.t
--- a/mercurial/commands.py	Mon Feb 27 15:09:19 2017 -0800
+++ b/mercurial/commands.py	Mon Feb 13 12:58:37 2017 -0800
@@ -5278,12 +5278,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 uncommitted 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
@@ -5302,8 +5303,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
@@ -5315,10 +5316,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
@@ -5343,8 +5348,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)
@@ -5358,7 +5370,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):
--- a/mercurial/hg.py	Mon Feb 27 15:09:19 2017 -0800
+++ b/mercurial/hg.py	Mon Feb 13 12:58:37 2017 -0800
@@ -691,18 +691,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"))
@@ -722,7 +723,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:
@@ -734,10 +735,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
@@ -749,9 +759,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():
--- a/mercurial/merge.py	Mon Feb 27 15:09:19 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
 
@@ -1468,14 +1469,17 @@
 
     This logic is tested by test-update-branches.t.
 
-    -c  -C  dirty  rev  linear  |  result
-     y   y    *     *     *     |    (1)
-     *   *    *     n     n     |     x
-     *   *    n     *     *     |    ok
-     n   n    y     *     y     |   merge
-     n   n    y     y     n     |    (2)
-     n   y    y     *     *     |  discard
-     y   n    y     *     *     |    (3)
+    -c  -C  -m  dirty  rev  linear  |  result
+     y   y   *    *     *     *     |    (1)
+     y   *   y    *     *     *     |    (1)
+     *   y   y    *     *     *     |    (1)
+     *   *   *    *     n     n     |     x
+     *   *   *    n     *     *     |    ok
+     n   n   n    y     *     y     |   merge
+     n   n   n    y     y     n     |    (2)
+     n   n   y    y     *     *     |   merge
+     n   y   n    y     *     *     |  discard
+     y   n   n    y     *     *     |    (3)
 
     x = can't happen
     * = don't-care
@@ -1486,9 +1490,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.
@@ -1545,7 +1556,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
--- a/tests/test-completion.t	Mon Feb 27 15:09:19 2017 -0800
+++ b/tests/test-completion.t	Mon Feb 13 12:58:37 2017 -0800
@@ -228,7 +228,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
--- a/tests/test-update-branches.t	Mon Feb 27 15:09:19 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