Start free
Andrew Hanna

Andrew Hanna

How to Set Up a Salesforce-Aware Git Merge Driver

How to Set Up a Salesforce-Aware Git Merge Driver

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.

What does a merge driver actually do?

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.

How do you configure one in git?

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.

Why does every clone need setup, and how do you automate it?

.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.

Which merges does the driver never see?

Three gaps are worth knowing before you rely on it:

  • Server-side merges. The merge button on GitHub, GitLab or Bitbucket runs on their infrastructure, with no access to your .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.
  • One-sided changes. Git only invokes a driver when both sides touched the file. If only one side changed it, git takes that side and your driver never starts.
  • Generated XML. A driver can union nodes, but it cannot know that two Flow versions describe incompatible logic. Keep *.flow-meta.xml out of your patterns and resolve those in Flow Builder.

Is there a zero-install version?

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.

Driver, platform, or resolution on the pull request?

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.

FAQ

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.

Related Articles

Curious about faster shipping before you dive in? Let's talk

Commitment free!