Patchwork D4664: mergecommit: add a new extension to merge in-memory and create a commit

login
register
mail settings
Submitter phabricator
Date Feb. 26, 2019, 2:53 p.m.
Message ID <17ba4c2198b683dce894be3825b445e7@localhost.localdomain>
Download mbox | patch
Permalink /patch/38941/
State Not Applicable
Headers show

Comments

phabricator - Feb. 26, 2019, 2:53 p.m.
pulkit updated this revision to Diff 14248.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D4664?vs=11198&id=14248

REVISION DETAIL
  https://phab.mercurial-scm.org/D4664

AFFECTED FILES
  hgext/mergecommit.py
  mercurial/filemerge.py
  tests/test-mergecommit.t

CHANGE DETAILS




To: pulkit, #hg-reviewers
Cc: marcink, mercurial-devel

Patch

diff --git a/tests/test-mergecommit.t b/tests/test-mergecommit.t
new file mode 100644
--- /dev/null
+++ b/tests/test-mergecommit.t
@@ -0,0 +1,235 @@ 
+#testcases mergecommit normal-merge
+
+Testing the mergecommit extension. The test demonstrates how this extension can
+be used by hosting providers to handle merging of PRs. It has two testcases,
+'mergecommit' and 'normal-merge' so that the normal-merge workflow can be
+compared with the workflow implemented by mergecommit extension.
+
+The test is using named branches to denote user PRs.
+
+
+  $ cat << EOF >> $HGRCPATH
+  > [alias]
+  > glog = log -GT "{rev}:{node|short} {desc}\n({branch})"
+  > EOF
+
+#if mergecommit
+  $ cat << EOF >> $HGRCPATH
+  > [extensions]
+  > mergecommit =
+  > EOF
+#endif
+
+Initialize a server
+  $ hg init server
+  $ cd server
+  $ for ch in a b c d; do echo foo > $ch; hg ci -Aqm "added "$ch; done;
+  $ hg glog
+  @  3:a44c3a524808 added d
+  |  (default)
+  o  2:8be98ac1a569 added c
+  |  (default)
+  o  1:80e6d2c47cfe added b
+  |  (default)
+  o  0:f7ad41964313 added a
+     (default)
+
+Initialize a client, make some changes and create a PR
+
+  $ cd ..
+  $ hg clone server client1
+  updating to branch default
+  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client1
+  $ hg up 1
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg glog
+  o  3:a44c3a524808 added d
+  |  (default)
+  o  2:8be98ac1a569 added c
+  |  (default)
+  @  1:80e6d2c47cfe added b
+  |  (default)
+  o  0:f7ad41964313 added a
+     (default)
+  $ hg branch pr1
+  marked working directory as branch pr1
+  (branches are permanent and global, did you want a bookmark?)
+  $ echo bar > bar
+  $ hg ci -Aqm "added bar"
+  $ hg push -r . --new-branch
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+
+Lets get back to server and handle the merge
+
+  $ cd ../server
+  $ hg glog
+  o  4:8933c09873b1 added bar
+  |  (pr1)
+  | @  3:a44c3a524808 added d
+  | |  (default)
+  | o  2:8be98ac1a569 added c
+  |/   (default)
+  o  1:80e6d2c47cfe added b
+  |  (default)
+  o  0:f7ad41964313 added a
+     (default)
+
+Merging default into user branch
+
+#if mergecommit
+
+  $ hg mergecommit -r default --dest pr1 --message "merge commit for pr1"
+  new commit formed is d7aed1d69d65
+
+#else
+
+  $ hg up pr1
+  1 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ hg merge default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg ci -m "merge commit for pr1"
+  $ hg up default
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+
+#endif
+
+  $ hg glog
+  o    5:d7aed1d69d65 merge commit for pr1
+  |\   (pr1)
+  | o  4:8933c09873b1 added bar
+  | |  (pr1)
+  @ |  3:a44c3a524808 added d
+  | |  (default)
+  o |  2:8be98ac1a569 added c
+  |/   (default)
+  o  1:80e6d2c47cfe added b
+  |  (default)
+  o  0:f7ad41964313 added a
+     (default)
+
+Merging user branch into default (accepting the PR)
+
+#if mergecommit
+
+  $ hg mergecommit -r 4 --dest default --message "merged pr1 into default"
+  new commit formed is 083ccc50a6c2
+
+#else
+
+  $ hg merge 4
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg ci -m "merged pr1 into default"
+  $ hg up 3
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+
+#endif
+
+  $ hg glog
+  o    6:083ccc50a6c2 merged pr1 into default
+  |\   (default)
+  +---o  5:d7aed1d69d65 merge commit for pr1
+  | |/   (pr1)
+  | o  4:8933c09873b1 added bar
+  | |  (pr1)
+  @ |  3:a44c3a524808 added d
+  | |  (default)
+  o |  2:8be98ac1a569 added c
+  |/   (default)
+  o  1:80e6d2c47cfe added b
+  |  (default)
+  o  0:f7ad41964313 added a
+     (default)
+
+
+Creating a conflicting PR on a new client side
+
+  $ cd ..
+  $ hg clone server client2 -r 3
+  adding changesets
+  adding manifests
+  adding file changes
+  added 4 changesets with 4 changes to 4 files
+  new changesets f7ad41964313:a44c3a524808
+  updating to branch default
+  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd client2
+  $ hg glog
+  @  3:a44c3a524808 added d
+  |  (default)
+  o  2:8be98ac1a569 added c
+  |  (default)
+  o  1:80e6d2c47cfe added b
+  |  (default)
+  o  0:f7ad41964313 added a
+     (default)
+
+  $ hg branch pr2
+  marked working directory as branch pr2
+  (branches are permanent and global, did you want a bookmark?)
+  $ echo foo > bar
+  $ hg ci -Aqm "added foo to bar"
+  $ hg push -r . --new-branch
+  pushing to $TESTTMP/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files (+1 heads)
+
+Go back on server and try to create a review by merging default into the branch
+which will lead to conflicts
+
+  $ cd ../server
+
+#if mergecommit
+
+  $ hg mergecommit -r default --dest pr2
+  merging bar
+  warning: conflicts while merging bar! (edit, then use 'hg resolve --mark')
+  list of unresolved files: bar
+  [1]
+
+#else
+
+  $ hg update pr2
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg merge default
+  merging bar
+  warning: conflicts while merging bar! (edit, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
+  [1]
+  $ hg resolve -l
+  U bar
+  $ hg merge --abort
+  aborting the merge, updating back to 9e82a702b48b
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg update 3
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+#endif
+
+  $ hg glog
+  o  7:9e82a702b48b added foo to bar
+  |  (pr2)
+  | o  6:083ccc50a6c2 merged pr1 into default
+  |/|  (default)
+  +---o  5:d7aed1d69d65 merge commit for pr1
+  | |/   (pr1)
+  | o  4:8933c09873b1 added bar
+  | |  (pr1)
+  @ |  3:a44c3a524808 added d
+  | |  (default)
+  o |  2:8be98ac1a569 added c
+  |/   (default)
+  o  1:80e6d2c47cfe added b
+  |  (default)
+  o  0:f7ad41964313 added a
+     (default)
diff --git a/mercurial/filemerge.py b/mercurial/filemerge.py
--- a/mercurial/filemerge.py
+++ b/mercurial/filemerge.py
@@ -959,7 +959,7 @@ 
 
         if r:
             if onfailure:
-                if wctx.isinmemory():
+                if wctx.isinmemory() and not ui.configbool('merge', 'inmemory'):
                     raise error.InMemoryMergeConflictsError('in-memory merge '
                                                             'does not support '
                                                             'merge conflicts')
diff --git a/hgext/mergecommit.py b/hgext/mergecommit.py
new file mode 100644
--- /dev/null
+++ b/hgext/mergecommit.py
@@ -0,0 +1,108 @@ 
+from mercurial import (
+    context,
+    destutil,
+    error,
+    extensions,
+    hg,
+    merge as mergemod,
+    phases,
+    registrar,
+    scmutil,
+)
+from mercurial.i18n import _
+
+cmdtable = {}
+command = registrar.command(cmdtable)
+
+configtable = {}
+configitem = registrar.configitem(configtable)
+
+configitem('merge', 'inmemory', True)
+
+@command('mergecommit',
+    [('', 'dest', '', _('merge destination')),
+     ('', 'message', '', _('description of the merge commit')),
+     ('', 'date', '', _('date of the merge commit')),
+     ('r', 'rev', '', _('revision to merge')),
+     ('', 'tool', '', _('specify merge tool')),
+    ], ())
+def mergecommit(ui, repo, node=None, **opts):
+    """Merge the rev passed into dest (or working directory parent) and
+    creates a commit with specified options if no conflicts occur.
+
+    If conlicts occur, returns 1 and print the list of unresolved files on
+    stderr. Also, the conflicted state is not applied to the working directory.
+    To do that, you should run `hg merge`
+
+    This does not update to destination node to merge the rev, it uses in-memory
+    merging and also creates in-memory commit. This also does not update to the
+    new commit formed."""
+
+    if opts.get('rev') and node:
+        raise error.Abort(_("please specify just one revision"))
+    if not node:
+        node = opts.get('rev')
+
+    if node:
+        node = scmutil.revsingle(repo, node).node()
+    else:
+        node = repo[destutil.destmerge(repo)].node()
+
+    destnode = None
+    if opts.get('dest'):
+        destnode = scmutil.revsingle(repo, opts.get('dest')).node()
+
+    overrides = {('ui', 'forcemerge'): opts.get('tool', '')}
+    with ui.configoverride(overrides, 'mergecommit'):
+        return merge(repo, node, destnode, date=opts['date'],
+                     message=opts['message'])
+
+def merge(repo, node, destnode, date, message):
+    """merges the node into destnode or parent of working directory and creates
+    a commit if no conflicts occur.
+
+    If conflicts are there, it returns 1 and prints the list of unresolved files
+    on stderr"""
+
+    # create a memwctx to merge
+    wctx = context.overlayworkingctx(repo)
+    # setting destnode as p1 if passed
+    if destnode:
+        currentp1 = repo[destnode]
+    else:
+        currentp1 = repo['.']
+    wctx.setbase(currentp1)
+
+    stats = None
+    try:
+        # actual merging
+        stats = mergemod.update(repo, node, True, None, wc=wctx)
+    except error.InMemoryMergeConflictsError:
+        pass
+
+    if stats is None or stats.unresolvedcount > 0:
+        # there were conflicts
+        ms = mergemod.mergestate.read(repo)
+        unresolved = list(ms.unresolved())
+        repo.ui.warn("list of unresolved files: %s\n" % ', '.join(unresolved))
+        mergemod.mergestate.clean(repo)
+        return 1
+
+    branch = currentp1.branch()
+    desc = message
+    if not desc:
+        desc = "in-memory merge commit"
+    if not date:
+        date = None
+    p1 = currentp1.node()
+    p2 = node
+
+    # creating a memctx and then commiting it
+    memctx = wctx.tomemctx(desc, parents=(p1, p2), branch=branch, date=date)
+    overrides = {('phases', 'new-commit'): phases.secret}
+    with repo.ui.configoverride(overrides, 'memorymerge'):
+        newctx = repo.commitctx(memctx)
+    wctx.clean()
+    mergemod.mergestate.clean(repo)
+    repo.ui.status("new commit formed is %s\n" % repo[newctx].hex()[:12])
+    return 0