Git Merge: Integrating Histories and Resolving Conflicts

TL;DR: The git merge command is the mechanism Git uses to synthesize divergent histories from two or more branches. Depending on the state of the commit graph, Git executes either a “fast-forward” merge—which simply moves a branch pointer to a newer commit—or a “three-way merge” that creates a dedicated merge commit referencing multiple parent commits.


What Does git merge Actually Do?

To understand merging, we must view Git history as a Directed Acyclic Graph (DAG) of commit nodes. Each commit contains references to its parent commits. When you branch off a main line of development, write code, and then want to bring those changes back, git merge reads this graph to decide how to reconcile the histories.

When you run git merge <feature-branch>, Git determines the relationship between your current branch (HEAD) and the target branch by finding their Lowest Common Ancestor (LCA), also known as the merge base.

Git then selects one of two primary merge strategies:

1. The Fast-Forward Merge (FF)

If the current branch has no new commits since the feature branch was created, the current branch is a direct ancestor of the feature branch. In this case:

  • Git does not need to perform any complex line-by-line diffing.
  • It simply moves the current branch pointer forward to the tip of the feature branch.
  • No new commit object is created.
Before Fast-Forward:
main (HEAD)
  v
[Commit A] ---> [Commit B] ---> [Commit C]
                                   ^
                                feature

After Fast-Forward:
                                 main (HEAD)
                                 feature
                                   v
[Commit A] ---> [Commit B] ---> [Commit C]

2. The Three-Way Merge (True Merge)

If both the current branch and the feature branch have diverged (i.e., both have new commits since the merge base), a fast-forward is mathematically impossible. Git must perform a three-way merge. It compares three distinct snapshots:

  1. The Merge Base: The common ancestor commit where the two branches originally split.
  2. Our Commit: The tip of the current active branch.
  3. Their Commit: The tip of the branch you are merging in.
Diverged History:
          ---> [Commit B] (main, HEAD)
         /
[Commit A] (Merge Base)
         \
          ---> [Commit C] (feature)

Git applies the changes from both paths since the merge base. If the changes do not overlap or conflict, Git automatically stages the combined changes and creates a Merge Commit.

The Merge Commit Structure

A merge commit is physically identical to a regular commit, except it contains two parent pointers in its metadata instead of one.

  • Parent 1: The commit that was at the tip of your active branch before the merge.
  • Parent 2: The commit at the tip of the branch you merged in.

This preserves the entire history of both branches in the Git graph. Learn more about how commits reference ancestors in /git-commit.


What Happens Inside .git/ (Hands-on exploration)

Let us perform a step-by-step walkthrough to see exactly how Git modifies the index, refs, and special metadata files in the .git/ directory during fast-forward merges, three-way merges, and merge conflicts.

Step 1: Setting up the Repository

First, let’s create a clean repository and establish our common ancestor commit.

$ mkdir git-merge-demo
$ cd git-merge-demo
$ git init
Initialized empty Git repository in /users/admin/git-merge-demo/.git/

$ echo -e "Line 1: Base\nLine 2: Base\nLine 3: Base" > file.txt
$ git add file.txt
$ git commit -m "Initial base commit"
[main (root-commit) d1e2f3a] Initial base commit

Our merge base commit is d1e2f3a. Let’s create a branch named branch-a and switch to it:

$ git checkout -b branch-a
Switched to a new branch 'branch-a'

We will modify Line 1 in file.txt on branch-a:

$ echo -e "Line 1: Branch A\nLine 2: Base\nLine 3: Base" > file.txt
$ git add file.txt
$ git commit -m "Modify line 1 on branch-a"
[branch-a a1a1a1a] Modify line 1 on branch-a

Step 2: Demonstrating a Fast-Forward Merge

Let’s switch back to the main branch:

$ git checkout main
Switched to branch 'main'

At this moment, main points to d1e2f3a and branch-a points to a1a1a1a. Because main has no new commits, merging branch-a should trigger a fast-forward:

