merge.mergestate: add support for persisting a custom merge driver
authorSiddharth Agarwal <sid0@fb.com>
Wed, 30 Sep 2015 21:42:52 -0700
changeset 26649 f618b6aa8cdd
parent 26648 c347d532bb56
child 26650 6ff5534c8afc
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.
mercurial/merge.py
--- a/mercurial/merge.py	Tue Oct 13 12:30:39 2015 -0700
+++ b/mercurial/merge.py	Wed Sep 30 21:42:52 2015 -0700
@@ -61,6 +61,14 @@
     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 @@
         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 @@
         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 @@
                 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 @@
             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)