# HG changeset patch # User Siddharth Agarwal # Date 1443674572 25200 # Node ID f618b6aa8cdde436e9b4cf23299bcd688683433c # Parent c347d532bb566b5af961800850563d1f6d35ed86 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. diff -r c347d532bb56 -r f618b6aa8cdd 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)