Skip to content

Conflicts step by step

  • Lesson 10
  • 40 min
  • Module 4: Resolve and recover
Lesson 10 of 15 · Git and Team Development EssentialsConflicts step by step

What you will be able to do

Finish a merge when you and a teammate changed the same line. After this lesson, you can read both changes and keep what each person meant. You can test the result and complete the merge, or stop it and start again.

Before you startWhat to know and to have readyAbout 40 minutes

What you need to know

What you need on your computer

  • The practice repositories in ~/projects/nextia-learning-labs. You cloned them in Pull-request sequence: Set up the practice lab. If you do not have them, get them once:

    Terminal window
    cd ~/projects
    git clone https://github.com/Nextia-Intelligence/nextia-learning-labs.git
  • Make the start state for this module. --reset deletes the old c14-lab folder and makes it again. It changes nothing else. Run the same command at any time to start this module again.

    Terminal
    cd ~/projects
    sh nextia-learning-labs/C14/setup.sh m04 --reset
    Output
    Ready: /Users/amira/projects/c14-lab
    ticket-notes your clone
    sam-notes Sam's clone
    team-remote.git the shared remote
    Next: cd c14-lab/ticket-notes
    To start this scenario again: sh nextia-learning-labs/C14/setup.sh m04 --reset
  • Go into your clone with cd c14-lab/ticket-notes. git status shows Your branch is ahead of 'origin/main' by 1 commit.

  • The script sets pull.ff only and merge.conflictStyle zdiff3 in the two clones. Your own commits use the name from Set up Git: Step 2: tell Git who you are.

  • The script made its commits with fixed hashes, so they are the same as in this lesson. The commits that you make have different hashes. Use the ones from your own output.

In Module 2, Amira merged Sam’s work, and Git did it alone. Git compares the two sides line by line. If each person changed different lines, Git takes both changes.

Git also uses a third version: the base, the last commit that both sides have. Module 3 called it the merge base. The base shows what each person changed. If both people changed the same line since the base, Git cannot know which version is correct. This is a conflict. Git stops the merge, writes both versions into the file, and waits for you to decide.

A conflict is not an error, and nothing is lost. It is a question that only a person can answer: what did each change mean, and what must the line say now?

In a merge, the two sides have names:

  • Ours is your current branch, the place where HEAD points. In this lesson, that is Amira’s main.
  • Theirs is the commit that you merge in. In this lesson, that is Sam’s commit from origin/main.

The setup script set merge.conflictStyle zdiff3 in the two clones. With this setting, Git also shows the base lines between the two sides. You can then see what each person changed, not only the result. Set it for all your repositories:

Terminal
git config --global merge.conflictStyle zdiff3

Amira added an action to ticket 102: Sent a new reset link. At the same time, Sam added the cause of the problem: The email was in the spam folder. Sam pushed first. Amira tries to push:

Terminal (Amira)
git push
Output
To /Users/amira/projects/c14-lab/team-remote.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to '/Users/amira/projects/c14-lab/team-remote.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

She pulls. Because of pull.ff only, Git only fetches, then stops:

Terminal (Amira)
git pull
Output (first and last lines)
From /Users/amira/projects/c14-lab/team-remote
05e00df..7e16da6 main -> origin/main
hint: Diverging branches can't be fast-forwarded, you need to either:
...
fatal: Not possible to fast-forward, aborting.

The histories have diverged. She decides to merge, as in Module 2:

Terminal (Amira)
git pull --no-rebase
Output
Auto-merging tickets.txt
CONFLICT (content): Merge conflict in tickets.txt
Automatic merge failed; fix conflicts and then commit the result.

The merge has started, but it is not complete. Git did not make a merge commit. git status shows what is still open:

Terminal (Amira)
git status
Output
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)
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: tickets.txt
no changes added to commit (use "git add" and/or "git commit -a")

both modified means that both sides changed tickets.txt in the same place. An unmerged path is a file with a conflict that you did not resolve yet. The output also gives the two ways forward: fix the conflict and commit, or abort the merge.

