Git conflicts make you panic a little, right up until you learn to understand what’s actually happening. Why did the conflict happen? What did I do wrong? Am I going to lose my changes? And how do I move forward from here?
In this article I’ll deliberately create a conflict, break it down from the inside, and look at what happens to the Git history before, during, and after a merge. The goal is that the next time you run into CONFLICT, you won’t be clicking buttons at random just to escape the situation as fast as possible - instead you’ll calmly figure out what’s going on and make the right call.
What is a Git conflict?
First, Git lets us, as developers, work independently on the same project in separate branches. As long as the changes touch different files, or different, independent parts of the same file, Git is usually able to merge them automatically. But what happens if they touch the same part of a file? Let’s find out.
Setting up the repository
For this example, let’s create a repository with a single PHP file:
mkdir git-conflict-repo
cd git-conflict-repo
git init
Let’s create a simple index.php file:
cat > index.php << 'EOF'
<?php
echo "Hello!";
EOF
Next, we add the file to the index and make our first commit:
git add index.php
git commit -m "Initial commit"
Let’s check our history:
git log --oneline
We get something like:
a1b2c3d (HEAD -> main) Initial commit
So, right now the repository has one commit - this is our shared starting point.
Let’s create a new branch, feature/branch1:
git checkout -b feature/branch1
Let’s change our line in the file:
cat > index.php << 'EOF'
<?php
echo "Hello! This change is from branch 1.";
EOF
Let’s commit the change and check the history:
git add .
git commit -m "Update echo text in branch 1"
git log --oneline
Let’s create one more branch, feature/branch2:
First, let’s go back to the main branch, and create the second branch from there, so we get a single common starting point.
git checkout main
git checkout -b feature/branch2
Let’s change the same line, but slightly differently:
cat > index.php << 'EOF'
<?php
echo "Hello! This change is from branch 2.";
EOF
Let’s commit the changes and check the history:
git add .
git commit -m "Update echo text in branch 2"
git log --oneline
Now we have two branches where we changed the same line in the same file. This exact situation is what leads to a conflict when we merge them later.
Let’s merge the first branch into main, simulating the work of the first developer who finished their task and decided to merge the history:
git checkout main
git merge feature/branch1
# Let's check the file right away
cat index.php
Now main contains the change from feature/branch1. Let’s try merging the second developer’s branch.
git merge feature/branch2
And this is where Git stops us:
Auto-merging index.php
CONFLICT (content): Merge conflict in index.php
Automatic merge failed; fix conflicts and then commit the result.
This happened precisely because main now already contains the version from the first branch, while feature/branch2 contains a different version of the same line. Git sees that both branches changed the same line relative to the common ancestor. Git cannot guess which version should be kept, so it needs the developer to make that decision.
In real work, this most often comes up when running git pull, because that’s when we’re pulling in someone else’s changes into a branch where we already have local changes of our own. Under the hood, git pull usually does exactly two things:
git fetch
# This downloads all the new commits from the remote server.
# It also updates the branch information.
git merge
# This merges the downloaded changes into our current local branch.
Let’s continue and check the state of the repository:
git status
Git tells us the file is in a conflicted state:
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: index.php
Let’s see what’s actually stored inside the file:
cat index.php
Inside, we find special conflict markers:
<?php
<<<<<<< HEAD
echo "Hello! This change is from branch 1.";
=======
echo "Hello! This change is from branch 2.";
>>>>>>> feature/branch2
Let’s briefly break down what’s written here.
<<<<<<< HEAD- below this is the version from the current branch, main=======- the line separating the two versions>>>>>>> feature/branch2- above this line is the version we’re trying to merge in
So, Git started the merge, hit the contested spot, and stopped. There’s no new merge commit yet. Let’s look at the current history as a graph:
git log --oneline --graph --all
We get a picture like this:
* e5f6a7b (feature/branch2) Update echo text in branch 2
| * d4e5f6a (HEAD -> main, feature/branch1) Update echo text in branch 1
|/
* a1b2c3d Initial commit
This means the history has diverged, and the repository is now in an intermediate state:
- the merge has already started,
- a conflict was found,
- Git is waiting for us to fix the file and finish the merge with a final merge commit.
So, the most important question: what are our options for resolving the conflict?
There are 4 main options:
- Keep the current version
- Keep the incoming version
- Manually create a third version
- Abort the merge
Most likely, both edits matter, so more often than not the right call isn’t to pick one side over the other, but to combine them.
Let’s replace the file’s contents with the final version:
cat > index.php << 'EOF'
<?php
echo "Hello! This change is from branch 1 and branch 2.";
EOF
This way we’ve replaced the line with the final version and also removed the conflict markers. If you leave those markers in and run git add, Git will consider the file resolved, but the code will still be left with garbage in it.
Finishing the merge
Let’s add the file to the index and check the status:
git add index.php
git status
We get the following message from Git:
All conflicts fixed but you are still merging.
This means we’ve resolved the conflict, but the merge itself isn’t done yet. Let’s finish it with a commit:
git commit -m "Merge feature/branch2 into main"
After that, the merge is complete and Git shows us:
nothing to commit, working tree clean
How did the history change after the merge? Let’s look at the graph with git log --oneline --graph --all:
* 9c8b7a6 (HEAD -> main) Merge feature/branch2 into main
|\
| * e5f6a7b (feature/branch2) Update echo text in branch 2
* | d4e5f6a (feature/branch1) Update echo text in branch 1
|/
* a1b2c3d Initial commit
A new merge commit has appeared, whose first parent is the current state of main, and whose second parent is the commit from feature/branch2.
This exact merge commit records the result of the merge. In our case, it stores neither the first nor the second version of the line, but the final version we chose ourselves.
Can you reduce the chance of conflicts, or avoid them altogether?
You can’t avoid conflicts completely. If several people work on the same project, sooner or later Git will run into changes it can’t merge automatically. But you can noticeably reduce the likelihood of conflicts:
- One common cause of large conflicts is a branch living separately from the main development line for too long. So, it’s worth regularly pulling in changes from the base branch.
- If two people work at the same time and change the same module, the same class, or the same area of business logic, the chance of a conflict rises sharply. So sometimes a simple question to your colleague - are you currently working on the same component? - will save you time sorting out future conflicts.
- Make smaller, less sprawling changes. This way each branch lives for less time and diverges less from the base.
A conflict isn’t a breakage - it’s simply the point where Git honestly hands its decision over to a human.
Thanks for reading! In this article we walked through the basic scenario: two branches, one file, one conflict, and a regular merge. But Git has other situations that often raise questions too:
git revert- how to undo changes with a new commit, without rewriting history;git stash- how to temporarily shelve unfinished work;git cherry-pick- how to move a single commit from one branch to another;git rebase- how to replay commits on top of another branch, and why that can also cause conflicts.
We’ll take a closer look at each of these in upcoming articles.