Salesforce Metadata Git Merge Driver
April 20, 2026 · View on GitHub
An intelligent Git merge driver specifically designed for Salesforce metadata files. Eliminates hours of manual merge conflicts resolution.
Demo
Without sf-git-merge-driver

With sf-git-merge-driver

Why use this plugin?
- Saves time: No more manual XML conflict resolution
- Zero-config: Works immediately after installation
- Reliable: Understands Salesforce metadata structure
- Transparent: Seamless Git workflow integration
How it works
sequenceDiagram
participant Dev
participant Git
participant MergeDriver
Dev->>Git: git merge
Git->>MergeDriver: Detects XML conflict
MergeDriver->>MergeDriver: Analyzes metadata structure
MergeDriver->>MergeDriver: Smart merging
MergeDriver->>Git: Returns merged result
Git->>Dev: Clean commit (no conflicts)
Conflict Style
This merge driver follows the zdiff3 conflict style philosophy:
- Shows the most compact diff possible: Only the specific conflicting elements are marked, not entire file sections
- Includes ancestor context: Conflicts display the base (ancestor) version alongside local and remote changes
- Respects Git configuration: Conflict marker size and labels are configurable via Git's standard parameters (
-L,-S,-X,-Yflags)
Example conflict output:
<<<<<<< ours
<field>localValue</field>
||||||| base
<field>originalValue</field>
=======
<field>remoteValue</field>
>>>>>>> theirs
This approach helps you understand:
- What the original value was (
base) - What your branch changed it to (
ours) - What the other branch changed it to (
theirs)
Deterministic Ordering
For metadata types where element order matters (like picklist values), the merge driver preserves ordering while intelligently merging changes:
- Disjoint reorderings merge automatically: If one branch reorders elements A↔B and another reorders C↔D, both changes are preserved
- Conflicting reorderings trigger conflict: If both branches move the same element to different positions, a conflict is raised for manual resolution
- Additions respect position: New elements are inserted at their intended position
Supported ordered metadata:
GlobalValueSet/StandardValueSet→ picklist valuesCustomField→ value set entriesRecordType→ picklist value assignments
This ensures picklist value ordering in your org matches what you expect after a merge.
Installation (30 seconds)
With Salesforce CLI
# Install plugin (one time, global)
sf plugins install sf-git-merge-driver
# Configure merge driver in your project (one time, local per project)
cd my/sf/project
sf git merge driver install
Upgrading from a previous version: re-run
sf git merge driver installin each repository after upgrading. Recent releases route conflicts through a bundled standalone binary (~80 ms per file instead of ~600 ms via the oclif command) — a 200-file rebase drops from ~120 s to ~16 s of merge-driver overhead. Previously-installed.git/configentries keep working via the retained oclif command, but do not receive the speedup until reinstall.Re-installing also wires git's
%Splaceholder into conflict markers: the ancestor-marker label now reflects git's own label (typically a short SHA) instead of the staticbasestring. See the CHANGELOG for the exact before/after.Safe install on upgrade. If another merge driver is already configured on one of our metadata globs (e.g. a different Salesforce tool added its own
.git/info/attributesline), install now aborts with a conflict report instead of silently stacking up. Use--on-conflict=skipto leave those globs alone,--on-conflict=overwrite(or--force) to take them over — in which case uninstall will restore the originals from an annotation comment. Add--dry-runto preview any install/uninstall plan before writing.
Integration in VsCode SFDX-Hardis
SFDX-Hardis VS Code extension has integrated the sf-git-merge-driver in its default dependencies, and offers a one click activation of the merge driver from the status bar, as shown below:

