Git & CI/CD Intermediate

Resolving Git Merge Conflicts: Manual Resolution of HEAD Branch Changes on WSL2 Ubuntu

A comprehensive guide to manually resolve Git merge conflicts where your local HEAD branch changes clash with incoming changes on Windows WSL2 Ubuntu.

👨‍💻
Senior Systems Architect • Verified in Staging Labs

A comprehensive guide to manually resolve Git merge conflicts where your local HEAD branch changes clash with incoming changes on Windows WSL2 Ubuntu.

Introduction

As a seasoned SysAdmin or DevOps engineer, encountering Git merge conflicts is an inevitable part of collaborative development workflows. This guide focuses on a specific, yet common, scenario: manually resolving conflicts that arise when your active branch's local changes (represented by HEAD) clash with incoming changes from another branch during a git merge or git pull operation within your Windows Subsystem for Linux 2 (WSL2) Ubuntu environment.

Understanding how to methodically identify, interpret, and resolve these conflicts is crucial for maintaining code integrity, preventing data loss, and ensuring smooth project progression. We'll walk through the process using command-line tools within your WSL2 Ubuntu terminal, applicable whether you're working on web applications, infrastructure-as-code, or any version-controlled project.

Symptom & Error Signature

When a Git merge conflict occurs, the operation will halt, and Git will notify you that an automatic merge failed. Your terminal will display messages indicating unmerged paths, and the affected files will contain special "conflict markers."

Typical Terminal Output:

$ git pull origin main
From github.com:your-org/your-repo
 * branch            main       -> FETCH_HEAD
Auto-merging src/components/Header.vue
CONFLICT (content): Merge conflict in src/components/Header.vue
Auto-merging server/api/users.js
CONFLICT (content): Merge conflict in server/api/users.js
Automatic merge failed; fix conflicts and then commit the result.

Following this, if you run git status, you'll see something similar to:

$ git status
On branch feature/new-dashboard
Your branch is up to date with 'origin/feature/new-dashboard'.

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:   src/components/Header.vue
        both modified:   server/api/users.js

no changes added to commit (use "git add" and/or "git commit -a")

Conflict Markers in Affected Files:

Within the conflicted files (e.g., src/components/Header.vue), you will find blocks of code marked with <<<<<<<, =======, and >>>>>>>.

// ... other code
function getUserData() {
<<<<<<< HEAD
    console.log("Fetching user data for dashboard display.");
    return fetch('/api/v2/users').then(res => res.json());
=======
    console.log("Initiating user data retrieval.");
    return fetch('/api/users').then(res => res.json());
>>>>>>> main
}
// ... more code
  • <<<<<<< HEAD: Marks the beginning of the changes from your current branch (HEAD).
  • =======: Separates the changes from your HEAD from the incoming changes.
  • >>>>>>> main (or >>>>>>> <commit-hash>/>>>>>>> <branch-name>): Marks the end of the incoming changes, often indicating the branch or commit hash from which these changes originated.

Root Cause Analysis

A Git merge conflict occurs when two divergent branches (or your local work and a remote branch) have made changes to the same lines in the same file, or when one branch modifies a file that the other branch deletes, and Git cannot automatically determine which change should prevail.

In the context of "HEAD branch changes," this specifically means:

  1. Your Local Changes (HEAD): You've made modifications and potentially committed them on your currently active branch (the one HEAD points to).
  2. Incoming Changes: You are attempting to integrate changes from another branch (e.g., main, develop, or a remote branch during git pull).
  3. Divergence: Both your local HEAD and the incoming branch have independently modified the identical lines or sections of one or more files since their last common ancestor commit. Git's three-way merge algorithm identifies this divergence and, unable to perform a "fast-forward" or an automatic merge, flags a conflict.

The role of WSL2 here is primarily as the execution environment. Git itself is a Linux-native tool, and running it within WSL2's Ubuntu instance provides a fully functional Linux environment for Git operations, effectively making the process identical to working on a native Linux machine. The integration with Windows-based editors (like VS Code) simplifies the file editing aspect, but the Git commands and resolution logic remain consistent with a Linux workflow.

Step-by-Step Resolution

This section details the manual process for resolving Git merge conflicts.

Before starting, ensure your working directory is clean or that you have backed up any critical uncommitted changes. While git merge --abort can revert the merge attempt, it's always safer to have a stable starting point.

1. Identify the Conflict

First, ensure you understand which files are conflicting.

# In your WSL2 Ubuntu terminal
git status

This command will clearly list all Unmerged paths and the type of conflict (e.g., both modified, deleted by us, added by them).

2. Understand Conflict Markers

Open each conflicted file using your preferred editor. For example, if you're using VS Code, you can open it from your WSL2 terminal:

code src/components/Header.vue
code server/api/users.js

Inside the file, you'll see the conflict markers. As explained, the content between <<<<<<< HEAD and ======= represents your changes on the current branch. The content between ======= and >>>>>>> <other-branch-name> (or a commit hash) represents the incoming changes from the branch you're merging or pulling from.

3. Choose Your Resolution Strategy

This is the core of manual resolution. For each conflicted block, you must decide:

  • Option A: Keep only your HEAD's changes. Discard the incoming changes.
  • Option B: Keep only incoming changes. Discard your HEAD's changes.
  • Option C: Combine changes. Manually edit the section to integrate both sets of changes, or create a new combined version. This is the most common and often necessary approach.