$ git merge branch-a
Updating d1e2f3a..a1a1a1a
Fast-forward
 file.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Let’s check .git/refs/heads/main to see what Git did:

$ cat .git/refs/heads/main
a1a1a1a6d82c0f2095eeefb925b42d3851b2c129

Git did not create a new commit. It simply wrote the hash of branch-a (a1a1a1a...) directly into the main ref file. This took virtually no CPU or memory.

Step 3: Demonstrating a Three-Way Merge

Now let’s diverge our history. First, we’ll create a branch named branch-b from our current position (main at a1a1a1a) and commit a change on it:

$ git checkout -b branch-b
Switched to a new branch 'branch-b'

$ echo -e "Line 1: Branch A\nLine 2: Base\nLine 3: Branch B" > file.txt
$ git add file.txt
$ git commit -m "Modify line 3 on branch-b"
[branch-b b1b1b1b] Modify line 3 on branch-b

Now we switch back to main, and commit a different non-conflicting change on it. Let’s modify the name of a new file on main to create divergence:

$ git checkout main
Switched to branch 'main'

$ echo "Supporting Docs" > doc.txt
$ git add doc.txt
$ git commit -m "Add documentation file"
[main m1c1c1c] Add documentation file

Now the history has diverged:

  • main is at commit m1c1c1c (contains file.txt with Branch A edit, and doc.txt).
  • branch-b is at commit b1b1b1b (contains file.txt with Branch B edit in Line 3, but no doc.txt).
  • The merge base is a1a1a1a.

Let’s merge branch-b into main:

$ git merge branch-b
Merge made by the 'ort' strategy.
 file.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Git uses the default ort merge strategy (which stands for “Ostensibly Recursive’s Twin,” introduced in Git 2.34 to replace the older recursive strategy). It automatically performs a three-way merge, successfully combining the changes because one edit was on Line 3 and the other was an addition of doc.txt.

Let’s inspect our new merge commit using git cat-file:

$ cat .git/refs/heads/main
m3r9e5c6d82c0f2095eeefb925b42d3851b2c129

$ git cat-file -p m3r9e5c6d82c0f2095eeefb925b42d3851b2c129
tree 7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c
parent m1c1c1c25b42ef245eefb925b42d3851b2c129
parent b1b1b1b8928424ef245eefb925b42d3851b2c129
author Developer <dev@example.com> 1783420900 +0000
committer Developer <dev@example.com> 1783420900 +0000

Merge branch 'branch-b' into main

Look at the two parent fields!

  • parent m1c1c1c... points back to our pre-merge commit on main.
  • parent b1b1b1b... points back to the commit on branch-b.

Step 4: Inducing and Resolving a Merge Conflict

What happens when changes conflict? Let’s create two branches that edit the same line of file.txt.

First, let’s create conflict-a and switch to it:

$ git checkout -b conflict-a
$ echo -e "Line 1: Branch A\nLine 2: Conflict A\nLine 3: Branch B" > file.txt
$ git add file.txt
$ git commit -m "Conflict edit on A"
[conflict-a c1c1c1c] Conflict edit on A

Now let’s go back to main and make a different edit to Line 2:

$ git checkout main
$ echo -e "Line 1: Branch A\nLine 2: Conflict B\nLine 3: Branch B" > file.txt
$ git add file.txt
$ git commit -m "Conflict edit on main"
[main d2d2d2d] Conflict edit on main

Now let’s attempt to merge conflict-a into main:

$ git merge conflict-a
Auto-merging file.txt
CONFLICT (content): Merge conflict in file.txt
Automatic merge failed; fix conflicts and then commit the result.

Git aborts the merge and warns us. Let’s inspect the .git/ directory in this conflicted state:

$ ls -la .git/

Inside .git/, you will find two files that only exist during an ongoing merge:

  1. .git/MERGE_HEAD: Contains the commit hash of the branch we are merging in (c1c1c1c...).
  2. .git/MERGE_MSG: Contains the default commit message template for the merge.

