Create your own
Lesson illustration

Resolving Git Merge Conflicts Safely

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 merge command.

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 regionMeaning in this merge
<<<<<<< HEADStart of the version from the current branch
Text above =======Current branch’s competing content
=======Separator
Text below =======Incoming branch’s competing content
>>>>>>> feat/configurable-endpointEnd 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:

  1. The service check needs a configurable endpoint through SERVICE_URL.
  2. 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.

A GitHub pull-request page reporting that a branch has conflicts, naming the conflicting file, and offering a “Resolve conflicts” button. The warning means GitHub cannot safely choose the final content automatically.

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.

GitHub’s conflict editor showing the “Mark as resolved” control for a conflicting file. This action corresponds conceptually to staging a resolved file with `git add` in the command line workflow.

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