Aller au contenu

Resolving merge conflicts

A three-way merge combines the changes of both branches automatically, as long as they do not touch the same part of the same file. When they do, Git cannot know which version to keep: the merge stops with a conflict, and you decide.

What is a conflict?

A merge conflict is Git's inability to reconcile the contents of one or more files between two branches. It typically happens when:

  • the same lines of a file were changed differently on both branches;
  • a file was modified on one branch and deleted on the other;
  • a file with the same name was added on both branches with different contents.

Changes to different files, or to different parts of the same file, merge automatically without a conflict.

Creating a conflict

The second line of the homelab README is an old to-do: TODO: describe the network layout. It gets edited twice:

  • On a branch docs, Sam replaces it with the actual description of the network.
  • On main, Sam rewords the to-do instead.
README.md on docs README.md on main
# Homelab # Homelab
The LAN uses 192.168.1.0/24 behind the ISP router. TODO: draw the network diagram.
%%{init: {"gitGraph": {"showBranches": true, "rotateCommitLabel": false, "mainBranchName": "main"}, "themeVariables": {"git0": "#43a047", "git1": "#1e88e5", "git2": "#fb8c00", "gitBranchLabel0": "#ffffff", "gitBranchLabel1": "#ffffff", "gitBranchLabel2": "#ffffff", "gitInv0": "#ffffff", "commitLabelFontSize": "13px"}}}%%
gitGraph TB:
    commit id: "d88fecf Merge branch 'backups'"
    branch docs
    commit id: "4f949e5 Describe the network"
    checkout main
    commit id: "68e3d70 Reword network to-do"
    merge docs id: "f9731d8 Merge docs (conflict resolved)" type: HIGHLIGHT

Merging docs into main stops halfway:

$ git switch main
Switched to branch 'main'
$ git merge docs
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
Line Meaning
Auto-merging README.md Git tried to combine the two versions of the file
CONFLICT (content): Merge conflict in README.md It failed: the same lines differ. The type in parentheses tells why: content (same lines changed), modify/delete, add/add (file created on both sides)...
Automatic merge failed; fix conflicts and then commit the result. No merge commit was created. The merge is in progress and waits for you.

Inspecting the conflict

git status lists the files to fix under Unmerged paths:

$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
    both modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")

both modified means that each branch changed the file. The hints already give the two ways out: fix the conflicts and commit, or abort the merge.

Conflict markers

Git wrote both versions into the file, between conflict markers. Open it in an editor, for example nano README.md:

# Homelab
<<<<<<< HEAD
TODO: draw the network diagram.
=======
The LAN uses 192.168.1.0/24 behind the ISP router.
>>>>>>> docs
Marker Meaning
<<<<<<< HEAD Start of the conflict. The lines below are the version of the current branch (main, where HEAD is)
======= Separator between the two versions
>>>>>>> docs End of the conflict. The lines above are the version of the branch being merged (docs)

Lines outside the markers, such as # Homelab, had no conflict and are already merged. A file can contain several conflict blocks.

Resolving the conflict

Resolving means editing the file into its final content and removing every marker. There are three options:

  • keep the HEAD version;
  • keep the docs version;
  • write a combination of both, or something new.

Here both lines are useful: the network is now described, and the diagram is still to be drawn. Sam keeps both and deletes the three marker lines:

# Homelab
The LAN uses 192.168.1.0/24 behind the ISP router.
TODO: draw the network diagram.

In nano, save with Ctrl + O (the letter O) then Enter, and exit with Ctrl + X.

Remove all the markers

Git does not check the content of the file. If a <<<<<<<, ======= or >>>>>>> line is left behind, it will be committed as part of the file. Search for <<<<<<< before marking the file as resolved.

Finishing the merge

Staging the file tells Git that its conflict is resolved:

$ git add README.md
$ git status
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
    modified:   README.md

Then commit to create the merge commit and conclude the merge:

$ git commit -m "Merge docs: describe the network"
[main f9731d8] Merge docs: describe the network

Without -m, Git opens the editor with a prepared message, Merge branch 'docs', followed by a comment listing the conflicted files. The history now looks like any three-way merge:

