
Andrew Hanna

Andrew Hanna

TL;DR: A merge driver is a program git calls instead of its own
line-based merge for the file patterns you nominate. Point one at your Salesforce
metadata in .gitattributes, register it in each clone's git config, and
independent XML node changes merge on their own instead of arriving as conflict
markers. It is a per-clone setting, so it needs a bootstrap step, and it never runs on
the host's merge button.
This is the setup guide, not the resolution guide. If you already have conflict markers open in front of you, read How to resolve Salesforce metadata merge conflicts without losing anyone's work first, then come back here to stop the next batch from reaching you at all.
When git performs a three-way merge it needs three versions of a file: the common ancestor, your side, and the incoming side. By default it reconciles them line by line. A merge driver replaces that step with a command of your choosing, and git hands it the three versions as temporary files.
The contract is small. Git substitutes the placeholders into your command line:
%O the common ancestor version%A your version, and the file the driver must write the result into
%B the incoming version%P the real path of the file being merged%L the conflict marker size
Exit 0 and git records the contents of %A as a clean merge.
Exit non-zero and git treats it as a conflict and leaves %A for a human.
That is the whole interface, which is why a Salesforce-aware driver can be as simple
as a script that parses both sides as XML, unions the child nodes under each parent,
and only bails out when the same key element carries different values on each side.
Two files, and only one of them is versioned.
First, register the driver in git config. This lives in .git/config,
which is local to the clone:
git config merge.sfxml.name "Salesforce metadata XML merge" git config merge.sfxml.driver "sf-xml-merge %O %A %B %P"
Second, tell git which files it owns, in .gitattributes at the repo root.
This one is committed, so every teammate gets it:
force-app/**/*.profile-meta.xml merge=sfxml force-app/**/*.permissionset-meta.xml merge=sfxml force-app/**/*.layout-meta.xml merge=sfxml force-app/**/*.translation-meta.xml merge=sfxml
Start narrow. Add the two or three metadata types that generate most of your conflicts, live with it for a sprint, then widen the patterns. A driver that silently mishandles a type you never tested is worse than the conflict it replaced.
.gitattributes is versioned but merge.sfxml.driver is not,
and git refuses to run a command it only learned about from a cloned file. That split
is deliberate security design: a repository you clone cannot make your machine execute
arbitrary commands.
The practical consequence is that a teammate who skips the setup gets no error. Git
quietly falls back to its built-in text merge and they see conflict markers, while
everyone else sees clean merges. Fix it with a bootstrap step nobody has to remember:
commit a scripts/setup-merge-driver.sh that runs the two
git config commands, call it from your package manager's post-install
hook, and have it exit loudly if the driver binary is missing.
Three gaps are worth knowing before you rely on it:
.git/config. A
pull request that would merge cleanly on your laptop can still report conflicts
there. The workaround is the usual one: merge the target branch into your feature
branch locally, where the driver runs, and push the resolution. How often that bites
depends on your
branching strategy: long-lived branches drift further and hit server-side merges more often.
*.flow-meta.xml out
of your patterns and resolve those in Flow Builder.
Yes, for the additive cases. Git ships a built-in union driver that keeps
every line from both sides, and it needs no config entry at all:
force-app/**/labels/*.labels-meta.xml merge=union
It is blunt. Union merge concatenates rather than de-duplicates, so a node both branches added lands twice and the file can come back invalid. Use it only on flat, purely additive lists like custom labels, and keep a deployment validation in CI to catch what it gets wrong. Treat it as the stopgap while you evaluate a real driver, not the destination.
A merge driver is the cheapest layer and the narrowest: it removes false conflicts at the git level, on developer machines, and does nothing about validation or deployment. DevOps platforms build semantic merge into the pipeline with a visual three-way panel, which covers the server-side gap but ties resolution to their tooling. That trade-off, portability against a packaged panel, is the crux of Serpent vs Copado.
Serpent takes the third route and resolves on the pull request itself. Serpent AI reads the conflict, proposes a resolution, and waits for your review before applying it, so the fix lands where the merge actually happens rather than on whichever laptop is configured correctly. See Serpent AI, or browse more evergreen Salesforce DevOps playbooks in the Serpent guides.
Do I need to write the merge driver myself?
Not necessarily. Open-source Salesforce XML merge drivers exist, and any XML-aware
merge tool can be wrapped in a two-line shell script that matches the
%O %A %B contract. Writing your own is a reasonable afternoon if your
metadata shape is unusual.
Will a merge driver change files that are already committed?
No. It only runs during a merge, on the merged result. History is untouched, so you can add one and remove it again without rewriting anything.
What happens if the driver crashes?
Git treats a non-zero exit as an unresolved conflict and leaves the markers in place, so a broken driver degrades to the behaviour you had before. Make yours exit non-zero on anything it does not understand rather than guessing.
Does this replace normalising metadata on retrieve?
No, and the two stack well. Normalising element order shrinks the diffs before git sees them, and the driver handles whatever survives that. Run both if you can.
Commitment free!