Skip to content
amuraviov.com
Go back

Git Conflicts Explained: A Practical Walkthrough for PHP Developers

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.

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:

So, the most important question: what are our options for resolving the conflict?

There are 4 main options:

  1. Keep the current version
  2. Keep the incoming version
  3. Manually create a third version
  4. 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:

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:

We’ll take a closer look at each of these in upcoming articles.


Share this post on:

Next Post
PHP Reflection for Runtime Config: A Legacy Pattern, Its Limits, and How We Evolved It