$ git log --oneline --graph -4
*   f9731d8 Merge docs: describe the network
|\
| * 4f949e5 Describe the network
* | 68e3d70 Reword network to-do
|/
*   d88fecf Merge branch 'backups'
|\

Running the merge again confirms that there is nothing left to bring in from docs, which can now be deleted:

$ git merge docs
Already up to date.
$ git branch -d docs
Deleted branch docs (was 4f949e5).
flowchart TD
    m["git merge docs"] --> c{"CONFLICT?"}
    c -- "No" --> done["Merge done"]
    c -- "Yes" --> s["git status<br/>lists the unmerged files"]
    s --> e["Edit each file:<br/>choose the content, remove the markers"]
    e --> a["git add &lt;file&gt;<br/>marks it as resolved"]
    a --> k["git commit<br/>creates the merge commit"]
    s -. "give up" .-> ab["git merge --abort<br/>back to the state before the merge"]

Aborting a merge: git merge --abort

If the conflict is not the right thing to solve now, cancel the merge. Git restores the files and the branch to their state before git merge:

$ git merge --abort
$ git status
On branch main
nothing to commit, working tree clean

--abort only works while the merge is in progress, before the final commit.

Preventing conflicts

Conflicts are normal, but prevention is better than cure:

  • Keep branches short-lived and focused on one purpose: the longer a branch lives, the more main drifts away from it.
  • Merge main into your branch regularly (git merge main from the branch), so that conflicts appear early, while they are small.
  • Avoid unrelated edits in the same files on different branches, such as reformatting a whole file while others work on it.
  • Communicate with the people working on the same files.

Summary

Command Result
git merge docs Starts the merge; stops with CONFLICT if the same lines differ
git status Lists the conflicted files under Unmerged paths
nano README.md Edit the file: keep the right content, remove the markers
git add README.md Mark the file as resolved
git commit Create the merge commit and conclude the merge
git merge --abort Cancel the merge and return to the state before it

Common mistakes

  • Leaving conflict markers in the file. Git commits them as ordinary text.
  • Forgetting git add after editing. The file stays in Unmerged paths and Git refuses to commit.
  • Mixing up the two sides. HEAD is the branch you are on (the destination); the name after >>>>>>> is the branch being merged.
  • Thinking the merge is over after CONFLICT. It is in progress until git commit or git merge --abort.

Hands-on labs

Three labs, from guided to more autonomous. Each one has its own setup, so you can do them in any order and repeat them as often as you like. Type the commands rather than pasting them, and read every output before moving on: the goal is to build reflexes, not to reach the end.

Lab 1: resolve a conflict

Objective: trigger a conflict, abort it once, then resolve it properly.

Prerequisites and initial state: Git installed. The setup creates ~/git-practice/conflict with two branches that changed the same line of services.md.

Setup:

mkdir -p ~/git-practice/conflict && cd ~/git-practice/conflict
git init -q -b main
git config user.name "Practice User"
git config user.email "[email protected]"
printf '# Services\n- nas: Samba shares\n' > services.md
git add . && git commit -q -m "Create services list"
git switch -q -c nfs
sed -i 's/Samba shares/NFS exports/' services.md
git commit -q -am "Switch NAS to NFS"
git switch -q main
sed -i 's/Samba shares/Samba shares (read-only)/' services.md
git commit -q -am "Make Samba read-only"

Tasks:

  1. Merge nfs into main. Identify the conflicted file with git status.
  2. Display the file and identify which line comes from which branch.
  3. Abort the merge and check that the working tree is clean.
  4. Merge again. This time, resolve the conflict so that the file reads:

    # Services
    - nas: Samba shares (read-only) and NFS exports
    
  5. Conclude the merge with the message Merge nfs, then delete the nfs branch.

Expected result and verification:

  • Task 1 prints CONFLICT (content): Merge conflict in services.md, and git status shows both modified: services.md.
  • After task 3, git status shows nothing to commit, working tree clean.
  • grep -c '<<<<<<<\|>>>>>>>\|=======' services.md prints 0.
  • git log --oneline --graph shows Merge nfs with two parents, and git branch lists only * main.
