How do custom merge drivers work in Git, and how do you configure them via .gitattributes to handle file types like lockfiles, generated code, and binary assets?
Quick Answer
Custom merge drivers are programs defined in git config and mapped to file patterns via .gitattributes. When Git encounters a merge conflict for a matching file, it invokes the driver instead of the default three-way text merge. Drivers receive the ancestor, ours, and theirs versions as arguments and must produce the merged result. Common uses include regenerating lockfiles, merging XML/JSON semantically, and handling binary files.
Detailed Answer
Think of custom merge drivers like hiring specialized arbitrators for different types of disputes. When two contract lawyers disagree about a legal clause, you bring in a legal mediator. When two architects disagree about a building plan, you bring in a structural engineer. The default Git merge is a general-purpose text mediator that works line-by-line. Custom merge drivers let you bring in domain-specific experts who understand the file format and can merge it correctly using semantic rules rather than line-based diffing.
A custom merge driver is configured in two steps. First, you define the driver in git config with a name, a description, and a command template: git config merge.npm-lockfile.driver 'npx npm-merge-driver merge %A %O %B %P'. The placeholders %A (ours/current), %O (ancestor/base), %B (theirs/incoming), and %P (path) are substituted by Git when invoking the driver. Second, you map file patterns to the driver in .gitattributes: 'package-lock.json merge=npm-lockfile'. When Git encounters a conflict in package-lock.json during a merge, it invokes the custom driver instead of the default text merge algorithm. The driver must exit 0 on success (writing the merged result to %A) or non-zero on failure (indicating an unresolvable conflict).
Internally, Git's merge machinery checks .gitattributes for each conflicted file before applying the default three-way merge. If a custom driver is specified, Git writes the ancestor, ours, and theirs versions to temporary files, invokes the driver command with the substituted placeholders, and reads back the result from the %A file. The driver has full control: it can invoke language-specific tools, parse the file semantically, or even make network calls. Built-in pseudo-drivers include 'binary' (always declares a conflict without merging), 'text' (forces text merge even for binary-detected files), and 'union' (concatenates both sides, useful for files like CHANGELOG.md where both additions should be kept). The merge.*.recursive config option specifies which driver to use when Git needs to merge merge-bases themselves during recursive merge resolution.
In production, custom merge drivers solve real pain points across several file types. Lockfiles (package-lock.json, yarn.lock, Gemfile.lock) are the most common case: these machine-generated files create enormous, incomprehensible conflicts during merges, but the correct resolution is simply to regenerate the lockfile after merging the dependency specification file. A driver that runs npm install (or yarn install) produces a correct lockfile without manual conflict resolution. Xcode project files (.pbxproj) use a structured format that text merge routinely corrupts; specialized drivers like mergepbx handle these semantically. JSON and YAML configuration files can use drivers that parse the structure, merge semantically, and re-serialize. For binary files like images or compiled assets, drivers can invoke format-specific diff tools or simply choose one version based on rules.
A critical gotcha: custom merge drivers defined in git config are local to each developer's machine and are not distributed with the repository. The .gitattributes file that maps patterns to drivers is tracked, but the driver definition is not. This means every developer must configure the driver locally, and CI/CD systems need the driver configured in their environment. Document required drivers in your project's contributing guide and automate setup with a script. Another trap: the driver command runs with the repository root as the working directory, not the file's directory, which can cause path issues in the driver script. Also, the 'union' driver blindly concatenates both sides, which can produce invalid files if both sides add entries with the same key in JSON or YAML. Test union-merged files before trusting the result.