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
andgit commit
(orgit 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:
- Ensure your receiving branch (e.g.,
main
) is up-to-date with any changes you want to preserve from its own line of development. - Check out the receiving branch:
git switch <receiving-branch-name>
. - 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:
- Identifying the common ancestor of the two branches (the point where they diverged).
- Identifying the changes made on the receiving branch since the common ancestor.
- Identifying the changes made on the source branch since the common ancestor.
- 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 >>>>>>>
.
<<<<<<< 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:
- Identify the conflicted files:
git status
will clearly list them. - Open each conflicted file: Look for the conflict markers.
- Manually edit the file: Decide which changes to keep (yours, theirs, a combination of both, or something entirely new). Remove the conflict markers (
<<<<<<<
,=======
,>>>>>>>
). - Stage the resolved file: Once you’ve resolved the conflict in a file, use
git add <resolved-file-name>
to mark it as resolved. - 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:
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:
# 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:
git switch -c feature/add-license
Make a commit on the feature branch:Create a LICENSE.txt file:
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:
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.
git log --oneline --graph --decorate --all
Expected Output:
* <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
:
git switch main
git merge feature/add-license
Expected Output:
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 themain
branch.
Check the log:
git log --oneline --graph --decorate --all
Expected Output:
* <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:
# git switch main # (if not already there)
git switch -c feature/add-ingredients
Make a commit on feature/add-ingredients: Create ingredients.txt:
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):
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.
git log --oneline --graph --decorate --all
Expected Output (assuming C4 was the previous tip of main
):
* <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
:
# 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
):
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:
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:
git log --oneline --graph --decorate --all
Expected Output:
* <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 (fromfeature/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:
# 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:
# (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
:
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
:
# Ensure you are on main: git switch main
git merge feature/update-readme-style
Expected Output:
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
inREADME.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:
<<<<<<< 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:
My Ultimate Recipe Collection for All!
Version 1.0
Contents:
Author: The Chef
Important: Remove the conflict markers (<<<<<<<
, =======
, >>>>>>>
).
Stage the resolved file:
git add README.md
Commit the merge:
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):
[main <hash_M10>] Merge branch 'feature/update-readme-style'
Check the log:
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:
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.
- The availability and configuration of graphical merge tools can vary. Common tools like
- 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 ifcore.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.
- Fast-Forward and Three-Way Practice:
- Start on
main
with C1 and C2 from the setup. - Create a branch
feature/contact-page
. Add acontact.html
file and commit it (C3_feature). - Switch to
main
. Mergefeature/contact-page
. Observe if it’s a fast-forward. - Now, on
main
(which should be at C3_feature), create a new branchfeature/about-page
. - Switch to
feature/about-page
, addabout.html
, and commit it (C4_feature_about). - Switch back to
main
. ModifyREADME.md
(e.g., add a date) and commit this change onmain
(C5_main). - Merge
feature/about-page
intomain
. Observe the merge type and the resulting commit message. View the log graph.
- Start on
- Conflict Resolution Challenge:
- Ensure
main
has some content inREADME.md
. - Create a branch
feature/readme-enhancements
frommain
. - On
feature/readme-enhancements
, modify the second line ofREADME.md
and commit. - Switch to
main
. Modify the same second line ofREADME.md
differently and commit. - Attempt to merge
feature/readme-enhancements
intomain
. - A conflict should occur. Open
README.md
, inspect the markers. - Resolve the conflict by choosing one version or creating a new combined version for the second line. Remove the conflict markers.
- Stage the resolved
README.md
. - Complete the merge by committing. Check the log graph.
- Ensure
- Aborting a Merge:
- Create a new branch
temp-feature
frommain
. - On
temp-feature
, make a change toREADME.md
and commit. - On
main
, make a different change to the same part ofREADME.md
and commit. - Attempt to merge
temp-feature
intomain
. A conflict will occur. - This time, instead of resolving, decide to abort the merge using
git merge --abort
. - Verify with
git status
that your working directory is clean and you are back onmain
as it was before the merge attempt. Check the content ofREADME.md
.
- Create a new branch
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)>
, thengit commit
.
- Git marks conflicts in files with
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
- Pro Git Book:
- Chapter 3.2 Git Branching – Basic Branching and Merging: https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging
- Chapter 3.3 Git Branching – Branch Management (relevant for context): https://git-scm.com/book/en/v2/Git-Branching-Branch-Management
- Chapter 7.7 Git Tools – Advanced Merging: https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging
- Official Git Documentation:
git merge
: https://git-scm.com/docs/git-mergegit mergetool
: https://git-scm.com/docs/git-mergetool
- Atlassian Git Tutorials – Merging: https://www.atlassian.com/git/tutorials/using-branches/git-merge