Solution
# 1. Trigger the conflict
git merge nfs                     # CONFLICT (content): Merge conflict in services.md
git status                        # both modified:   services.md

# 2. Read the markers
cat services.md
# <<<<<<< HEAD
# - nas: Samba shares (read-only)    <- main, the current branch
# =======
# - nas: NFS exports                 <- nfs, the branch being merged
# >>>>>>> nfs

# 3. Abort
git merge --abort
git status                        # nothing to commit, working tree clean

# 4. Merge again and resolve (or edit the file with nano)
git merge nfs
printf '# Services\n- nas: Samba shares (read-only) and NFS exports\n' > services.md
grep -c '<<<<<<<\|>>>>>>>\|=======' services.md   # 0: no markers left
git add services.md
git status                        # All conflicts fixed but you are still merging.

# 5. Conclude and clean up
git commit -m "Merge nfs"
git branch -d nfs
git log --oneline --graph
  • git merge --abort puts everything back as it was before git merge, so the merge can be retried later.
  • git add is what marks the conflict as resolved: Git does not look at the content.
  • Since the merge is a commit, git branch -d nfs accepts the deletion afterwards.

Clean up when you are done: rm -rf ~/git-practice/conflict.

Lab 2: two conflicts, two decisions

Objective: resolve a file with two separate conflicts, keeping one side in the first and the other side in the second.

Prerequisites and initial state: Git installed. The setup creates ~/git-practice/two-conflicts: main and migration both changed the DNS line and the web line of services.md, in different ways.

Setup:

mkdir -p ~/git-practice/two-conflicts && cd ~/git-practice/two-conflicts
git init -q -b main
git config user.name "Practice User"
git config user.email "[email protected]"
printf '# Services\n- pi-dns: Pi-hole\n- nas: Samba shares\n- mon01: Grafana\n- backup01: rsync\n- media01: Jellyfin\n- web01: Nginx\n' > services.md
git add . && git commit -q -m "Create services list"
git switch -q -c migration
sed -i 's/Pi-hole/AdGuard Home/; s/Nginx/Caddy/' services.md
git commit -q -am "Migrate DNS and web services"
git switch -q main
sed -i 's/Pi-hole/Pi-hole v6/; s/Nginx/Nginx 1.28/' services.md
git commit -q -am "Upgrade DNS and web services"

Tasks:

  1. Merge migration into main. How many conflict blocks does services.md contain?
  2. Resolve them so that the DNS server keeps main's version and the web server takes migration's version. The lines without conflict must stay as they are.
  3. Check that no marker is left, then conclude the merge without opening an editor.
  4. Display the graph, and show what the merge commit changed compared to each parent.

Expected result and verification:

  • Task 1 reports a conflict in services.md, with two blocks between <<<<<<< and >>>>>>>.
  • After task 2, services.md reads:

    # Services
    - pi-dns: Pi-hole v6
    - nas: Samba shares
    - mon01: Grafana
    - backup01: rsync
    - media01: Jellyfin
    - web01: Caddy
    
  • Task 3: grep -c '^<<<<<<<\|^=======\|^>>>>>>>' services.md prints 0, and git log -1 shows Merge branch 'migration'.

  • Task 4: git diff HEAD~1 HEAD shows only the web line changing; git diff migration HEAD shows only the DNS line.
Solution
# 1. Two separate conflicts in the same file
git merge migration                       # CONFLICT (content): Merge conflict in services.md
grep -c '^<<<<<<<' services.md            # 2

# 2. Resolve each block on its own (or edit the file with nano)
cat services.md
# <<<<<<< HEAD
# - pi-dns: Pi-hole v6           <- keep this one
# =======
# - pi-dns: AdGuard Home
# >>>>>>> migration
# ...
# <<<<<<< HEAD
# - web01: Nginx 1.28
# =======
# - web01: Caddy                 <- keep this one
# >>>>>>> migration
printf '# Services\n- pi-dns: Pi-hole v6\n- nas: Samba shares\n- mon01: Grafana\n- backup01: rsync\n- media01: Jellyfin\n- web01: Caddy\n' > services.md

