Chapter 6: Merging Strategies with Git

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the concept of merging as a way to integrate changes from different branches.
  • Perform basic merges using git merge <branch-name>.
  • Differentiate between a fast-forward merge and a three-way merge (recursive strategy).
  • Understand the purpose and structure of a merge commit.
  • Identify when a merge conflict occurs.
  • Manually resolve merge conflicts within affected files.
  • Use git add and git commit (or git merge --continue) to finalize a merge after resolving conflicts.
  • Abort a merge in progress using git merge --abort.
  • Briefly understand that graphical merge tools can assist in conflict resolution.

Introduction

In Chapter 5, you learned how to create and manage branches, allowing you to work on different lines of development in isolation. This is incredibly powerful for developing features, fixing bugs, or experimenting without disrupting the main codebase. However, the true value of branching is realized when you integrate these isolated lines of work back together. This integration process is called merging.

This chapter focuses on how Git combines work from different branches. We’ll explore the git merge command, understand the two primary types of merges (fast-forward and three-way), and learn about the special “merge commit” that Git creates. Crucially, we’ll also tackle one of the most common (and sometimes daunting) aspects of merging: merge conflicts. You’ll learn how to identify, understand, and resolve these conflicts, ensuring that your project’s history remains coherent and your codebase functional.

Theory

What is Merging?

Merging is the process of taking the independent lines of development created by branches and integrating them back together. When you merge one branch into another, Git attempts to combine the histories of these branches. It looks at the commits on both branches and tries to incorporate the changes into a single, unified history and codebase.

The branch you are currently on (where HEAD is pointing) is typically called the receiving branch or target branch. The branch whose changes you want to integrate is called the source branch. When you run git merge <source-branch>, you are telling Git to integrate the changes from <source-branch> into your current branch.

The git merge Command

The primary command for merging is git merge <branch-name>. Before running this command, you typically need to:

  1. Ensure your receiving branch (e.g., main) is up-to-date with any changes you want to preserve from its own line of development.
  2. Check out the receiving branch: git switch <receiving-branch-name>.
  3. Then, run git merge <source-branch-name>.

Git employs different strategies to perform a merge, primarily “fast-forward” and “three-way merge.”

Fast-Forward Merges

A fast-forward merge can occur when the receiving branch has not diverged from the source branch. In other words, if the tip of the receiving branch is a direct ancestor of the tip of the source branch, Git can simply move the receiving branch’s pointer forward to point to the same commit as the source branch. No new “merge commit” is created because there’s no divergent history to combine; it’s just a linear progression.

%%{
  init: {
    "theme": "base",
    "themeVariables": {
      "primaryColor": "#EDE9FE",
      "primaryTextColor": "#5B21B6",
      "primaryBorderColor": "#5B21B6",
      "lineColor": "#5B21B6",
      "textColor": "#1F2937",
      "fontSize": "16px",
      "fontFamily": "Open Sans",
      "commitLabelColor": "#FFFFFF",
      "commitLabelBackground": "#5B21B6",
      "gitBranchLabel0": "#5B21B6",
      "gitBranchLabel1": "#2563EB"
    },
    "gitGraph": {
      "showBranches": true,
      "showCommitLabel": true,
      "mainBranchName": "main",
      "mainBranchOrder": 0,
      "commitMessageColor": "#1F2937",
      "commitMessageType": "INFO"
    }
  }
}%%
gitGraph
    commit id: "C1" msg: "Initial Commit"
    commit id: "C2" msg: "main at C2"
    branch feature
    checkout feature
    commit id: "C3" msg: "feature at C3"
    commit id: "C4" msg: "feature at C4"
    checkout main
    merge feature
    commit type: HIGHLIGHT id: "C5" msg: "main, feature, HEAD at same commit"

This is the simplest type of merge. It keeps the history linear and clean.

Three-Way Merges (and the Recursive Strategy)

A three-way merge is necessary when the histories of the receiving and source branches have diverged. This means both branches have had new commits since they branched off from a common ancestor. In this scenario, Git cannot simply move a branch pointer forward.