Usage
The merge driver activates automatically for conflicts on:
No additional steps required! Works during normal Git operations:
git pull # Conflicts resolved automatically
git merge # Same here
Configuration
Configured for these metadata files by default:
*.profile-meta.xml merge=salesforce-source
*.permissionset-meta.xml merge=salesforce-source
*.labels-meta.xml merge=salesforce-source
*.label-meta.xml merge=salesforce-source
*.applicationVisibility-meta.xml merge=salesforce-source
*.classAccess-meta.xml merge=salesforce-source
*.customMetadataTypeAccess-meta.xml merge=salesforce-source
*.customPermission-meta.xml merge=salesforce-source
*.customSettingAccess-meta.xml merge=salesforce-source
*.externalCredentialPrincipalAccess-meta.xml merge=salesforce-source
*.externalDataSourceAccess-meta.xml merge=salesforce-source
*.fieldPermission-meta.xml merge=salesforce-source
*.flowAccess-meta.xml merge=salesforce-source
*.objectPermission-meta.xml merge=salesforce-source
*.pageAccess-meta.xml merge=salesforce-source
*.recordTypeVisibility-meta.xml merge=salesforce-source
*.tabSetting-meta.xml merge=salesforce-source
*.userPermission-meta.xml merge=salesforce-source
*.objectSettings-meta.xml merge=salesforce-source
*.permissionsetgroup-meta.xml merge=salesforce-source
*.permissionSetLicenseDefinition-meta.xml merge=salesforce-source
*.mutingpermissionset-meta.xml merge=salesforce-source
*.sharingRules-meta.xml merge=salesforce-source
*.sharingCriteriaRule-meta.xml merge=salesforce-source
*.sharingGuestRule-meta.xml merge=salesforce-source
*.sharingOwnerRule-meta.xml merge=salesforce-source
*.sharingTerritoryRule-meta.xml merge=salesforce-source
*.workflow-meta.xml merge=salesforce-source
*.workflowAlert-meta.xml merge=salesforce-source
*.workflowFieldUpdate-meta.xml merge=salesforce-source
*.workflowFlowAction-meta.xml merge=salesforce-source
*.workflowKnowledgePublish-meta.xml merge=salesforce-source
*.workflowOutboundMessage-meta.xml merge=salesforce-source
*.workflowRule-meta.xml merge=salesforce-source
*.workflowSend-meta.xml merge=salesforce-source
*.workflowTask-meta.xml merge=salesforce-source
*.assignmentRules-meta.xml merge=salesforce-source
*.autoResponseRules-meta.xml merge=salesforce-source
*.escalationRules-meta.xml merge=salesforce-source
*.marketingappextension-meta.xml merge=salesforce-source
*.matchingRule-meta.xml merge=salesforce-source
*.globalValueSet-meta.xml merge=salesforce-source
*.standardValueSet-meta.xml merge=salesforce-source
*.globalValueSetTranslation-meta.xml merge=salesforce-source
*.standardValueSetTranslation-meta.xml merge=salesforce-source
*.translation-meta.xml merge=salesforce-source
*.objectTranslation-meta.xml merge=salesforce-source
How to disable it for a specific merge
When you don't want to use the merge driver for a specific merge, just backup the .git/info/attributes file and put it back after the merge.
mv .git/info/attributes .git/info/attributes.bak
git merge <branch>
mv .git/info/attributes.bak .git/info/attributes
If you want to disable the merge driver for a specific file, just comment the merge driver configuration from the .git/info/attributes file.
# *.profile-meta.xml merge=salesforce-source
If you want to disable it for all the project, just uninstall the driver:
sf git merge driver uninstall
How to know if it is installed and enabled ?
You can check if the merge driver is installed by running the following command:
git config --show-origin --get-regexp '^merge.salesforce-source(\..*)?'
You can check if the merge driver is enabled by running the following command:
grep "merge=salesforce-source" .git/info/attributes
Specificities in CI/CD context
The plugin will add content in the file $GIT_DIR/info/attributes to enable the merge driver.
This files need to be present for the plugin to be installed.
Ensure the repository is properly cloned / checked out in CI/CD context before installing the plugin.
Advanced: direct binary invocation (for scripts)
Most users never invoke the driver directly — git does. But for scripted recovery
(e.g. replaying a merge after a botched rebase, batch-resolving conflicts in CI,
or diagnosing a specific file pair) you can call the bundled binary directly. It
has no sf CLI / oclif startup cost (~37 ms cold vs ~500 ms via sf git merge driver run,
which is why git itself also bypasses the sf CLI once sf git merge driver install
has been run).
Locate the binary from the sf-installed plugin (no separate npm install -g needed —
this is the same copy git uses, so there is no version-drift risk):
# Unix/Linux version — Resolve the plugin root from sf's plugin registry (user-installed plugins)
PLUGIN_ROOT=$(dirname "$(sf plugins inspect sf-git-merge-driver --json | jq -r '.[0].options.root')")/node_modules/sf-git-merge-driver
DRIVER="$PLUGIN_ROOT/bin/merge-driver.cjs"
# Powershell version — Resolve the plugin root from sf's plugin registry (user-installed plugins)
$DRIVER = Join-Path (Join-Path (Split-Path ((sf plugins inspect sf-git-merge-driver --json | ConvertFrom-Json)[0].options.root) -Parent) "node_modules/sf-git-merge-driver") "bin/merge-driver.cjs"
# Execution
# POSIX — the binary has a shebang and +x, so it's directly executable:
"$DRIVER" -O base.xml -A local.xml -B other.xml -P merged.xml
# Windows / anywhere shebangs aren't honoured, invoke via node explicitly:
node "$DRIVER" -O base.xml -A local.xml -B other.xml -P merged.xml
Flags (git's merge-driver %O %A %B %P convention):
-Oancestor file,-Alocal file,-Bother file,-Poutput file — the merged result is written back over-A(matching git's merge-driver contract);-Pis accepted for compatibility but not used as the output target.-Lconflict marker size (1–100, default 7).-S/-X/-Yconflict tag labels for ancestor / local / other.
Exit codes (scripts should branch on these, not on stderr content):
0— clean merge, no conflict markers written.1— merge completed with conflict markers in the output.2— usage error (missing/unreadable file, bad flag, unsupported Node version).
Example — batch-resolve all profile conflicts in the working tree:
PLUGIN_ROOT=$(dirname "$(sf plugins inspect sf-git-merge-driver --json | jq -r '.[0].options.root')")/node_modules/sf-git-merge-driver
DRIVER="$PLUGIN_ROOT/bin/merge-driver.cjs"
for f in $(git diff --name-only --diff-filter=U | grep '\.profile-meta\.xml$'); do
git show :1:"$f" > /tmp/base.xml # :1: = common ancestor
git show :2:"$f" > /tmp/local.xml # :2: = our side
git show :3:"$f" > /tmp/other.xml # :3: = their side
node "$DRIVER" -O /tmp/base.xml -A /tmp/local.xml -B /tmp/other.xml -P /tmp/merged.xml || true
cp /tmp/local.xml "$f" # -A receives the merged (or conflict-marked) result
done
sf git merge driver runis a thin oclif wrapper kept only for backward compatibility with.git/configentries generated by sf-git-merge-driver ≤ 1.5. It is deprecated and scheduled for removal in the next major release. Use the binary directly for scripts.
Behavior when the merge driver does not know the key
If the merge driver encounters a list of elements (like fields in a profile, or permissions in a permission set) but does not know which field acts as the "key" (unique identifier) for that type, it will fallback to standard conflict behavior. This means you might see a conflict block containing the entire array instead of a smart merge of individual elements.
If you encounter this behavior for a Salesforce metadata type the driver is supposed to handle, please open an issue! We can add the missing key definition to support smart merging for that type.
If you encounter this behavior for a Salesforce metadata type the driver does not already handle, please open an issue! We can evaluate how to support smart merging for that type.
Troubleshooting
The plugin uses the Salesforce CLI logging system to log information.
You can control the logging level by setting the SF_LOG_LEVEL environment variable.
You can redirect the logging in the terminal using DEBUG=sf-git-merge-driver.
You can also use GIT_TRACE=1 to get more information about git operations.
You can also use GIT_MERGE_VERBOSITY=5 to get more information about the merge process.
Git environment variables are detailed here.
Example:
DEBUG=sf-git-merge-driver
SF_LOG_LEVEL=trace # can be error | warn | info | debug | trace, default: warn
GIT_MERGE_VERBOSITY=5 # can be 0 to 5
GIT_TRACE=true
git merge ...
Commands
sf git merge driver disablesf git merge driver enablesf git merge driver installsf git merge driver runsf git merge driver uninstall
sf git merge driver disable
Uninstalls the local git merge driver from the current project.
USAGE
$ sf git merge driver disable [--json] [--flags-dir <value>] [--dry-run]
FLAGS
--dry-run Plan the uninstall without touching git config or .git/info/attributes. Exits 0; shows what would be
removed.
GLOBAL FLAGS
--flags-dir=<value> Import flag values from a directory.
--json Format output as json.
DESCRIPTION
Uninstalls the local git merge driver from the current project.
Removes the `merge.salesforce-source` section from `.git/config` and strips the driver's rules from
`.git/info/attributes`. Lines that combined the driver with user attributes (e.g. `*.profile-meta.xml text=auto
merge=salesforce-source`) are rewritten to keep the user attributes; only the `merge=` token is removed. If a previous
install used `--on-conflict=overwrite`, the original driver rule is restored from the annotation comment written at
install time. `--dry-run` previews the plan without writing.
ALIASES
$ sf git merge driver disable
EXAMPLES
Uninstall the driver for a given project:
$ sf git merge driver disable
Preview the changes that would be written:
$ sf git merge driver disable --dry-run
sf git merge driver enable
Installs a local git merge driver for Salesforce metadata in the current project.
USAGE
$ sf git merge driver enable [--json] [--flags-dir <value>] [--dry-run] [--on-conflict abort|skip|overwrite] [--force]
FLAGS
--dry-run Plan the install without writing to git config or .git/info/attributes. Exits 0; shows the
list of rules that would be added/skipped/conflict.
--force Alias for --on-conflict=overwrite. Non-interactive shortcut for CI.
--on-conflict=<option> [default: abort] How to handle patterns already owned by another merge driver in
.git/info/attributes. Default: abort (refuse to change anything).
<options: abort|skip|overwrite>
GLOBAL FLAGS
--flags-dir=<value> Import flag values from a directory.
--json Format output as json.
DESCRIPTION
Installs a local git merge driver for Salesforce metadata in the current project.
Registers the driver in `.git/config` and adds one merge rule per Salesforce metadata glob to `.git/info/attributes`.
Safe to re-run: install is idempotent, preserves any user attributes already on the globs, and dedupes legacy
duplicate rules silently.
If another merge driver is already configured on one of our globs, install aborts by default and lists the conflicts.
Pass `--on-conflict=skip` to leave those globs to the other driver, `--on-conflict=overwrite` (or `--force`) to take
them over (uninstall restores the originals), or run the command from a TTY to be prompted interactively. `--dry-run`
previews the plan without writing.
ALIASES
$ sf git merge driver enable
EXAMPLES
Install the driver for a given project:
$ sf git merge driver enable
Preview the changes that would be written:
$ sf git merge driver enable --dry-run
Take over conflicting globs non-interactively (for CI):
$ sf git merge driver enable --force
sf git merge driver install
Installs a local git merge driver for Salesforce metadata in the current project.
USAGE
$ sf git merge driver install [--json] [--flags-dir <value>] [--dry-run] [--on-conflict abort|skip|overwrite]
[--force]
FLAGS
--dry-run Plan the install without writing to git config or .git/info/attributes. Exits 0; shows the
list of rules that would be added/skipped/conflict.
--force Alias for --on-conflict=overwrite. Non-interactive shortcut for CI.
--on-conflict=<option> [default: abort] How to handle patterns already owned by another merge driver in
.git/info/attributes. Default: abort (refuse to change anything).
<options: abort|skip|overwrite>
GLOBAL FLAGS
--flags-dir=<value> Import flag values from a directory.
--json Format output as json.
DESCRIPTION
Installs a local git merge driver for Salesforce metadata in the current project.
Registers the driver in `.git/config` and adds one merge rule per Salesforce metadata glob to `.git/info/attributes`.
Safe to re-run: install is idempotent, preserves any user attributes already on the globs, and dedupes legacy
duplicate rules silently.
If another merge driver is already configured on one of our globs, install aborts by default and lists the conflicts.
Pass `--on-conflict=skip` to leave those globs to the other driver, `--on-conflict=overwrite` (or `--force`) to take
them over (uninstall restores the originals), or run the command from a TTY to be prompted interactively. `--dry-run`
previews the plan without writing.
ALIASES
$ sf git merge driver enable
EXAMPLES
Install the driver for a given project:
$ sf git merge driver install
Preview the changes that would be written:
$ sf git merge driver install --dry-run
Take over conflicting globs non-interactively (for CI):
$ sf git merge driver install --force
See code: src/commands/git/merge/driver/install.ts
sf git merge driver run
Runs the merge driver for the specified files.
USAGE
$ sf git merge driver run -O <value> -A <value> -B <value> -P <value> [--json] [--flags-dir <value>] [-L <value>] [-S
<value>] [-X <value>] [-Y <value>]
FLAGS
-A, --local-file=<value> (required) path to our version of the file
-B, --other-file=<value> (required) path to their version of the file
-L, --conflict-marker-size=<value> [default: 7] number of characters to show for conflict markers
-O, --ancestor-file=<value> (required) path to the common ancestor version of the file
-P, --output-file=<value> (required) path to the file where the merged content will be written
-S, --ancestor-conflict-tag=<value> [default: base] string used to tag ancestor version in conflicts
-X, --local-conflict-tag=<value> [default: ours] string used to tag local version in conflicts
-Y, --other-conflict-tag=<value> [default: theirs] string used to tag other version in conflicts
GLOBAL FLAGS
--flags-dir=<value> Import flag values from a directory.
--json Format output as json.
DESCRIPTION
Runs the merge driver for the specified files.
DEPRECATED — will be removed in the next major release. Run `sf git merge driver install` in the repository to upgrade
to the faster standalone binary driver.
Runs the merge driver for the specified files, handling the merge conflict resolution using Salesforce-specific merge
strategies. This command is typically called automatically by Git when a merge conflict is detected.
EXAMPLES
Run the merge driver for conflicting files:
$ sf git merge driver run --ancestor-file=<value> --local-file=<value> --other-file=<value> \
--output-file=<value>
Where:
- ancestor-file is the path to the common ancestor version of the file
- local-file is the path to our version of the file
- other-file is the path to their version of the file
- output-file is the path to the file where the merged content will be written
See code: src/commands/git/merge/driver/run.ts
sf git merge driver uninstall
Uninstalls the local git merge driver from the current project.
USAGE
$ sf git merge driver uninstall [--json] [--flags-dir <value>] [--dry-run]
FLAGS
--dry-run Plan the uninstall without touching git config or .git/info/attributes. Exits 0; shows what would be
removed.
GLOBAL FLAGS
--flags-dir=<value> Import flag values from a directory.
--json Format output as json.
DESCRIPTION
Uninstalls the local git merge driver from the current project.
Removes the `merge.salesforce-source` section from `.git/config` and strips the driver's rules from
`.git/info/attributes`. Lines that combined the driver with user attributes (e.g. `*.profile-meta.xml text=auto
merge=salesforce-source`) are rewritten to keep the user attributes; only the `merge=` token is removed. If a previous
install used `--on-conflict=overwrite`, the original driver rule is restored from the annotation comment written at
install time. `--dry-run` previews the plan without writing.
ALIASES
$ sf git merge driver disable
EXAMPLES
Uninstall the driver for a given project:
$ sf git merge driver uninstall
Preview the changes that would be written:
$ sf git merge driver uninstall --dry-run
See code: src/commands/git/merge/driver/uninstall.ts
Changelog
changelog.md is available for consultation.
Versioning
Versioning follows SemVer specification.
Authors
Contributing
Contributions are what make the trailblazer community such an amazing place. We regard this component as a way to inspire and learn from others. Any contributions you make are appreciated.
See contributing.md for sgd contribution principles.
License
This project license is MIT - see the LICENSE.md file for details