# 3. Check, mark as resolved, conclude
grep -c '^<<<<<<<\|^=======\|^>>>>>>>' services.md   # 0
git add services.md
git commit --no-edit                      # default message: Merge branch 'migration'

# 4. The merge commit against each parent
git log --oneline --graph
git diff HEAD~1 HEAD                      # from main's side: only the web line changes
git diff migration HEAD                   # from migration's side: only the DNS line changes
  • Git marks each conflicting area separately, and keeps the unchanged lines between them out of the markers. When two conflicting areas are only a few lines apart, Git reports them as a single, larger block: resolve it the same way, line by line.
  • "Ours" and "theirs" are decided block by block: one resolution can mix both sides.
  • git commit --no-edit concludes a merge with the message Git prepared, like git merge --no-edit does for a merge without conflict.

Clean up when you are done: rm -rf ~/git-practice/two-conflicts.

Lab 3: two admins, one script

Objective: play two administrators who each change the same line of a script on their own branch, merge both, and check that the result still runs.

Prerequisites and initial state: Git installed. The setup creates ~/git-practice/script-conflict with a small backup script on main.

Setup:

mkdir -p ~/git-practice/script-conflict && cd ~/git-practice/script-conflict
git init -q -b main
git config user.name "Practice User"
git config user.email "[email protected]"
printf '#!/bin/sh\nDEST=/mnt/backup\necho "Backing up /srv/data to $DEST"\n' > backup.sh
git add . && git commit -q -m "Add backup script"
sh backup.sh

Tasks:

  1. Create two branches from main: usb and nas.
  2. Play the first admin: on usb, change the destination to /mnt/usb, and commit with the message Back up to the USB drive.
  3. Play the second admin: on nas, change the destination to /mnt/nas, and commit with the message Back up to the NAS.
  4. Back on main, merge usb, then nas. Which merge goes smoothly, and which one stops?
  5. Before resolving, run sh backup.sh. What does a file with conflict markers do to a script?
  6. Resolve the conflict as you prefer (one destination, or both, for example DEST="/mnt/usb /mnt/nas"), run the script again, and conclude the merge.
  7. Delete both branches.

Expected result and verification:

  • Task 4: merging usb is a fast-forward; merging nas fails with CONFLICT (content): Merge conflict in backup.sh.
  • Task 5: the script fails with a syntax error on the <<<<<<< line.
  • Task 6: sh backup.sh prints Backing up /srv/data to ... with your chosen destination, and git log --oneline --graph shows the merge commit.
  • Task 7: git branch lists only * main.
Solution
# 1. Two branches from the same commit
git branch usb
git branch nas

# 2. First admin
git switch usb
sed -i 's|DEST=/mnt/backup|DEST=/mnt/usb|' backup.sh
git commit -am "Back up to the USB drive"

# 3. Second admin
git switch nas
sed -i 's|DEST=/mnt/backup|DEST=/mnt/nas|' backup.sh
git commit -am "Back up to the NAS"

# 4. The first merge is a fast-forward, the second one conflicts
git switch main
git merge usb                     # Fast-forward
git merge nas                     # CONFLICT (content): Merge conflict in backup.sh

# 5. Markers break the script
sh backup.sh                      # syntax error near unexpected token `<<<' (wording varies by shell)

# 6. Resolve, test, conclude
printf '#!/bin/sh\nDEST="/mnt/usb /mnt/nas"\necho "Backing up /srv/data to $DEST"\n' > backup.sh
sh backup.sh                      # Backing up /srv/data to /mnt/usb /mnt/nas
git add backup.sh
git commit --no-edit              # Merge branch 'nas'
git log --oneline --graph

# 7. Clean up
git branch -d usb nas
  • Whoever merges first gets a fast-forward; whoever merges second meets the conflict. Merging often keeps these conflicts small.
  • Conflict markers are written into the file itself. A configuration file or a script left with markers is broken: always test the result before concluding.
  • In printf, the single quotes keep $DEST as text, so it ends up in the script instead of being expanded by your shell.

Clean up when you are done: rm -rf ~/git-practice/script-conflict.