Additionally, if we inspect the staging index using git ls-files --stage:

$ git ls-files --stage
100644 19a4e0a6d0c2e68449c25f464016a28bb0fbf2de 0	doc.txt
100644 a7c8e9d8928424ef245eefb925b42d3851b2c129 1	file.txt
100644 d2d2d2d8928424ef245eefb925b42d3851b2c129 2	file.txt
100644 c1c1c1c8928424ef245eefb925b42d3851b2c129 3	file.txt

Notice the numbers 1, 2, and 3 for file.txt. These represent the three slots of a conflicted file in the index:

  • Slot 1 (The Merge Base): The version of file.txt at the common ancestor commit.
  • Slot 2 (Ours/HEAD): The version on the branch we are merging into (main).
  • Slot 3 (Theirs/MERGE_HEAD): The version on the branch we are merging (conflict-a).

To resolve the conflict, we open file.txt in our editor. Git has modified it to include conflict markers:

Line 1: Branch A
<<<<<<< HEAD
Line 2: Conflict B
=======
Line 2: Conflict A
>>>>>>> conflict-a
Line 3: Branch B

We choose the version we want, delete the conflict markers, and save:

Line 1: Branch A
Line 2: Conflict Resolved
Line 3: Branch B

Now we stage the resolved file:

$ git add file.txt

Let’s check git ls-files --stage again:

$ git ls-files --stage
100644 19a4e0a6d0c2e68449c25f464016a28bb0fbf2de 0	doc.txt
100644 f5a6b7c8928424ef245eefb925b42d3851b2c129 0	file.txt

The multiple slots (1, 2, 3) have collapsed back into a single entry at Slot 0. This tells Git that the conflict has been resolved. Now we commit to finalize:

$ git commit -m "Resolve merge conflict between main and conflict-a"
[main 7e8f9a0] Resolve merge conflict between main and conflict-a

Upon successful commit, Git automatically deletes .git/MERGE_HEAD and .git/MERGE_MSG.


Detailed Command Options & Advanced Usage

Git offers options to fine-tune how merging behaves under different architectural constraints.

1. Controlling Fast-Forwards (--ff-only vs --no-ff)

Different development teams prefer different Git graph styles:

  • Forcing Merge Commits (--no-ff):
    $ git merge --no-ff feature-branch
    Even if a fast-forward is possible, Git will execute a three-way merge and create a merge commit. This preserves the historical context that a branch existed and was merged, which is useful for audit logs or Git Flow methodologies.
  • Preventing Merge Commits (--ff-only):
    $ git merge --ff-only feature-branch
    This ensures that Git will only merge if it can do so via a fast-forward. If the branches have diverged, the merge will abort. This is excellent for pulling remote updates onto a local branch without creating unwanted merge loops.

2. Squashing Commits (--squash)

If you want to integrate a feature branch but don’t want to pollute your main branch with dozens of work-in-progress commits (e.g., “fix typo”, “test fix”), you can use:

$ git merge --squash feature-branch

Internals of Squash:

  1. Git performs a three-way merge of the changes.
  2. It copies all the combined modifications into your index and working directory.
  3. It does not create a commit object.
  4. It does not create a .git/MERGE_HEAD file. You can then run git commit to create a standard, single-parent commit. The historical connection to the feature branch is discarded, keeping the main timeline linear.

3. Squash / Merge Strategies and Conflict Handlers

When merging, you can pass parameters to the merge engine to automatically resolve conflicts:

  • Favor Ours (-Xours):
    $ git merge -Xours feature-branch
    If Git detects conflicting lines, it will automatically choose the version from your current branch (main), while still auto-merging non-conflicting changes.
  • Favor Theirs (-Xtheirs):
    $ git merge -Xtheirs feature-branch
    If a conflict occurs, Git automatically accepts the incoming branch’s changes.

Real-World Scenario: Reverting a Published Merge Commit