Instead, Git performs a three-way merge by:

  1. Identifying the common ancestor of the two branches (the point where they diverged).
  2. Identifying the changes made on the receiving branch since the common ancestor.
  3. Identifying the changes made on the source branch since the common ancestor.
  4. Combining these two sets of changes.

If there are no conflicting changes (i.e., both branches didn’t modify the same part of the same file in different ways), Git will create a new commit, called a merge commit.

%%{
  init: {
    "theme": "base",
    "themeVariables": {
      "primaryColor": "#EDE9FE",      /* Default commit node color */
      "primaryTextColor": "#5B21B6",
      "primaryBorderColor": "#5B21B6",
      "lineColor": "#5B21B6",         /* Default branch line color */
      "textColor": "#1F2937",
      "fontSize": "13px",
      "fontFamily": "Open Sans",
      "commitLabelColor": "#FFFFFF",
      "commitLabelBackground": "#5B21B6",
      "gitBranchLabel0": "#5B21B6", /* main branch color */
      "gitBranchLabel1": "#2563EB", /* feature branch color */
      "mergeCommitFill": "#D1FAE5",  /* Merge commit specific fill */
      "mergeCommitStroke": "#059669",
      "mergeCommitColor": "#065F46"
    },
    "gitGraph": {
      "showBranches": true,
      "showCommitLabel": true,
      "mainBranchName": "main",
      "mainBranchOrder": 0,
      "commitMessageColor": "#1F2937",
      "commitMessageType": "INFO",
      "commitSpacing": 100
    }
  }
}%%
gitGraph LR:

    commit id: "C1" tag: "Initial Commit"
    commit id: ""
    commit id: ""
    commit id: "C2" tag: "Common Ancestor (main @ C2)"
    commit id: ""
    branch feature order: 1
    checkout feature
    commit id: "C4" tag: "feature at C4"
    
    checkout main
    commit id: "C3" tag: "main at C3"
    merge feature id: "M" tag: "Merge commit M (main, HEAD @ M)"

The default strategy Git uses for three-way merges is called “recursive.” It’s generally very good at handling complex merge scenarios.

Merge Commits

A merge commit is a special type of commit that has two or more parent commits (usually two: one from the receiving branch and one from the source branch). It doesn’t introduce any new file changes itself (unless conflicts were resolved); rather, its purpose is to tie together the divergent histories. The merge commit’s snapshot represents the combined state of the project after integrating the changes from both branches. When you run git commit after a successful three-way merge (or after resolving conflicts), Git automatically creates this merge commit, usually with a pre-populated message like “Merge branch ‘feature-branch-name'”.

Merge Conflicts

A merge conflict occurs when Git is unable to automatically resolve differences in code between the two branches being merged. This typically happens when:

  • Both branches have modified the same lines in the same file in different ways.
  • One branch deleted a file that another branch modified.

When a conflict occurs, Git will stop the merge process partway and leave the conflicting file(s) in a special unmerged state. Git will mark the conflicting sections in the affected file(s) with visual indicators like <<<<<<<, =======, and >>>>>>>.

Bash
<<<<<<< HEAD (or current branch name)
Changes from the receiving branch (e.g., main)
=======
Changes from the source branch (e.g., feature)
>>>>>>> <branch-name> (source branch name)

