Merge Three File Versions Safely with diff3
You will finish with a reviewable merged file made from an original file and two edited copies, while keeping all three inputs unchanged. The examples use GNU diff3 from diffutils 3.10, installed here on a Linux system. Allow about fifteen minutes. You need a shell and three text files: the common original, your edited copy, and another edited copy.
The route
Jump straight to the step you need, or tick off Done means at the end.
diff3's argument order is easy to get wrong. Its required operands are MYFILE OLDFILE YOURFILE. The middle operand is the shared original, not simply the oldest file you happen to have. The first and third operands are the two versions whose changes are compared against that original.
1. Check the installed command
Confirm which implementation and version you will use. This is a read-only command and does not need elevated privileges:
$ command -v diff3
/usr/bin/diff3
$ diff3 --version | head -n 1
diff3 (GNU diffutils) 3.10
GNU diff3 compares files line by line. Its normal output is a human-readable change report. The -m or --merge option is the mode used here: it writes the merged file to standard output rather than changing one of the input files.
Checkpoint: if command -v prints a different path, read that program's local documentation before relying on GNU-specific options such as --strip-trailing-cr.
2. Put the three roles in writing
Before running the merge, label the files by role. For this guide, use these placeholders:
$ MYFILE=/path/to/my-edits.txt
$ OLDFILE=/path/to/original.txt
$ YOURFILE=/path/to/other-edits.txt
$ printf 'my file: %s\noriginal: %s\nother file: %s\n' "$MYFILE" "$OLDFILE" "$YOURFILE"
my file: /path/to/my-edits.txt
original: /path/to/original.txt
other file: /path/to/other-edits.txt
Do not guess the original from timestamps. A three-way merge only makes sense when both edited files descend from the same original, or from content that is equivalent line by line. If the files are binary, or if you do not know their relationship, stop and establish that first.
Check readability without changing anything:
$ test -r "$MYFILE" && test -r "$OLDFILE" && test -r "$YOURFILE" && echo 'all three inputs are readable'
all three inputs are readable
3. Generate a merge into a new file
Do not redirect straight back to either input. Shell redirection truncates its destination before diff3 has finished, so a typo in the destination can destroy a useful version. Write to a new temporary output in the same directory, then inspect it:
$ OUT=/path/to/merged.txt.new
$ diff3 --merge "$MYFILE" "$OLDFILE" "$YOURFILE" > "$OUT"
$ status=$?
$ printf 'diff3 exit status: %s\n' "$status"
diff3 exit status: 1
$ sed -n '1,120p' "$OUT"
With GNU diff3, status 0 means the operation completed without conflicts, status 1 means conflicts were found, and status 2 means trouble such as an input or option error. Status 1 is not a failed merge: it is a request for you to review the conflict markers. The output file can still contain a useful combination of both sets of changes.
Use a separate status variable immediately after diff3. A later command such as sed would otherwise replace $?, hiding whether the merge found conflicts.
4. Resolve visible conflicts
For overlapping edits, --merge emits conflict markers in the new file. A typical section looks like this:
<<<<<<< my-edits.txt
line written in the first edited copy
=======
line written in the other edited copy
>>>>>>> other-edits.txt
The labels come from the input names unless you supply your own. The marker text is data for your review, not a resolution. Edit the new file and remove every marker, keeping the intended content:
$ rg -n '^(<<<<<<<|=======|>>>>>>>)' "$OUT"
12:<<<<<<< my-edits.txt
14:=======
16:>>>>>>> other-edits.txt
$ ${EDITOR:-vi} "$OUT"
$ if rg -n '^(<<<<<<<|=======|>>>>>>>)' "$OUT"; then
echo 'unresolved merge markers remain' >&2
exit 1
else
echo 'no merge markers remain'
fi
no merge markers remain
That check is a guard, not a proof that the text is correct. Read each changed section and run the file's normal tests, parser or lint command. If you chose the wrong original, the output may look tidy while still combining unrelated histories.
5. Give the inputs readable labels
When reviewing a conflict, descriptive labels are clearer than long temporary paths. Supply up to three labels in the same order as the operands:
$ diff3 --merge \
--label 'my-edits.txt' \
--label 'original.txt' \
--label 'other-edits.txt' \
"$MYFILE" "$OLDFILE" "$YOURFILE" > "$OUT"
$ printf 'exit status: %s\n' "$?"
exit status: 1
--label changes the names shown in the generated output. It does not rename files or alter how diff3 finds differences. Keep the operand order unchanged when adding labels.
6. Install the reviewed result
This is the point where you may overwrite a destination, so treat it as a destructive change. Keep the three source files and a backup until the application or test suite accepts the result. First compare the new file with the intended destination:
$ diff -u /path/to/merged.txt "$OUT"
$ cp --preserve=all /path/to/merged.txt /path/to/merged.txt.bak
$ mv "$OUT" /path/to/merged.txt
If the review exposes a mistake before the final move, remove only the new output and start again from the untouched inputs:
$ rm -f "$OUT"
After the move, recovery is the backup copy, provided you have not removed it:
$ cp --preserve=all /path/to/merged.txt.bak /path/to/merged.txt
Do not use sudo just because a merge reports conflicts. Elevated privileges do not resolve text. Use them only if the destination is deliberately protected and your normal change process requires root access; review the generated file first as an unprivileged user.
7. Diagnose the common traps
If diff3 exits 2, check the paths and permissions, then rerun without changing any destination:
$ ls -l "$MYFILE" "$OLDFILE" "$YOURFILE"
$ diff3 --merge "$MYFILE" "$OLDFILE" "$YOURFILE" > "$OUT"
$ printf 'exit status: %s\n' "$?"
exit status: 2
An exit status of 1 usually means overlapping edits, not a missing package. An empty or surprising result can mean that the original was placed in the wrong position. Remember: the first and third operands are the edited files, and the original is always second.
If one input is standard input, use - for that operand. Because standard input is consumed once, this is easiest to test interactively or from a prepared pipe, and it is less clear in a repeatable script than naming all three files. The -a option forces text treatment, while --strip-trailing-cr removes trailing carriage returns on input when line endings are the known source of noise. Use those options only when that input detail is understood, not as a general fix for unrelated conflicts.
Done means
- You verified GNU diff3 3.10 and identified the three file roles.
- The original and both edited inputs remain unchanged.
- You wrote the merge to a new file and captured the exit status immediately.
- You reviewed every conflict and removed all merge markers.
- You tested the resolved file before replacing a destination.
- A backup remains available if the final replacement needs to be undone.