mergeDriver
The merge driver is a callback which is called for each conflicting file during a merge. It takes the file contents on each branch as an array and returns the merged result.
By default the merge command uses the diff3 algorithm to try to solve merge conflicts, and throws an error if the conflict cannot be resolved. This is not always ideal, so isomorphic-git implements merge drivers so that users may implement their own merging algorithm.
A merge driver implements the following API:
async ({ branches, contents, path }) => { cleanMerge, mergedText }
| param | type [= default] | description |
|---|---|---|
| branches | Array<string> | an array of human readable branch names |
| contents | Array<string> | an array of the file's contents on each respective branch |
| path | string | the file's path relative to the git repository |
| return | Promise<{cleanMerge: bool, mergedText: string}> | Whether merge was successful, and the merged text |
If cleanMerge is true, then the mergedText string will be written to the file. If cleanMerge is false, a MergeConflictError will be thrown and no merge commit will be created.
If merge was called with abortOnConflict: false, the mergedText string will be written to the file even if there is a merge conflict. Otherwise, in the event of a merge conflict, no changes will be written to the worktree or index.
MergeDriverParams#path
The path parameter refers to the path of the conflicted file, relative to the root of the git repository.
MergeDriverParams#branches
The branches array contains the human-readable names of the branches we are merging. The first index refers to the merge base, the second refers to the branch being merged into, and any subsequent indexes refer to the branches we are merging. For example, say we have a git history that looks like this:
A topic
/
D---E main
If we were to merge topic into main, the branches array would look like: ['base', 'main', 'topic']. In this case, the name base refers to commit D which is the common ancestor of our two branches. base will always be the name at the first index.
MergeDriverParams#contents
The contents array contains the file contents respective of each branch. Like the branches array, the first index always refers to the merge base. The second index always refers to the branch we are merging into, i.e. 'ours'. Subsequent indexes refer to the branches we are merging, i.e. 'theirs'.
For example, say we have a file text.txt which contains:
original
text
file
On the main branch, we modify the text file to read:
text
file
was
modified
However, on the topic branch, we modify the text file to read:
modified
text
file
In this case, when our merge driver is called on text.txt, the contents array will look like this:
[
'original\ntext\nfile',
'text\n\file\nwas\nmodified',
'modified\ntext\nfile',
]
Examples
Below is an example of a very simple merge driver which always chooses the other branch's version of the file whenever it was modified by both branches.
const mergeDriver = ({ contents }) => {
const mergedText = contents[2]
return { cleanMerge: true, mergedText }
}
If we applied this algorithm to the conflict in the previous example, the resolved file would simply read:
modified
text
file
and if instead we wanted to chose our branch's version of the file, whenever it was modified by both branches,we simply change the line:
const mergedText = contents[2]
to read:
const mergedText = contents[1]
which results in the resolved file reading:
text
file
was
modified
As a more complex example, we use the default diff3 algorithm, but choose the other branch's changes whenever specific lines of the file conflict.
const diff3Merge = require('diff3')
const mergeDriver = ({ contents }) => {
const baseContent = contents[0]
const ourContent = contents[1]
const theirContent = contents[2]
const LINEBREAKS = /^.*(\r?\n|$)/gm
const ours = ourContent.match(LINEBREAKS)
const base = baseContent.match(LINEBREAKS)
const theirs = theirContent.match(LINEBREAKS)
const result = diff3Merge(ours, base, theirs)
let mergedText = ''
for (const item of result) {
if (item.ok) {
mergedText += item.ok.join('')
}
if (item.conflict) {
mergedText += item.conflict.b.join('')
}
}
return { cleanMerge: true, mergedText }
}
If we apply this algorithm to the conflict in the previous example, the resolved file reads:
modified
text
file
was
modified
and if we wanted to choose our branch's changes whenever specific lines of the file conflict, we simply change the above line:
mergedText += item.conflict.b.join('')
to read:
mergedText += item.conflict.a.join('')
which results in a resolved file that reads:
text
file
was
modified
Finally, what if we wanted to make a slight modification to the behavior of the default merge driver, like changing the size of conflict markers? The code for the default merge driver is located in src/utils/mergeFile.js. We can copy the code into our merge driver like so:
const diff3Merge = require('diff3')
const mergeDriver = ({ contents, branches }) => {
const ourName = branches[1]
const theirName = branches[2]
const baseContent = contents[0]
const ourContent = contents[1]
const theirContent = contents[2]
const ours = ourContent.match(LINEBREAKS)
const base = baseContent.match(LINEBREAKS)
const theirs = theirContent.match(LINEBREAKS)
const result = diff3Merge(ours, base, theirs)
const markerSize = 7
let mergedText = ''
let cleanMerge = true
for (const item of result) {
if (item.ok) {
mergedText += item.ok.join('')
}
if (item.conflict) {
cleanMerge = false
mergedText += `${'<'.repeat(markerSize)} ${ourName}\n`
mergedText += item.conflict.a.join('')
mergedText += `${'='.repeat(markerSize)}\n`
mergedText += item.conflict.b.join('')
mergedText += `${'>'.repeat(markerSize)} ${theirName}\n`
}
}
return { cleanMerge, mergedText }
}
If we want larger conflict markers, we can simply change the line
const markerSize = 7
to
const markerSize = 14
Which will give us conflict markers that are 14 characters wide instead of the default 7.
Now if we use this merge driver when merging the branch 'topic' into 'main', and if we have abortOnConflict set to false, the worktree will be updated with a text.txt file that looks like this:
<<<<<<<<<<<<<< main
modified
==============
>>>>>>>>>>>>>> topic
text
file
was
modified