Salesforce Metadata Git Merge Driver

April 20, 2026 · View on GitHub

License Compatibility Performance

An intelligent Git merge driver specifically designed for Salesforce metadata files. Eliminates hours of manual merge conflicts resolution.

Demo

Without sf-git-merge-driver

Demo without sf-git-merge-driver

With sf-git-merge-driver

Demo 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, -Y flags)

Example conflict output:

<<<<<<< ours
    <field>localValue</field>
||||||| base
    <field>originalValue</field>
=======
    <field>remoteValue</field>
>>>>>>> theirs

This approach helps you understand:

  1. What the original value was (base)
  2. What your branch changed it to (ours)
  3. 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 values
  • CustomField → value set entries
  • RecordType → 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 install in 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/config entries keep working via the retained oclif command, but do not receive the speedup until reinstall.

Re-installing also wires git's %S placeholder into conflict markers: the ancestor-marker label now reflects git's own label (typically a short SHA) instead of the static base string. 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/attributes line), install now aborts with a conflict report instead of silently stacking up. Use --on-conflict=skip to 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-run to 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:

VsCode SFDX-Hardis integration

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):

  • -O ancestor file, -A local file, -B other file, -P output file — the merged result is written back over -A (matching git's merge-driver contract); -P is accepted for compatibility but not used as the output target.
  • -L conflict marker size (1–100, default 7).
  • -S / -X / -Y conflict 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 run is a thin oclif wrapper kept only for backward compatibility with .git/config entries 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 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