Let's use the example from server/api/users.js:

// ... other code
function getUserData() {
<<<<<<< HEAD
    console.log("Fetching user data for dashboard display.");
    return fetch('/api/v2/users').then(res => res.json());
=======
    console.log("Initiating user data retrieval.");
    return fetch('/api/users').then(res => res.json());
>>>>>>> main
}
// ... more code

To apply Option A (Keep HEAD's changes):

Edit the file to look like this, removing the =======, >>>>>>> main, and the incoming content:

// ... other code
function getUserData() {
    console.log("Fetching user data for dashboard display.");
    return fetch('/api/v2/users').then(res => res.json());
}
// ... more code

Alternatively, you can use git checkout --ours for a file-level discard (not line-level):

git checkout --ours src/components/Header.vue

When performing a git merge, HEAD (ours) refers to your local branch, and MERGE_HEAD (theirs) refers to the branch being merged in. git checkout --ours <file> will keep your local changes for that file.

To apply Option B (Keep incoming changes):

Edit the file to look like this, removing the <<<<<<< HEAD, =======, and your content:

// ... other code
function getUserData() {
    console.log("Initiating user data retrieval.");
    return fetch('/api/users').then(res => res.json());
}
// ... more code

Alternatively, you can use git checkout --theirs for a file-level discard:

git checkout --theirs src/components/Header.vue

Be absolutely sure which version you want to keep when using git checkout --ours or git checkout --theirs, as it will overwrite the entire file with one version. This is best for simple cases where one entire file should definitely take precedence.

To apply Option C (Manual Combination – Recommended for most cases):

This involves carefully editing the conflicted section. For our example, let's say we want to keep the detailed log message from HEAD and the new API endpoint from main:

// ... other code
function getUserData() {
    // Combined the best aspects of both changes
    console.log("Fetching user data for dashboard display."); // From HEAD
    return fetch('/api/v2/users').then(res => res.json()); // Assuming /v2 is the desired endpoint
}
// ... more code

Most modern IDEs like VS Code have excellent built-in merge conflict resolution tools that present the conflicting sections side-by-side, allowing you to click buttons to accept 'Current Change', 'Incoming Change', or 'Both Changes', and then edit the result. This can significantly speed up the process.

4. Stage Resolved Files

After you have manually edited all conflicted files to remove the conflict markers and include the desired content, you must inform Git that these files are resolved.

# For each file you resolved
git add src/components/Header.vue
git add server/api/users.js

# To verify all conflicts are resolved
git status

git status should now show the files under "Changes to be committed" and no longer list "Unmerged paths".

5. Commit the Merge

Once all conflicts are resolved and staged, you can commit the merge. Git will pre-populate a commit message for you, which you should review and modify if necessary to clearly describe the merge and conflict resolution.

git commit

This will open your default Git editor (often Vim or Nano in WSL2). The message will look something like this:

Merge branch 'main' into feature/new-dashboard

# Conflicts:
#       server/api/users.js
#       src/components/Header.vue
#
# It looks like you may be editing a merge commit message.
# If you would like to add an explanation to go along with your merge commit,
# simply fill in the blank above the already-prepopulated message.
# However, if you would like to cancel the merge commit, then simply close
# this editor without saving the changes.
#
# On branch feature/new-dashboard
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#       modified:   server/api/users.js
#       modified:   src/components/Header.vue
#

Save and exit the editor (e.g., in Vim, type :wq and press Enter). This completes the merge.

6. (Optional) Revert Merge Attempt

If you find yourself in a complex conflict situation and wish to abandon the merge attempt entirely, returning your branch to its state before the git pull or git merge command, you can use:

git merge --abort

git merge --abort will only work if there were conflicts. If Git performed any part of the merge successfully before encountering conflicts, --abort might not fully revert changes that were already merged. Always review git status after an abort to ensure your working directory is as expected.

7. (Optional) Using a Merge Tool

For more complex conflicts, or if you prefer a graphical interface, Git can integrate with external merge tools (e.g., VS Code, KDiff3, Meld, Beyond Compare).

First, configure your merge tool. For VS Code (highly recommended for WSL2 users):

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
git config --global mergetool.vscode.trustExitCode true

Then, when conflicts occur, run:

git mergetool

This will launch your configured merge tool for each conflicted file, guiding you through the three-way merge process (base version, your version, their version). After resolving in the tool, save the file and close it. git mergetool will automatically git add the file once you confirm its resolution.

When using git mergetool with VS Code, ensure the code command is available in your WSL2 path. If not, you might need to install the VS Code Remote – WSL extension and ensure code is linked.

By following these steps, you can confidently navigate and resolve Git merge conflicts, maintaining control over your codebase in your WSL2 Ubuntu development environment.

👨‍💻

Johnathon Wheeler

Senior Systems Architect & DevOps Engineer • Austin, TX

Connect on LinkedIn →

Johnathon has over 16 years of hands-on experience designing, debugging, and scaling Linux web hosting stacks, container clusters, and high-availability database architectures. Every guide on ButItWorkedLocal is independently tested against Debian 12, Ubuntu 24.04/22.04 LTS, Rocky Linux, and Docker environments to guarantee reproducibility in production.

🛡️

Our Production Verification Guarantee

Encountering a bug not covered here or running a non-standard kernel configuration? Our solutions are continually refined against real production incidents. Submit an environment trace for our editorial team to replicate.