Amira opens tickets.txt in her editor:

tickets.txt
Ticket 101: printer offline. Restarted the spooler.
<<<<<<< HEAD
Ticket 102: password reset email not received. Sent a new reset link.
||||||| 05e00df
Ticket 102: password reset email not received.
=======
Ticket 102: password reset email not received. The email was in the spam folder.
>>>>>>> 7e16da60a8af11e14488d6be51aeff28dadcfc1d
Ticket 103: shared drive is slow. Moved the user to the new server.
Ticket 104: laptop will not charge. Replaced the power adapter.

Git wrote four marker lines around the conflict. The lines outside the markers merged without a problem.

  • <<<<<<< HEAD starts ours: Amira’s version of the line.
  • ||||||| 05e00df starts the base: the line in 05e00df, before either change.
  • ======= starts theirs: Sam’s version of the line.
  • >>>>>>> 7e16da6… ends the conflict. The hash is Sam’s commit.

Compare each side with the base. Amira added Sent a new reset link. Sam added The email was in the spam folder. Without the base, it is harder to see that each person only added text.

The text of a line shows what changed. The commit message tells why. git log --merge lists the commits on each side that changed the files with conflicts:

Terminal (Amira)
git log --merge --oneline
Output
9e1b9df (HEAD -> main) Add the action for ticket 102
7e16da6 (origin/main, origin/HEAD) Add the cause of ticket 102

To see Sam’s full change, she uses git show with his hash:

Terminal (Amira)
git show 7e16da6
Output
commit 7e16da60a8af11e14488d6be51aeff28dadcfc1d
Author: Sam Okafor <sam@example.com>
Date: Tue Oct 6 09:10:00 2026 -0400
Add the cause of ticket 102
diff --git a/tickets.txt b/tickets.txt
index 76ca965..ae59119 100644
--- a/tickets.txt
+++ b/tickets.txt
@@ -1,4 +1,4 @@
Ticket 101: printer offline. Restarted the spooler.
Ticket 102: password reset email not received.
Ticket 102: password reset email not received. The email was in the spam folder.
Ticket 103: shared drive is slow. Moved the user to the new server.
Ticket 104: laptop will not charge. Replaced the power adapter.

Sam recorded the cause. Amira recorded the action. Both facts are true and useful, so the correct line keeps both. If she is not sure what a change means, she asks the author before she decides.

To resolve a conflict, you edit the file until it has the correct text and no markers. Amira replaces the seven lines from <<<<<<< to >>>>>>> with one line. The cause comes first, then the action:

tickets.txt
Ticket 101: printer offline. Restarted the spooler.
Ticket 102: password reset email not received. The email was in the spam folder. Sent a new reset link.
Ticket 103: shared drive is slow. Moved the user to the new server.
Ticket 104: laptop will not charge. Replaced the power adapter.

She saves the file. Then she checks for markers that she forgot. git diff --check reports leftover conflict markers. If it finds none, it prints nothing:

Terminal (Amira)
git diff --check

Next, she runs the team’s test. check.sh is a small script in the repository. It checks that each line of tickets.txt starts with Ticket <number>: :

Terminal (Amira)
sh check.sh
Output
PASS: 4 ticket lines checked

A file can merge with no markers and still be wrong. The test is how you find out before your team does.

Now she tells Git that the conflict is resolved. git add marks the file as resolved:

Terminal (Amira)
git add tickets.txt
git status
Output
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: tickets.txt

She completes the merge with a commit. --no-edit keeps the message that Git prepared:

Terminal (Amira)
git commit --no-edit
git log --oneline --graph -4
Output
[main 5c84cbf] Merge branch 'main' of /Users/amira/projects/c14-lab/team-remote
* 5c84cbf (HEAD -> main) Merge branch 'main' of /Users/amira/projects/c14-lab/team-remote
|\
| * 7e16da6 (origin/main, origin/HEAD) Add the cause of ticket 102
* | 9e1b9df Add the action for ticket 102
|/
* 05e00df Add ticket 104

