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

login
register
mail settings
Submitter phabricator
Date Sept. 19, 2018, 12:52 p.m.
Message ID <differential-rev-PHID-DREV-b42duyoywlcs6g2fcyrs-req@phab.mercurial-scm.org>
Download mbox | patch
Permalink /patch/34831/
State New
Headers show

Comments

phabricator - Sept. 19, 2018, 12:52 p.m.
pulkit created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  This patch adds a new mergecommit command which is very useful for workflows
  where there is automated merging involved. This patch uses existing in-memory
  merge API to merge in-memory and create a commit.
  
  I was thinking about --commit flag to merge command, but then we need other
  commitopts too, so created a new command.
  
  This patches filemerge.py to not raise IMMConflictsError on first conflict
  because we need a list of conflicts in the end.

REPOSITORY
  rHG Mercurial

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

AFFECTED FILES
  hgext/mergecommit.py
  mercurial/filemerge.py

CHANGE DETAILS




To: pulkit, #hg-reviewers
Cc: mercurial-devel
phabricator - Sept. 30, 2018, 9:14 p.m.
pulkit added a comment.


  Thinking about this more, I think I have not done a good job writing commit message. The extension is very useful when you have automation around merges or you do merges because of the following reasons:
  
  - The merge is done in-memory and is very fast if you disable the path conflicts auditing
  - You can specify a merge destination using --dest flag, which means you don't need to update to merge. You can merge two changesets from anywhere
  - You can parallelize multiple merges
  - This can be used as --dry-run for hg merge as it can tell whether a merge will result in conflicts or not without applying anything
  
  That said, I am not sure if this looks like a good candidate for in-core extension. However, we want to share the work we have done on speeding up merge internally so that it can help other companies which uses merges. So, feel free to pick this up from here if you need this. :)

REPOSITORY
  rHG Mercurial

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

To: pulkit, #hg-reviewers
Cc: mercurial-devel
phabricator - Feb. 26, 2019, 3:02 p.m.
pulkit added a comment.


  I don't aim for this to get reviewed and pushed. This is here right now for sharing. We are using this extension internally for months now and has shown good results.

REPOSITORY
  rHG Mercurial

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

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

Patch

diff --git a/mercurial/filemerge.py b/mercurial/filemerge.py
--- a/mercurial/filemerge.py
+++ b/mercurial/filemerge.py
@@ -910,7 +910,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,106 @@ 
+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)
+
+@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