Good to see you again. In the previous lesson, you used a short-lived branch to isolate a documentation change, reviewed it from main, and merged it cleanly. That was the easy integration path: Git could combine the histories without asking for help.
This lesson covers the other path. You will deliberately create a conflict, inspect Git’s conflict markers, produce a final version that incorporates both changes, verify it, and finish the merge. This is a routine collaborative skill: a conflict is not evidence that Git has lost work; it is Git stopping before it makes an ambiguous decision for you.
What a merge conflict actually means
Git performs a three-way merge by comparing:
- the common ancestor, where the two branches last agreed;
- the version on the branch currently checked out, called
HEAD; - the version on the branch being merged in.
When the two branches change different parts of a file, Git can normally combine them. A conflict occurs when the changes overlap in a way that makes the intended final result uncertain—for example, when two people rewrite the same configuration setting differently.
The important distinction is this:
A conflict is a decision problem, not data loss.
Both committed versions remain available. Git writes both alternatives into the working copy so that you can decide what the merged file should mean. Your job is not mechanically to “accept ours” or “accept theirs.” It is to understand the purpose of each change and create the correct final state.
The labels ours and theirs can be misleading. During a merge:
- ours is the branch currently checked out, represented by
HEAD; - theirs is the branch named in your
git mergecommand.
They do not necessarily mean “my work” and “someone else’s work.” If you merge main into a feature branch to update a pull request, HEAD refers to the feature branch; if you merge a feature branch into main, HEAD refers to main.
Git - Basic Branching and Merging
Read the official Git book’s explanation of a failed merge and its completion. It establishes the command-line workflow you will use in the lab: inspect, edit deliberately, stage each resolved file, and commit the integration.
In the “Basic Merge Conflicts” section, read from the explanation of overlapping edits through the example resolution. Focus first on the conflict report: Git identifies the file but does not choose its final content. Then study the marker block and the explanation immediately following it. Finally, read the completion guidance beginning after resolution, noting that staging marks a file resolved but git commit concludes the merge.
Reading the conflict markers
Suppose main and a feature branch both edit the same line. Git may put this into the affected file:
<<<<<<< HEAD
The service check uses the default endpoint and a configurable TIMEOUT_SECONDS timeout.
=======
The service check uses the configured SERVICE_URL endpoint and a 10-second timeout.
>>>>>>> feat/configurable-endpoint
This is ordinary text placed in the file by Git:
| Marker or region | Meaning in this merge |
|---|---|
<<<<<<< HEAD | Start of the version from the current branch |
Text above ======= | Current branch’s competing content |
======= | Separator |
Text below ======= | Incoming branch’s competing content |
>>>>>>> feat/configurable-endpoint | End of the incoming version, labelled with its source branch |
Neither version is automatically “right.” Here, the change on main makes the timeout configurable, while the feature branch makes the endpoint configurable. Keeping either block unchanged would silently discard a requirement.
A correct integrated result is a third version:
The service check uses the configured SERVICE_URL endpoint and a configurable TIMEOUT_SECONDS timeout.
The final file must contain no conflict-marker lines. It may retain one side, the other side, both lines, or a newly written combination. The decision comes from the intended behavior, not from the convenience of an editor button.
Lab: resolve a controlled conflict without discarding either change
Continue in the service-check repository. This lab creates an operations note that describes two future script parameters. The file is simple, so you can focus on Git’s state and the resolution decision.
1. Start from a clean main branch
cd ~/devops-labs/service-check
git switch main
git status
git log --oneline --decorate -n 5
git status should report a clean working tree. Resolve or commit unrelated work before beginning a merge. A clean starting point prevents unrelated local edits from becoming mixed into the resolution.
Create a baseline document on main:
mkdir -p docs
cat > docs/operations-notes.md <<'EOF'
# Service check operational notes
## Default behavior
The service check uses the default endpoint and a 10-second timeout.
EOF
git add docs/operations-notes.md
git commit -m "docs: add service check operation notes"
This commit is the common ancestor for the two later edits.
2. Make the endpoint change on a feature branch
Create a branch and change the sentence so the endpoint can be set through SERVICE_URL:
git switch -c feat/configurable-endpoint
cat > docs/operations-notes.md <<'EOF'
# Service check operational notes
## Default behavior
The service check uses the configured SERVICE_URL endpoint and a 10-second timeout.
EOF
git diff
git add docs/operations-notes.md
git commit -m "docs: describe configurable service endpoint"
The branch now holds the endpoint requirement. Confirm it:
git log --oneline --decorate --graph --all -n 8
cat docs/operations-notes.md
3. Make the timeout change on main
Switch back to main. Replace the same sentence, but this time make the timeout configurable:
git switch main
cat > docs/operations-notes.md <<'EOF'
# Service check operational notes
## Default behavior
The service check uses the default endpoint and a configurable TIMEOUT_SECONDS timeout.
EOF
git diff
git add docs/operations-notes.md
git commit -m "docs: describe configurable timeout"
At this point, both branches changed the same original line in different ways. Inspect the divergent history:
git log --oneline --decorate --graph --all -n 10
main contains the timeout change; feat/configurable-endpoint contains the endpoint change. Neither commit has disappeared.
4. Begin the merge and inspect the conflict
While still on main, merge the feature branch:
git merge feat/configurable-endpoint
Git should report a content conflict in docs/operations-notes.md and state that automatic merging failed. This is expected.
First, ask Git for its authoritative view of the repository state:
git status
Look for:
- a message that a merge is in progress;
- an Unmerged paths section;
both modified: docs/operations-notes.md;- guidance to fix conflicts and run
git commit.
Now inspect the file:
cat docs/operations-notes.md
You should see the conflict block described earlier. Notice that Git has preserved both versions in the file. Do not delete a block until you know why it exists.
If the surrounding context were larger or the file more complex, compare each committed version directly:
git show main:docs/operations-notes.md
git show feat/configurable-endpoint:docs/operations-notes.md
These commands read the committed snapshots, not the marker-filled working copy. In a production repository, this is often useful when you need to separate the actual intent of the changes from a large conflict hunk.
Resolve by preserving the requirements
Before editing, state the requirements in plain language:
- The service check needs a configurable endpoint through
SERVICE_URL. - The service check needs a configurable timeout through
TIMEOUT_SECONDS.
The merged note must say both. Replace the entire conflicted file with the final version:
cat > docs/operations-notes.md <<'EOF'
# Service check operational notes
## Default behavior
The service check uses the configured SERVICE_URL endpoint and a configurable TIMEOUT_SECONDS timeout.
EOF
Now review the resolution before staging it:
git diff
git diff --check
git grep -nE '^(<<<<<<<|=======|>>>>>>>)' || true
git diff shows what the working copy will contribute to the merge. git diff --check is valuable for configuration, scripts, and infrastructure files because it detects whitespace issues and warns about leftover conflict markers. The git grep command gives a direct final check for marker lines.
For an application or automation change, run its relevant validation now—perhaps a shell syntax check, unit test, container build, Terraform validation, or a targeted health check. A clean Git resolution is not necessarily a correct operational result. Here the file is documentation, so carefully reviewing the final sentence is the meaningful validation.
Stage, verify, and finish the merge
Once you have verified the result, stage the specific resolved file:
git add docs/operations-notes.md
git status
git diff --staged
git add has a special meaning during a conflict: it tells Git that the current file content is your deliberate resolution. It does not mean Git independently confirmed that the merged behavior is correct.
After staging, git status should say that all conflicts are fixed but you are still merging. The merge is not complete until you commit:
git commit -m "merge: combine endpoint and timeout configuration"
Confirm the completed history and final content:
git status
git log --oneline --decorate --graph --all -n 10
cat docs/operations-notes.md
Because both main and feat/configurable-endpoint had new commits, the resulting commit on main is a merge commit. It records the intentional integration of two histories. Test and inspect the destination branch after merging; this is the branch a later pipeline or release process would use.
The feature branch is now merged, so remove it with Git’s safe deletion option:
git branch -d feat/configurable-endpoint
If you need to stop: abort safely
Sometimes a conflict reveals that you need context from a teammate, a decision from a reviewer, or simply more time. Do not guess. In this lab, before completing the commit, you could abandon the in-progress merge with:
git merge --abort
This restores the working tree and HEAD to their pre-merge state. It does not remove the commits already on either branch. You can inspect the histories, communicate with the change authors, and try the merge again later.
Use git merge --abort specifically to cancel a merge that is in progress. Do not reach for destructive commands such as git reset --hard merely because conflict markers look alarming. Those commands solve a different problem and can discard unrelated uncommitted work.
Never fear merge conflicts again - git merge/pull tutorial
Watch “Never fear merge conflicts again” by Philomatics for a compact visual walkthrough of the same command-line state transitions: recognize the conflict, interpret the markers, combine semantics rather than blindly choosing a side, stage, test, and commit.
Watch inspection and markers to reinforce what git status reports and why conflict markers are literal file content. Then watch manual integration, focusing on the example where neither “accept current” nor “accept incoming” alone is correct. Finish with staging and commit, which distinguishes resolving the text from formally completing the merge. If you want a quick review of the escape hatch, the opening abort option demonstrates git merge --abort.
A brief note about GitHub’s conflict interface
The same underlying Git situation can appear in a GitHub pull request. GitHub may identify the conflicting file and offer a browser-based resolver.

The web interface can be convenient for a small text-only conflict. But the same reasoning still applies: inspect both changes, form the intended result, remove markers, and validate the outcome. For script, infrastructure, or application conflicts, resolving locally is often safer because you can run your normal checks before committing.

Later in this module, you will use pull requests more directly. For now, build the habit that works in both interfaces: a conflict is resolved only when the final content meets the combined technical requirements.
Key takeaways
A merge conflict occurs when Git cannot safely infer how overlapping changes should be combined. It preserves both alternatives and pauses the merge rather than discarding either contributor’s work.
Use git status to identify unmerged files. In each conflict block, content above ======= comes from HEAD, the checked-out branch; content below it comes from the incoming branch. Treat “ours” and “theirs” as merge positions, not as ownership labels.
Resolve the conflict by writing the correct final content, which may be a new combination of both versions. Validate the result, remove all markers, stage each resolved file with git add, review the staged resolution, and run git commit to complete the merge. If you are not ready to decide, use git merge --abort rather than making a destructive guess.
Next, you will connect this local workflow to collaboration by synchronizing repositories with clone, fetch, pull, and push.
Can't find a good explanation? Sign up and we'll make it for you
Sign up