The merge commit has two parents: her commit and Sam’s commit. Your merge commit has a different hash. Now main contains Sam’s work, so the push is safe:

Terminal (Amira)
git push
Output
To /Users/amira/projects/c14-lab/team-remote.git
7e16da6..5c84cbf main -> main

Sometimes you cannot finish a conflict now. You may need to ask the other author, or you may have made a mess of the file. git merge --abort stops the merge. It puts your branch, the staging area and the files back to the state before the merge started.

Amira tried this before she resolved the conflict. She ran it while git status still showed You have unmerged paths:

Terminal (Amira)
git merge --abort
git status
git log --oneline -1
Output
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)
nothing to commit, working tree clean
9e1b9df (HEAD -> main) Add the action for ticket 102

main is back on her own commit, and the file has no markers. Sam’s commit is still in origin/main, because the fetch is not undone. When she is ready, she runs git pull --no-rebase again, and the same conflict appears.

First, do the worked example in your own ticket-notes, from git push to the last git push. Then make a second conflict yourself. You play Sam in sam-notes and Amira in ticket-notes, and you both change the line for ticket 103.

  1. Go to Sam’s clone, and get the merge that you pushed:

    Terminal window
    cd ~/projects/c14-lab/sam-notes
    git pull

    The output includes Fast-forward.

  2. In your editor, open tickets.txt in sam-notes. Change the ticket 103 line to this text, and save:

    Ticket 103: shared drive is slow. Moved the user to the new server. The user confirmed the fix.
  3. Commit and push as Sam:

    Terminal window
    git commit -am "Close ticket 103"
    git push
  4. Go to your clone. Change the same line in ticket-notes/tickets.txt to this text, and save:

    Ticket 103: shared drive is slow. Moved the user to the new server. Ticket raised by Finance.
  5. Commit, then try to push:

    Terminal window
    cd ~/projects/c14-lab/ticket-notes
    git commit -am "Add who raised ticket 103"
    git push

    The push shows ! [rejected] main -> main (fetch first).

  6. Merge Sam’s work:

    Terminal window
    git pull --no-rebase

    The output shows CONFLICT (content): Merge conflict in tickets.txt.

  7. Run git status. tickets.txt shows as both modified.

  8. Run git log --merge --oneline. The output shows your commit and Sam’s commit Close ticket 103.

  9. Open tickets.txt. Find the base line between ||||||| and =======. Compare each side with it.

  10. Replace the whole block, from <<<<<<< to >>>>>>>, with one line that keeps both facts:

    Ticket 103: shared drive is slow. Moved the user to the new server. Ticket raised by Finance. The user confirmed the fix.
  11. Check and test:

    Terminal window
    git diff --check
    sh check.sh

    git diff --check prints nothing. check.sh prints PASS: 4 ticket lines checked.

  12. Mark the file as resolved, complete the merge and push:

    Terminal window
    git add tickets.txt
    git commit --no-edit
    git push
  13. Run git log --oneline --graph -4. The top commit is a merge with two parents: Add who raised ticket 103 and Close ticket 103.

Not every conflict needs both sides. Make this conflict, then decide.

  1. In sam-notes, run git pull. Change the last line of README.md to Notes about support tickets for the help desk. Use one line for each ticket. Commit with the message Explain the ticket line format in the README, and push.
  2. In ticket-notes, change the same line to Notes about support tickets for the help desk. Write each ticket on one line. Commit with the message Describe the ticket format.
  3. Run git pull --no-rebase, and resolve the conflict in README.md. Finish the merge and push.

Before you edit, answer: what did each person want? Must the result keep both sides?

Show a model answer

The conflict looks like this. Your two hashes are different:

