Patchwork [1,of,5,mergedriver,V2] merge.mergestate: add support for persisting a custom merge driver

login
register
mail settings
Submitter Siddharth Agarwal
Date Oct. 13, 2015, 10:10 p.m.
Message ID <d5137bc7dc43dfbe0e69.1444774214@dev6666.prn1.facebook.com>
Download mbox | patch
Permalink /patch/11040/
State Accepted
Headers show

Comments

Siddharth Agarwal - Oct. 13, 2015, 10:10 p.m.
# HG changeset patch
# User Siddharth Agarwal <sid0@fb.com>
# Date 1443674572 25200
#      Wed Sep 30 21:42:52 2015 -0700
# Node ID d5137bc7dc43dfbe0e69a115ea1fd548fb6f7e01
# Parent  36383507a6f8adb39df38fd20ca3bf5bc9d8ac25
merge.mergestate: add support for persisting a custom merge driver

A 'merge driver' is a coordinator for the overall merge process. It will be
able to control:

- tools for individual files, much like the merge-patterns configuration does
  today
- tools that can work across groups of files
- the ordering of file resolution
- resolution of automatically generated files
- adding and removing additional files to and from the dirstate

Since it is a critical part of the merge process, it really is part of the
merge state.

This is a lowercase character (i.e. optional) because ignoring this is fine for
older versions of Mercurial -- however, if there are any files that are
specially treated by the driver, we should abort. That will happen in upcoming
patches.

There is a potential security issue with storing the merge driver in the merge
state. See the inline comments for more details.

Patch

diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -61,6 +61,14 @@  class mergestate(object):
     L: the node of the "local" part of the merge (hexified version)
     O: the node of the "other" part of the merge (hexified version)
     F: a file to be merged entry
+    m: the external merge driver defined for this merge plus its run state
+       (experimental)
+
+    Merge driver run states (experimental):
+    u: driver-resolved files unmarked -- needs to be run next time we're about
+       to resolve or commit
+    m: driver-resolved files marked -- only needs to be run before commit
+    s: success/skipped -- does not need to be run any more
     '''
     statepathv1 = 'merge/state'
     statepathv2 = 'merge/state2'
@@ -77,6 +85,7 @@  class mergestate(object):
         if node:
             self._local = node
             self._other = other
+        self._mdstate = 'u'
         shutil.rmtree(self._repo.join('merge'), True)
         self._dirty = False
 
@@ -89,12 +98,33 @@  class mergestate(object):
         self._state = {}
         self._local = None
         self._other = None
+        self._mdstate = 'u'
         records = self._readrecords()
         for rtype, record in records:
             if rtype == 'L':
                 self._local = bin(record)
             elif rtype == 'O':
                 self._other = bin(record)
+            elif rtype == 'm':
+                bits = record.split('\0', 1)
+                mdstate = bits[1]
+                if len(mdstate) != 1 or mdstate not in 'ums':
+                    # the merge driver should be idempotent, so just rerun it
+                    mdstate = 'u'
+
+                # protect against the following:
+                # - A configures a malicious merge driver in their hgrc, then
+                #   pauses the merge
+                # - A edits their hgrc to remove references to the merge driver
+                # - A gives a copy of their entire repo, including .hg, to B
+                # - B inspects .hgrc and finds it to be clean
+                # - B then continues the merge and the malicious merge driver
+                #  gets invoked
+                if self.mergedriver != bits[0]:
+                    raise error.ConfigError(
+                        _("merge driver changed since merge started"),
+                        hint=_("revert merge driver change or abort merge"))
+                self._mdstate = mdstate
             elif rtype == 'F':
                 bits = record.split('\0')
                 self._state[bits[0]] = bits[1:]
@@ -198,6 +228,10 @@  class mergestate(object):
                 raise
         return records
 
+    @util.propertycache
+    def mergedriver(self):
+        return self._repo.ui.config('experimental', 'mergedriver')
+
     def active(self):
         """Whether mergestate is active.
 
@@ -216,6 +250,9 @@  class mergestate(object):
             records = []
             records.append(('L', hex(self._local)))
             records.append(('O', hex(self._other)))
+            if self.mergedriver:
+                records.append(('m', '\0'.join([
+                    self.mergedriver, self._mdstate])))
             for d, v in self._state.iteritems():
                 records.append(('F', '\0'.join([d] + v)))
             self._writerecords(records)