Merge Two Edited Files with git merge-file

Two people edited the same config file, and you need one merged result without spinning up a full repository. git merge-file does the three-way merge on plain files, no repository required. The examples use Git 2.43.0 from Ubuntu's git-man package, version 1:2.43.0-1ubuntu7.3. Allow about fifteen minutes. You need Git and three text files: the common original, your current copy, and the other copy.

git merge-file is a file-level three-way merge. Its arguments are current, base, and other, in that order. Git applies the changes made in other since base onto current. Unless you pass -p, it writes the merged result straight back into the current file, so make a copy before experimenting.

1. Create a disposable merge case

Work in a temporary directory for this first run. This only touches files below that directory and needs no elevated privileges:

$ workdir=$(mktemp -d /tmp/git-merge-file-demo.XXXXXX)
$ printf '%s\n' 'colour=blue' 'timeout=30' > "$workdir/base.conf"
$ printf '%s\n' 'colour=green' 'timeout=30' > "$workdir/current.conf"
$ printf '%s\n' 'colour=blue' 'timeout=45' > "$workdir/other.conf"
$ git --version
git version 2.43.0

base.conf is the shared starting point. The current copy changes the colour; the other copy changes the timeout. Different lines, so this merge should be clean.

Checkpoint: Confirm the three paths before running a command that omits -p. The first path is the file that would be overwritten.

2. Preview the merge without overwriting

Send the result to standard output with -p. Labels make later conflict markers readable:

$ git merge-file -p \
    -L CURRENT -L BASE -L OTHER \
    "$workdir/current.conf" "$workdir/base.conf" "$workdir/other.conf"
colour=green
timeout=45
$ printf 'merge status: %s\n' "$?"
merge status: 0

Zero status means no conflicts. The original files are untouched because -p printed the result instead of writing it. To keep the preview, redirect it to a new path and inspect it before replacing anything:

$ git merge-file -p "$workdir/current.conf" "$workdir/base.conf" "$workdir/other.conf" > "$workdir/merged.conf"
$ diff -u "$workdir/current.conf" "$workdir/merged.conf"
--- /tmp/.../current.conf
+++ /tmp/.../merged.conf
@@
-timeout=30
+timeout=45

Your own temporary directory name will differ in that diff output. A clean exit from diff just means the command ran and the output exists as a separate file. If you decide the merged file should become the current one, replace it only after reviewing it, and keep a backup while the current file still matters.

3. Reproduce and inspect a conflict

A conflict happens when both edited copies change the same part of the base. Set that case up:

$ printf '%s\n' 'mode=standard' > "$workdir/base.conf"
$ printf '%s\n' 'mode=fast' > "$workdir/current.conf"
$ printf '%s\n' 'mode=safe' > "$workdir/other.conf"
$ git merge-file -p -L CURRENT -L BASE -L OTHER \
    "$workdir/current.conf" "$workdir/base.conf" "$workdir/other.conf"
<<<<<<< CURRENT
mode=fast
=======
mode=safe
>>>>>>> OTHER

Here is the surprising bit: the command's return value is the number of conflicts, not a plain success/failure flag. Capture that status immediately, before you do anything else:

$ git merge-file -p "$workdir/current.conf" "$workdir/base.conf" "$workdir/other.conf" > "$workdir/conflicted.conf"
$ status=$?
$ printf 'conflicts: %s\n' "$status"
conflicts: 1

A useful check for stray markers:

$ rg -n '^(<<<<<<<|=======|>>>>>>>||||||||)' "$workdir/conflicted.conf"
1:<<<<<<< CURRENT
3:=======
5:>>>>>>> OTHER

Nothing printed after you edit the file means the marker check passed. It does not prove the chosen setting is correct, so review the surrounding content too.

4. Reach for an automatic conflict policy only when it is justified

These resolve conflicts in the generated output, but they can silently discard or duplicate a change. Use them only when that policy is genuinely part of the file's rules:

$ git merge-file --ours -p \
    "$workdir/current.conf" "$workdir/base.conf" "$workdir/other.conf"
mode=fast
$ printf 'status: %s\n' "$?"
status: 0

Status is zero because the conflict was resolved automatically, not because anyone approved the content. Never use --ours or --theirs as a shortcut for actually reading a security policy, deployment file, or data migration.

For more context around a conflict, add --diff3 or --zdiff3. The extra base text can explain what each side changed, at the cost of more to read. The default conflict style is shorter.

5. Apply a reviewed result to the current file

Once you are ready to update the current file, keep a backup and let Git write the result in place:

$ cp -- "$workdir/current.conf" "$workdir/current.conf.before-merge"
$ git merge-file -L CURRENT -L BASE -L OTHER \
    "$workdir/current.conf" "$workdir/base.conf" "$workdir/other.conf"
$ printf 'status: %s\n' "$?"
status: 0

This changes current.conf and nothing else. It does not create a commit, update the Git index, or touch a repository by itself. To undo this example, restore the backup:

$ cp -- "$workdir/current.conf.before-merge" "$workdir/current.conf"

There is no need for sudo here. If the target file is not writable, fix ownership or permissions through your normal administration process rather than running an unfamiliar merge as root.

6. Know when object IDs are the better interface

Inside a valid Git repository, --object-id accepts blob object IDs instead of file paths. Without -p it writes the merged blob to the object store and prints its object ID; with -p it prints merged content as usual. This suits plumbing code that already has blobs in hand, but it is not a way to merge arbitrary commit IDs: the arguments must refer to blobs.

For ordinary files, use paths and -p first. It keeps the preview visible and makes an accidental overwrite less likely. Reach for -q only when a script deliberately handles the conflict count itself and does not need Git's warning on standard error.

Done means