Your job is to:

  1. Identify the conflicted files: git status will clearly list them.
  2. Open each conflicted file: Look for the conflict markers.
  3. Manually edit the file: Decide which changes to keep (yours, theirs, a combination of both, or something entirely new). Remove the conflict markers (<<<<<<<, =======, >>>>>>>).
  4. Stage the resolved file: Once you’ve resolved the conflict in a file, use git add <resolved-file-name> to mark it as resolved.
  5. Commit the merge: After resolving all conflicts and staging all resolved files, you complete the merge by running git commit. Git will usually open an editor with a pre-populated merge commit message (e.g., “Merge branch ‘feature'”). You can customize this message if needed. Alternatively, git merge --continue can be used if available and appropriate after staging.

Aborting a Merge

If a merge becomes too complicated or you decide you’re not ready to proceed, you can abort the merge process and return your project to the state it was in before you started the merge. This is done with:

Bash
git merge --abort

This command will clean up the working directory and staging area, discarding any partial merge changes and conflict markers.

Merge Tools

While manual conflict resolution is fundamental, Git also supports the use of graphical merge tools (like kdiff3, meld, p4merge, VS Code’s built-in tool, etc.). These tools can provide a side-by-side (or three-way, including the common ancestor) view of the conflicting changes, often making it easier to see and resolve differences. You can configure Git to use your preferred merge tool with git mergetool.

Command / Concept Description Example / Notes
git merge <branch-name> Integrates changes from <branch-name> (source) into the current branch (receiving/target). Before running: git switch main
Then: git merge feature-branch
Fast-Forward Merge Occurs if the receiving branch’s tip is a direct ancestor of the source branch’s tip. The receiving branch pointer is moved forward; no new merge commit. Keeps history linear. Default behavior when possible.
Three-Way Merge Occurs if branch histories have diverged. Git finds a common ancestor and creates a new merge commit with two parents. Default strategy is “recursive”. The merge commit ties the histories together.
Merge Commit A commit with two or more parents, created during a three-way merge. Its snapshot represents the combined state. Message often defaults to “Merge branch ‘source-branch'”.
Merge Conflict Occurs when Git cannot automatically resolve differences in the same part of a file modified on both branches. Git pauses the merge and marks files. git status shows conflicted files.
Conflict Markers Visual cues Git inserts into conflicted files:
<<<<<<< HEAD
… (changes from current branch) …
=======
… (changes from source branch) …
>>>>>>> <branch-name>
Must be manually removed after deciding on the correct content.
Resolving Conflicts (Steps) 1. Identify conflicted files (git status).
2. Manually edit files to fix conflicts and remove markers.
3. Stage resolved files (git add <file>).
4. Commit the merge (git commit or git merge –continue).
Test thoroughly after resolving.
git merge –abort Aborts a merge in progress, returning the project to its pre-merge state. Useful if conflicts are too complex or the merge was unintended. git merge –abort
git merge –no-ff <branch-name> Forces Git to create a merge commit even if a fast-forward merge is possible. Useful for explicitly marking feature integrations in the history.
git mergetool Launches a configured graphical merge tool to help resolve conflicts. Requires a merge tool to be installed and configured in Git.

Practical Examples

Let’s practice merging. We’ll use a setup similar to previous chapters.

Setup:

Bash
# Create a new directory for this chapter's examples
mkdir my-git-merging
cd my-git-merging
git init

# Commit C1 on main
echo "Project Recipe Book" > README.md
echo "Version 1.0" >> README.md
git add README.md
git commit -m "C1: Initial README"

# Commit C2 on main
echo "Contents:" >> README.md
git add README.md
git commit -m "C2: Add Contents section to README"

1. Fast-Forward Merge Example

Create and switch to a feature branch:

Bash
git switch -c feature/add-license

Make a commit on the feature branch:Create a LICENSE.txt file:

Bash
echo "MIT License" > LICENSE.txt
echo "Copyright (c) 2025 Your Name" >> LICENSE.txt
git add LICENSE.txt
git commit -m "C3: Add LICENSE.txt"

Make another commit on the feature branch:

Bash
echo "Permission is hereby granted..." >> LICENSE.txt
git add LICENSE.txt
git commit -m "C4: Add full MIT license text"

At this point, feature/add-license is two commits ahead of main. main has not changed since feature/add-license was created.

Bash
git log --oneline --graph --decorate --all


Expected Output:

Bash
* <hash_C4> (HEAD -> feature/add-license) C4: Add full MIT license text
* <hash_C3> C3: Add LICENSE.txt
* <hash_C2> (main) C2: Add Contents section to README
* <hash_C1> C1: Initial README

Switch back to main and merge feature/add-license:

Bash
git switch main
git merge feature/add-license

Expected Output:

Bash
Updating <hash_C2>..<hash_C4>
Fast-forward
 LICENSE.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 LICENSE.txt

Explanation of Output:

  • “Fast-forward”: Git indicates it performed a fast-forward merge.
  • The main branch pointer was simply moved forward to commit C4. No new merge commit was created.
  • LICENSE.txt is now present in your working directory on the main branch.

Check the log:

Bash
git log --oneline --graph --decorate --all

Expected Output:

Bash
* <hash_C4> (HEAD -> main, feature/add-license) C4: Add full MIT license text
* <hash_C3> C3: Add LICENSE.txt
* <hash_C2> C2: Add Contents section to README
* <hash_C1> C1: Initial README
```main` and `feature/add-license` now point to the same commit. The history is linear.

2. Three-Way Merge Example (Recursive Merge)

Ensure you are on main. Create a new feature branch:

Bash
# git switch main # (if not already there)
git switch -c feature/add-ingredients

Make a commit on feature/add-ingredients: Create ingredients.txt:

Bash
echo "Flour" > ingredients.txt
echo "Sugar" >> ingredients.txt
git add ingredients.txt
git commit -m "C5: Add initial ingredients list"

Switch back to main and make a different commit there (to create divergence):

Bash
git switch main
echo "Author: The Chef" >> README.md
git add README.md
git commit -m "C6: Add Author to README"

Now, main has commit C6, and feature/add-ingredients has commit C5. Both branched off from C4 (or C2 if you didn’t do the fast-forward example fully). Let’s assume C4 was the common ancestor for simplicity in this isolated example, or C2 if you started fresh from the initial setup. For our log, C4 is the tip of main before C6.

Bash
git log --oneline --graph --decorate --all


Expected Output (assuming C4 was the previous tip of main):

Bash
* <hash_C6> (HEAD -> main) C6: Add Author to README
| * <hash_C5> (feature/add-ingredients) C5: Add initial ingredients list
|/
* <hash_C4> C4: Add full MIT license text  # (or C2 if you skipped previous example)
* <hash_C3> C3: Add LICENSE.txt
* <hash_C2> C2: Add Contents section to README
* <hash_C1> C1: Initial README

The history has diverged.

Merge feature/add-ingredients into main:

Bash
# Ensure you are on main: git switch main
git merge feature/add-ingredients

Since there are no conflicting changes (README.md was changed on main, ingredients.txt was added on feature/add-ingredients), Git will perform a three-way merge and open your configured text editor to ask for a merge commit message.

Expected Output (before editor opens, or if using --no-edit):

Bash
Merge made by the 'recursive' strategy.
 ingredients.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 ingredients.txt

If your editor opens, it will have a default message like:

Bash
Merge branch 'feature/add-ingredients'

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

You can keep the default message or modify it. Save and close the editor.

Check the log:

Bash
git log --oneline --graph --decorate --all

Expected Output:

Bash
* <hash_M7> (HEAD -> main) Merge branch 'feature/add-ingredients' # This is the merge commit
|\
| * <hash_C5> (feature/add-ingredients) C5: Add initial ingredients list
* | <hash_C6> C6: Add Author to README
|/
* <hash_C4> C4: Add full MIT license text
* <hash_C3> C3: Add LICENSE.txt
* <hash_C2> C2: Add Contents section to README
* <hash_C1> C1: Initial README


Explanation of Output:

  • A new merge commit (M7) was created on main.
  • M7 has two parents: C6 (from main) and C5 (from feature/add-ingredients).
  • The graph clearly shows the histories diverging and then coming back together.

3. Merge Conflict Example

Ensure you are on main. Create a new branch:

Bash
# git switch main
git switch -c feature/update-readme-style

On feature/update-readme-style, modify README.md:Change the first line of README.md:

Bash
# (Manually edit README.md to change the first line)
# For example, change "Project Recipe Book" to "THE ULTIMATE RECIPE BOOK"
# Then save the file.
# Simulating with echo:
echo "THE ULTIMATE RECIPE BOOK" > README.md # This overwrites, be careful in real scenarios
echo "Version 1.0" >> README.md
echo "Contents:" >> README.md
echo "Author: The Chef" >> README.md # Assuming this was from previous merge
git add README.md
git commit -m "C8: Update README title style on feature branch"

Switch to main and make a conflicting change to the same line in README.md:

Bash
git switch main
# (Manually edit README.md to change the first line differently)
# For example, change "Project Recipe Book" to "My Humble Recipe Collection"
# Then save the file.
# Simulating with echo:
echo "My Humble Recipe Collection" > README.md # Overwrites
echo "Version 1.0" >> README.md
echo "Contents:" >> README.md
echo "Author: The Chef" >> README.md
git add README.md
git commit -m "C9: Refine README title on main"

Attempt to merge feature/update-readme-style into main:

Bash
# Ensure you are on main: git switch main
git merge feature/update-readme-style

Expected Output:

Bash
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

Explanation of Output:

  • Git reports a CONFLICT in README.md.
  • The merge process is paused. git status will show this.

Inspect the conflict:

Open README.md in your text editor. You’ll see something like:

Bash
<<<<<<< HEAD
My Humble Recipe Collection
=======
THE ULTIMATE RECIPE BOOK
>>>>>>> feature/update-readme-style
Version 1.0
Contents:
Author: The Chef

Resolve the conflict:

Manually edit README.md to be the way you want it. For example, you might decide to keep one, the other, or combine them:

Bash
My Ultimate Recipe Collection for All!
Version 1.0
Contents:
Author: The Chef

Important: Remove the conflict markers (<<<<<<<, =======, >>>>>>>).

Stage the resolved file:

Bash
git add README.md

Commit the merge:

Bash
git commit

Git will open your editor with a default merge commit message. You can keep it or modify it. Save and close.

Expected Output (after commit):

Bash
[main <hash_M10>] Merge branch 'feature/update-readme-style'

Check the log:

Bash
git log --oneline --graph --decorate --all


The log will show a new merge commit (M10) with two parents (C9 and C8).

Aborting a Merge (Alternative to steps 6-8):

If, at step 5, you decided the conflict was too complex or you wanted to rethink, you could run:

Bash
git merge --abort

Expected Output:

(No explicit output, but git status would show you are back on main before the merge attempt, and README.md would be reverted to its state at C9).

OS-Specific Notes

  • Merge Tools:
    • The availability and configuration of graphical merge tools can vary. Common tools like kdiff3, meld, p4merge, Beyond Compare, or the one integrated into VS Code need to be installed separately.
    • To configure a merge tool, you’d typically use:git config --global merge.tool <toolname> # Example for kdiff3: # git config --global mergetool.kdiff3.path /usr/bin/kdiff3 (or path to executable) # git config --global mergetool.kdiff3.trustExitCode false
    • Once configured, you can run git mergetool when a conflict occurs, and Git will open the tool for each conflicted file. The exact setup commands can differ slightly per tool and OS.
  • Line Endings: As with git diff, differences in line endings (CRLF on Windows, LF on macOS/Linux) can sometimes make merge conflicts appear more extensive than they are if core.autocrlf is not configured appropriately (see Chapter 2). Consistent line ending configuration across a team is crucial.
  • Conflict Marker Display: The conflict markers (<<<<<<<, etc.) are plain text and should display correctly in any standard text editor on any OS.

Common Mistakes & Troubleshooting Tips

Git Issue / Error Symptom(s) Troubleshooting / Solution
Merging into wrong branch Target branch gets unexpected commits/history. Solution: Always check current branch (git status) before git merge. If merge was recent & unpushed: git reset –hard HEAD~1 (CAUTION: discards changes on current branch, use if merge commit was just created). More complex fixes for pushed/older merges.
Poorly resolved conflicts Application breaks or tests fail post-merge. Conflict markers might still be present. Solution: Review conflict resolutions carefully. Test thoroughly. Use git diff against common ancestor or branch tips. If merge commit is bad, git revert <merge-commit-hash> or reset and re-merge.
Forgot git add after resolving git commit during merge complains of unmerged paths, or commit doesn’t include resolutions. Solution: Run git add <resolved-file> for each fixed file. git status shows “Changes to be committed” for resolved files.
Committed conflict markers Files contain <<<<<<<, =======, >>>>>>>. Code is broken. Solution: If commit is local, git reset HEAD~1, fix files, re-add, and re-commit. If pushed, create a new commit fixing the markers. Search for markers before committing.
Unexpected merge type History is too linear (unexpected fast-forward) or has an unexpected merge bubble. Solution: Understand fast-forward vs. three-way merge conditions. Use git merge –no-ff to always create a merge commit.
Merge seems stuck or too complex Many conflicts, unsure how to proceed. Solution: Use git merge –abort to cancel and return to pre-merge state. Re-evaluate changes, or merge smaller pieces if possible. Consider using a graphical git mergetool.
“Already up to date.” Running git merge <branch> but nothing happens. Explanation: The target branch already contains all commits from the source branch. This means the source branch has already been merged, or no new commits were made on it since it was created/last merged. Check with git log –oneline –graph –all –decorate.

Exercises

Use the my-git-merging repository. Reset it or create branches as needed to match exercise starting points.

  1. Fast-Forward and Three-Way Practice:
    1. Start on main with C1 and C2 from the setup.
    2. Create a branch feature/contact-page. Add a contact.html file and commit it (C3_feature).
    3. Switch to main. Merge feature/contact-page. Observe if it’s a fast-forward.
    4. Now, on main (which should be at C3_feature), create a new branch feature/about-page.
    5. Switch to feature/about-page, add about.html, and commit it (C4_feature_about).
    6. Switch back to main. Modify README.md (e.g., add a date) and commit this change on main (C5_main).
    7. Merge feature/about-page into main. Observe the merge type and the resulting commit message. View the log graph.
  2. Conflict Resolution Challenge:
    1. Ensure main has some content in README.md.
    2. Create a branch feature/readme-enhancements from main.
    3. On feature/readme-enhancements, modify the second line of README.md and commit.
    4. Switch to main. Modify the same second line of README.md differently and commit.
    5. Attempt to merge feature/readme-enhancements into main.
    6. A conflict should occur. Open README.md, inspect the markers.
    7. Resolve the conflict by choosing one version or creating a new combined version for the second line. Remove the conflict markers.
    8. Stage the resolved README.md.
    9. Complete the merge by committing. Check the log graph.
  3. Aborting a Merge:
    1. Create a new branch temp-feature from main.
    2. On temp-feature, make a change to README.md and commit.
    3. On main, make a different change to the same part of README.md and commit.
    4. Attempt to merge temp-feature into main. A conflict will occur.
    5. This time, instead of resolving, decide to abort the merge using git merge --abort.
    6. Verify with git status that your working directory is clean and you are back on main as it was before the merge attempt. Check the content of README.md.

Summary

Merging is how you combine work from different branches, a critical part of the Git workflow:

  • git merge <branch-name>: Integrates changes from <branch-name> into the current branch.
  • Fast-Forward Merge: Occurs if the receiving branch hasn’t diverged; its pointer is simply moved forward. No new merge commit.
  • Three-Way Merge: Occurs if branches have diverged. Git finds a common ancestor and creates a new merge commit with two parents, representing the combined state.
  • Merge Conflicts: Happen when Git can’t automatically reconcile changes to the same lines in the same file(s) on both branches.
    • Git marks conflicts in files with <<<<<<< HEAD, =======, and >>>>>>> branch-name.
    • To resolve: Manually edit the file(s) to the desired state, remove markers, git add <resolved-file(s)>, then git commit.
  • git merge --abort: Cancels a merge in progress and returns to the pre-merge state.
  • Graphical merge tools can be configured to help with conflict resolution.

Understanding merging and conflict resolution is vital for effective collaboration and maintaining a coherent project history.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top