Suppose you merged a large feature branch into main and pushed the commit to your production branch:

$ git merge feature-auth
$ git push origin main

Shortly after, production monitoring reports that the authentication service is failing. You must rollback the merge immediately. Because the merge is already pushed and other developers have pulled it, you cannot use git reset --hard to rewrite history. You must revert the merge.

If you try to run a standard revert:

$ git revert m3r9e5c
error: commit m3r9e5c is a merge but no -m option was given.

Git fails because a merge commit has multiple parents, and it doesn’t know which parent branch represents the “mainline” history that should be kept.

How to Resolve This:

To revert a merge, you must specify the mainline parent number (usually 1 for the branch you merged into):

$ git revert -m 1 m3r9e5c

This creates a new commit that applies the inverse diff of all changes brought in by the merge, returning the files to their pre-merge state on main while maintaining a linear graph.

[!WARNING] Reverting a merge commit has a major side effect: if you attempt to merge feature-auth again later after fixing the bug, Git will think those commits are already in main’s history and will skip them, resulting in missing files. To re-introduce the changes, you must first revert the revert commit before merging the branch again.


Common Pitfalls and Mistakes

1. Committing Merge Conflict Markers

It is very common for developers to run git add . and commit without checking if they resolved conflict markers. This pushes raw <<<<<<<, =======, and >>>>>>> text strings into the production codebase.

  • Avoidance: Always run git diff or use tools like git status to ensure all conflict markers are gone before staging and committing.

2. Merging Instead of Rebasing (and Vice Versa)

  • Merge is non-destructive; it preserves historical context but creates a complex “railroad track” graph of commit connections.
  • Rebase rewrites commits to create a linear graph, but alters commit metadata and hashes.
  • Rule of Thumb: Use git merge when bringing changes into shared public branches. Use git rebase to update your private local feature branches with changes from main. For more details, see our guide on /git-rebase.

3. Forgetting an Ongoing Merge

Sometimes developers get interrupted during a merge conflict. They try to run checkout or write new commits, resulting in errors.

  • Fix: If you get lost during a merge conflict and want to clean the slate and start over, abort the merge:
    $ git merge --abort
    This deletes the .git/MERGE_HEAD and restores your working tree to its pre-merge state.

Command Quick Reference

CommandActionGit Internals Action
git merge <branch>Merges branch into active HEADIf possible, fast-forwards; else creates merge commit
git merge --ff-only <branch>Merges only if fast-forward is possibleMoves branch ref file to target hash; else aborts
git merge --no-ff <branch>Forces creation of a merge commitCreates commit with Parent 1 (current) and Parent 2 (target)
git merge --squash <branch>Combines changes into uncommitted stateCopies changes to index/working tree; no merge ref created
git merge --abortCancels active merge conflictDeletes MERGE_HEAD and restores index and working tree
git merge -Xours <branch>Merges, auto-resolving conflicts for HEADResolves conflict slots using index Slot 2
git merge -Xtheirs <branch>Merges, auto-resolving conflicts for targetResolves conflict slots using index Slot 3

FAQ (People Also Ask)

What is the difference between git merge and git rebase?

git merge takes the histories of two branches and combines them by creating a new merge commit that links both branches together in the graph. git rebase lifts the commits from your current branch and replays them one-by-one on top of the target branch, rewriting history to create a single, clean line of commits.

How do I undo a merge before pushing it?

If you completed a merge locally and want to undo it, look at the parent commit of your merge (usually HEAD~1 or the commit you were on before the merge) and run:

$ git reset --hard HEAD~1

This moves your branch pointer back to the pre-merge commit, abandoning the merge commit. Note that any uncommitted local changes will be lost.

What is the “Merge Base”?

The merge base is the closest common ancestor commit shared by the two branches you are trying to merge. Git uses this common ancestor to calculate a three-way diff, identifying what changes occurred on both branches since they split.


Test Your Knowledge

Loading quiz…