Automatic Merge Conflict Resolving for git
This post describes how merge conflicts can be automatically resolved using maven POM files as an example. The example related to the dependencies section of Maven POM files depicted below:
The highlighting indicates that dependency order is different. This reflects what IDEs do automatically and also interactive user manipulation. When feature branches are created dependencies may need to be updated or increased; they aren’t typically reversed or reverted to a lower or earlier version.
Lets look at what happens if we use git’s built-in diff3 based merge:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>groupId</groupId>
<artifactId>git-merge-driver</artifactId>
<version>7.0.4</version>
<dependencies>
<<<<<<< HEAD
<dependency>
<groupId>com.deltaxml.merge</groupId>
<artifactId>merge</artifactId>
<version>7.1.0</version>
</dependency>
=======
>>>>>>> v2
<dependency>
<groupId>com.deltaxml.merge</groupId>
<artifactId>flexlm</artifactId>
<version>7.1.0</version>
</dependency>
<dependency>
<groupId>com.deltaxml.merge</groupId>
<<<<<<< HEAD
<artifactId>deltaxml</artifactId>
<version>10.3.0</version>
=======
<artifactId>flexlm</artifactId>
<version>7.0.6</version>
</dependency>
<dependency>
<groupId>com.deltaxml.merge</groupId>
<artifactId>merge</artifactId>
<version>7.0.6</version>
>>>>>>> v2
</dependency>
</dependencies>
</project>
Here’s my first attempt to use our XML Merge with rule processing:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:deltaxml="https://www.deltaxml.com/ns/well-formed-delta-v1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" deltaxml:version-order="ancestor, edit1, edit2" deltaxml:content-type="merge-concurrent" deltaxml:version="2.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" deltaxml:deltaV2="ancestor=edit1!=edit2">
<modelVersion deltaxml:deltaV2="ancestor=edit1=edit2">4.0.0</modelVersion>
<groupId deltaxml:deltaV2="ancestor=edit1=edit2">groupId</groupId>
<artifactId deltaxml:deltaV2="ancestor=edit1=edit2">git-merge-driver</artifactId>
<version deltaxml:deltaV2="ancestor=edit1=edit2">7.0.4</version>
<dependencies deltaxml:deltaV2="ancestor=edit1!=edit2">
<dependency deltaxml:edit-type="delete" deltaxml:deltaV2="ancestor=edit1">
<groupId>com.deltaxml.merge</groupId>
<artifactId>merge</artifactId>
<version>7.1.0</version>
</dependency>
<dependency deltaxml:deltaV2="ancestor=edit1=edit2">
<groupId>com.deltaxml.merge</groupId>
<artifactId>flexlm</artifactId>
<version>7.1.0</version>
</dependency>
<dependency deltaxml:deltaV2="ancestor=edit1=edit2">
<groupId>com.deltaxml.merge</groupId>
<artifactId>deltaxml</artifactId>
<version>10.3.0</version>
<version>7.0.6</version>
</dependency>
<dependency deltaxml:deltaV2="ancestor=edit1=edit2">
<groupId>com.deltaxml.merge</groupId>
<artifactId>merge</artifactId>
<version>7.0.6</version>
</dependency>
</dependencies>
</project>
dev/git-merge
It’s easier to see the dependencies, but why are there two version children in one of them? I delved a little deeper… The above result was generated using our command line driver for concurrent3 using the default parameter settings. An important one here was element splitting. What was happening was there was a textGroup containing the version numbers (‘10.3.0’ and ‘7.0.6’) and that was the only content of the element. As there was a high proportion of change it was then split. The splitting resulted into logically two additions which were then rule processed and added.
Lets look at this without splitting, word-by-word or rule processing applied:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:deltaxml="https://www.deltaxml.com/ns/well-formed-delta-v1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
deltaxml:version-order="a, v1, v2"
deltaxml:content-type="merge-concurrent"
deltaxml:version="2.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
deltaxml:deltaV2="a!=v1!=v2">
<modelVersion deltaxml:deltaV2="a=v1=v2">4.0.0</modelVersion>
<groupId deltaxml:deltaV2="a=v1=v2">groupId</groupId>
<artifactId deltaxml:deltaV2="a=v1=v2">git-merge-driver</artifactId>
<version deltaxml:deltaV2="a=v1=v2">7.0.4</version>
<dependencies deltaxml:deltaV2="a!=v1!=v2">
<dependency deltaxml:deltaV2="a!=v1">
<groupId deltaxml:deltaV2="a=v1">com.deltaxml.merge</groupId>
<artifactId deltaxml:deltaV2="a=v1">merge</artifactId>
<version deltaxml:deltaV2="a!=v1">
<deltaxml:textGroup deltaxml:deltaV2="a!=v1">
<deltaxml:text deltaxml:deltaV2="a">7.0.4</deltaxml:text>
<deltaxml:text deltaxml:deltaV2="v1">7.1.0</deltaxml:text>
</deltaxml:textGroup>
</version>
</dependency>
<dependency deltaxml:deltaV2="a=v2!=v1">
<groupId deltaxml:deltaV2="a=v1=v2">com.deltaxml.merge</groupId>
<artifactId deltaxml:deltaV2="a=v2!=v1">
<deltaxml:textGroup deltaxml:deltaV2="a=v2!=v1">
<deltaxml:text deltaxml:deltaV2="a=v2">deltaxml</deltaxml:text>
<deltaxml:text deltaxml:deltaV2="v1">flexlm</deltaxml:text>
</deltaxml:textGroup>
</artifactId>
<version deltaxml:deltaV2="a=v2!=v1">
<deltaxml:textGroup deltaxml:deltaV2="a=v2!=v1">
<deltaxml:text deltaxml:deltaV2="a=v2">10.0.0</deltaxml:text>
<deltaxml:text deltaxml:deltaV2="v1">7.1.0</deltaxml:text>
</deltaxml:textGroup>
</version>
</dependency>
<dependency deltaxml:deltaV2="a!=v1!=v2">
<groupId deltaxml:deltaV2="a=v1=v2">com.deltaxml.merge</groupId>
<artifactId deltaxml:deltaV2="a=v2!=v1">
<deltaxml:textGroup deltaxml:deltaV2="a=v2!=v1">
<deltaxml:text deltaxml:deltaV2="a=v2">flexlm</deltaxml:text>
<deltaxml:text deltaxml:deltaV2="v1">deltaxml</deltaxml:text>
</deltaxml:textGroup>
</artifactId>
<version deltaxml:deltaV2="a!=v1!=v2">
<deltaxml:textGroup deltaxml:deltaV2="a!=v1!=v2">
<deltaxml:text deltaxml:deltaV2="a">7.0.4</deltaxml:text>
<deltaxml:text deltaxml:deltaV2="v1">10.3.0</deltaxml:text>
<deltaxml:text deltaxml:deltaV2="v2">7.0.6</deltaxml:text>
</deltaxml:textGroup>
</version>
</dependency>
<dependency deltaxml:deltaV2="v2">
<groupId>com.deltaxml.merge</groupId>
<artifactId>merge</artifactId>
<version>7.0.6</version>
</dependency>
</dependencies>
</project>
Now we can see the artifacts are not aligning and this is because dependencies are an unordered set. We can fix this with orderless and in the current XML merge by keying, so for example:
<dependencies deltaxml:ordered="false">
<dependency deltaxml:key="com.deltaxml.merge:merge">
<groupId>com.deltaxml.merge</groupId>
<artifactId>merge</artifactId>
<version>7.0.4</version>
</dependency>
...
With this we get our expected alignment. Here we’ve done rule processing and the second dependency had some non-conflicting changes, but we don’t see these after rule processing. However the three way version conflicts remain:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:deltaxml="https://www.deltaxml.com/ns/well-formed-delta-v1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
deltaxml:version-order="ancestor, mine, theirs"
deltaxml:content-type="merge-concurrent"
deltaxml:version="2.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
deltaxml:deltaV2="ancestor!=mine!=theirs">
<modelVersion deltaxml:deltaV2="ancestor=mine=theirs">4.0.0</modelVersion>
<groupId deltaxml:deltaV2="ancestor=mine=theirs">groupId</groupId>
<artifactId deltaxml:deltaV2="ancestor=mine=theirs">git-merge-driver</artifactId>
<version deltaxml:deltaV2="ancestor=mine=theirs">7.0.4</version>
<dependencies deltaxml:deltaV2="ancestor!=mine!=theirs">
<dependency deltaxml:key="com.deltaxml.merge:merge" deltaxml:deltaV2="ancestor!=mine!=theirs">
<groupId deltaxml:deltaV2="ancestor=mine=theirs">com.deltaxml.merge</groupId>
<artifactId deltaxml:deltaV2="ancestor=mine=theirs">merge</artifactId>
<version deltaxml:deltaV2="ancestor!=mine!=theirs">
<deltaxml:textGroup deltaxml:deltaV2="ancestor!=mine!=theirs" deltaxml:edit-type="modify">
<deltaxml:text deltaxml:deltaV2="ancestor">7.0.4</deltaxml:text>
<deltaxml:text deltaxml:deltaV2="mine">7.1.0</deltaxml:text>
<deltaxml:text deltaxml:deltaV2="theirs">7.0.6</deltaxml:text>
</deltaxml:textGroup>
</version>
</dependency>
<dependency deltaxml:key="com.deltaxml.merge:deltaxml" deltaxml:deltaV2="ancestor=mine=theirs">
<groupId>com.deltaxml.merge</groupId>
<artifactId>deltaxml</artifactId>
<version>10.3.0</version>
</dependency>
<dependency deltaxml:key="com.deltaxml.merge:flexlm" deltaxml:deltaV2="ancestor!=mine!=theirs">
<groupId deltaxml:deltaV2="ancestor=mine=theirs">com.deltaxml.merge</groupId>
<artifactId deltaxml:deltaV2="ancestor=mine=theirs">flexlm</artifactId>
<version deltaxml:deltaV2="ancestor!=mine!=theirs">
<deltaxml:textGroup deltaxml:deltaV2="ancestor!=mine!=theirs" deltaxml:edit-type="modify">
<deltaxml:text deltaxml:deltaV2="ancestor">7.0.4</deltaxml:text>
<deltaxml:text deltaxml:deltaV2="mine">7.1.0</deltaxml:text>
<deltaxml:text deltaxml:deltaV2="theirs">7.0.6</deltaxml:text>
</deltaxml:textGroup>
</version>
</dependency>
</dependencies>
</project>
Now do we want my local branch or the remote or theirs in this case? I’d argue we want the highest version, irrespective of which branch its on; given that APIs are generally upwardly compatible it should hopefully work for all branches.
Luckily we can do this automatically in XSLT: here’s a POM dependency resolver:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
xmlns:deltaxml="https://www.deltaxml.com/ns/well-formed-delta-v1"
xmlns:pom="http://maven.apache.org/POM/4.0.0"
exclude-result-prefixes="xs math"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<!-- pom versions conflicts will have a version element with a
nested textGroup. If WordByWord=false and Splitting=false
then there should be no other non-whitespace text or
elements inside the version -->
<xsl:template match="pom:dependency/pom:version[deltaxml:textGroup]
[empty(* except deltaxml:textGroup) and empty(text()[normalize-space(.) ne ''])]">
<xsl:copy>
<!-- https://stackoverflow.com/questions/40202510/xsl-sort-numbers-separated-by-periods -->
<xsl:sequence select="sort(deltaxml:textGroup/deltaxml:text/text(), (), function($t){tokenize($t, '\.')!xs:integer(.)})[last()]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
This will ‘auto-resolve’ the version conflicts (which are going to be three way by definition). One final thing to consider – should this filter fix up deltas as it goes, so for example the deltaV2 on the parent dependency element, or should it just do a simple local change and we then have a final process which checks for any unresolved conflicts and when none are found removes all deltaxml attributes including deltas, version-order and content-type?
Putting this all together
The existing git merge driver: deltaxml/git-merge-driver will need an input filter for orderless and keying and also the above output filter and possibly as discussed above a filter to tidy up delta attributes.
This necessitates changing the driver code and the API changes needed require the move from the ThreeWayMerge
class to the ConcurrentMerge
class.
Future
There are certainly possibilities to extend this further; dependencies in POMs was the first example that comes to mind, it wasn’t an exhaustive analysis of the POM syntax.
This post has concentrated on an example and use-case for automatic conflict resolution, a future post will look at interactive conflict resolution.