README.md
# Ticket notes
<<<<<<< HEAD
Notes about support tickets for the help desk. Write each ticket on one line.
||||||| 928f2ed
Notes about support tickets for the help desk.
=======
Notes about support tickets for the help desk. Use one line for each ticket.
>>>>>>> 555dcc3fcf504b021d15a76f90f351179bc04655

git log --merge --oneline shows the two messages: Describe the ticket format and Explain the ticket line format in the README. Both people wanted the same thing, and wrote it in different words. If you keep both sides, the README says the same rule twice. So keep one side. Here, keep Sam’s line, because his version is already on the remote:

README.md
# Ticket notes
Notes about support tickets for the help desk. Use one line for each ticket.

Then finish as before:

Terminal window
git diff --check
sh check.sh
git add README.md
git commit --no-edit
git push

git diff HEAD^1 HEAD shows how the merge changed your side: your sentence went out, and Sam’s sentence came in. Your sentence is still in your commit Describe the ticket format, so the history keeps both.

Common mistake

Conflict markers in a commit

Symptom: after a merge, sh check.sh fails and lists the marker lines:

Output
FAIL: these lines in tickets.txt do not start with 'Ticket <number>: '
2:<<<<<<< HEAD
4:||||||| 05e00df
6:=======
8:>>>>>>> 7e16da60a8af11e14488d6be51aeff28dadcfc1d

git diff --check HEAD^ HEAD shows tickets.txt:2: leftover conflict marker, and the same for lines 4, 6 and 8.

Cause: you ran git add and git commit before you removed the markers. git add does not check the contents of a file. It only tells Git that you finished.

Fix: do not change the merge commit. Fix the file in a new commit.

  1. Open the file, and edit the block until it has the correct text and no markers.

  2. Run git diff --check and sh check.sh. The test must print PASS.

  3. Commit the fix:

    Terminal window
    git commit -am "Remove leftover conflict markers"
    Output
    [main 9a2eb8f] Remove leftover conflict markers
    1 file changed, 1 insertion(+), 7 deletions(-)
  4. Push. Next time, run git diff --check and the test before git add.

Common mistake

Committing is not possible because you have unmerged files

Symptom: you edited the file and removed the markers. git commit --no-edit shows:

Output
error: Committing is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.
U tickets.txt

Cause: Git does not read the file to decide if you finished. It waits for git add. U means that the file is still unmerged.

Fix: run git add tickets.txt, then git commit --no-edit again. git status shows All conflicts fixed but you are still merging when the file is marked as resolved.

Check your understanding

Predict: Amira's commit changes line 2 of tickets.txt. Sam's commit adds a new line at the end of the file. Does git pull --no-rebase stop with a conflict?

Show the answer

No. The two changes are in different places, and line 2 and the last line are not next to each other. Git takes both changes and makes the merge commit. A conflict happens only when both sides changed the same lines, or lines next to each other, since the base. Even so, run the test after every merge. A clean merge can still give a wrong result.

Check your understanding

Practical check: after git pull --no-rebase stops with a conflict, list the commands, in order, that complete the merge safely. Then name the command that cancels the merge instead.

Show the answer
  1. git status, to find the files that show both modified.
  2. git log --merge --oneline and git show <hash>, to see what each person changed and why.
  3. Edit each file, keep the correct text and remove all markers.
  4. git diff --check (prints nothing) and the test, sh check.sh (prints PASS).
  5. git add <file> for each resolved file.
  6. git commit --no-edit, then git push.

git merge --abort cancels the merge and returns to the state before it started.

Recap

  • A conflict happens when both sides changed the same lines since the base. Git stops and lets you decide.
  • In a merge, ours is your current branch (HEAD) and theirs is the commit that you merge in. With zdiff3, the base shows between them.
  • Read the commit messages with git log --merge and git show to learn what each person meant. Then keep both changes, or only the correct one.
  • Before you commit, run git diff --check and the tests. Then git add marks the file as resolved, and git commit completes the merge.
  • git merge --abort stops a merge and returns to the state before it started.

Next: Undo safely