Branching lets you split your work into isolated parallel lines. But a branch is only useful if you can eventually bring its work back home. That's what merging does, and most of the time Git does it so smoothly you barely notice. This article covers how merging actually works, the most common ways Git combines histories, and the moment every developer eventually meets: the merge conflict. By the end you'll see that a conflict isn't a disaster: it's Git telling you that it needs your decision.

What Merging Means

Merging takes the commits from one branch and integrates the changes represented by one history into another. You stand on the branch you want to receive the changes (usually main) and tell Git to merge another branch into it:

bash
# Stand on the branch that will RECEIVE the changes... $ git switch main Switched to branch 'main' # ...then merge the other branch INTO it. $ git merge feature/add-login

That direction matters and trips up almost everyone at first, so it's worth saying twice: you switch to the branch you want to merge into, then name the branch you're merging from. Git then determines how the histories can be combined. Depending on their relationship and the options you use, the result may be a fast-forward, a true merge commit, or another merge mode such as a squash merge.

The Two Most Common Merge Outcomes

For the everyday case of merging one branch into another, the first question to ask is whether the current branch has commits that are not already ancestors of the branch being merged. If it hasn't moved since the feature branch was created, Git can often fast-forward. If both sides contain unique commits, Git normally performs a true three-way merge.

Fast-forward merge: nothing to combine

If main hasn't changed at all since you branched off it, your feature branch is simply main plus a few extra commits in a straight line. In that case Git can do the simplest thing possible: it moves the main pointer forward to the tip of your branch. This is a fast-forward merge: no new merge commit is created, and the resulting history stays linear.

A fast-forward is not really a combination of two divergent histories. Git is simply updating the current branch reference because the current commit is already an ancestor of the commit you asked it to merge.

Three-way merge: two histories that diverged

The more interesting case is when both branches contain commits that are not present in the other branch (for example, work happened on main at the same time as your feature branch). Now the histories have genuinely diverged, and there is no single pointer Git can simply move forward. Git performs a three-way merge using the two branch tips and a suitable merge base, usually their common ancestor, and normally records the result as a new merge commit with two parents.

Fast-forward main didn't move, just slide the pointer A B C D main before main after (slid forward →) Three-way (true merge) both sides moved, create a merge commit O merge base M F MC merge commit (two parents) main feature
When the current branch is directly behind the branch being merged, Git can fast-forward the branch pointer (left). When both histories contain unique commits, Git normally performs a three-way merge and creates a merge commit with two parents (right).

This is called a three-way merge, and the name is worth understanding because it demystifies the whole process: Git compares the two branch tips with a third reference point, the merge base, to determine what changed on each side.

Seeing branches in a graph

The abstract diagrams above are how Git's history can be visualised. But when you work day to day, you'll usually see your branches through a graphical tool: the source-control panel built into many modern code editors, or a dedicated Git client. These tools can render your commit history as a coloured graph, with each branch getting its own colour and lane. It's the same underlying structure we've been drawing, just prettier, interactive, and sometimes simplified. Here's how a two-branch scenario looks in that style, first while both branches are still diverging:

COMMIT GRAPH · before merging GRAPH COMMIT MESSAGE feature-b Add dark-mode toggle feature-a Add login form main Set up project structure Add README Initial commit
Two feature branches (blue and orange) created from the same point on main (green), each with its own commit. This is the kind of graph view a source-control tool can show, and the coloured lanes make the parallel histories easier to understand.

And here's the same project after both feature branches have been merged back into main. Notice how the coloured lanes curve back into the green trunk, and each true merge produces a merge commit where two lines rejoin:

COMMIT GRAPH · after merging both branches GRAPH COMMIT MESSAGE main Merge branch 'feature-b' Merge branch 'feature-a' Add dark-mode toggle Add login form Set up project structure Add README = merge commit (two parents)
The same repository after merging both feature branches back into main. The orange and blue lanes rejoin the green trunk, and each true merge junction is represented by a merge commit with two parents.
Tip for following along. Many code editors offer a commit-graph view (either built in or through an extension), and there are standalone Git clients that do the same. Open one on a repository and you'll see this kind of history update as you create branches, commit, and merge. It's one of the fastest ways to build an intuition for how branches diverge and rejoin.

Why "Three-Way"? The Merge Base

You might expect Git to compare just two things: your branch and main. But comparing only the two final versions does not tell Git which differences came from which side. If a line reads "blue" on one branch and "green" on the other, for example, Git needs a baseline to determine how each side got there.

So Git uses a third reference point: the merge base, which is normally a suitable common ancestor of the two commits being merged. In simple histories, that is the last commit both branches shared before they diverged. In more complex histories there can be multiple possible common ancestors, and Git's merge machinery can construct a virtual base from them.

The Analogy: Two editors, one original

Imagine you and a colleague each took a photocopy of the same original document and edited your copies separately. To combine them, a sensible editor wouldn't just stare at your two versions; they'd put the original next to them. By comparing each copy against the original, they can see what you changed and what your colleague changed. If your edits are in different paragraphs, they can often combine them automatically. The merge base is like that original document: it gives Git the baseline it needs to reason about the two sets of changes.

With the merge base as a baseline, Git compares the changes from the base to each side and attempts to combine the resulting changes. When the changes can be reconciled automatically, Git produces the merged result without asking you to resolve anything manually. When the changes overlap in a way the merge algorithm cannot safely reconcile, Git reports a conflict and asks you to decide.

Git compares both sides against the merge base MERGE BASE title = "Home" color = "blue" the shared baseline CURRENT BRANCH (main) title = "Home" color = "green" ← changed INCOMING BRANCH (feature) title = "Welcome" ← changed color = "blue" MERGED RESULT title = "Welcome" color = "green" both changes kept
Because each branch changed a different line relative to the merge base, Git can usually combine both changes automatically. The exact result depends on the merge algorithm and the surrounding changes, but no manual conflict resolution is needed in this simple example.
A note on the engine. In current Git versions, the default merge strategy for a normal two-head merge is ort. It was introduced as the replacement for the older recursive implementation and became the default in Git 2.34. The name is an acronym for "Ostensibly Recursive's Twin". In Git 2.50 and later, recursive is redirected to ort. You normally do not need to select the strategy yourself; ordinary git merge uses the default.

Merges Can Stack: Branches Built on Branches

So far we've merged a feature branch straight back into main. But nothing in Git says a merge has to end at main. Because a branch is simply a movable reference to a commit, and a merge commit normally has two parents, you can merge one branch into another. Git itself does not attach special semantics to names such as main, develop, or feature/login; those meanings come from the workflow your team chooses.

This makes integration branches possible. One well-known workflow, often associated with Gitflow, uses a branch called develop as an integration point for completed features before they are released through main. However, this is a workflow convention, not a requirement of Git, and many modern teams instead use a simpler model in which short-lived feature branches merge directly into main or another protected integration branch.

In a workflow that uses develop, a typical sequence might look like this:

  • Two branches, feature/login and feature/payments, are created from develop.
  • Each is finished and merged back into develop, where the combined work can be tested together.
  • Further integration fixes or polish can be made on develop.
  • Once the project is ready for release, the team's chosen workflow can merge or otherwise promote the tested state of develop into main.

The important lesson is not the branch name but the principle: Git gives you the building blocks; your team decides how to organise them.

COMMIT GRAPH · features to develop to main main develop login payments Merge branch 'develop' into main ← release point Merge 'feature/payments' into develop Merge 'feature/login' into develop Add checkout page Add login form Start develop branch Set up project ringed nodes = merge commits (two parents)
One possible layered workflow. Two features merge into develop (purple), where their combined work can be integrated and tested, and the resulting state can later be merged into main (green). This is a workflow convention, not something Git requires.

This layered approach is useful for teams that deliberately separate integration from release. Other teams prefer a simpler trunk-based workflow, where short-lived branches merge directly into main or another protected branch. Neither approach is inherently "the Git way": Git supports both.

Where conflicts show up in a layered flow. A conflict is detected when Git tries to combine histories and cannot automatically reconcile part of the result. So if feature/login and feature/payments contain incompatible changes in the same area, the conflict may surface when one is merged into develop. Once the resulting merge is committed, later merges may be clean, but that is not guaranteed. New changes made after the earlier resolution can always create new conflicts.

When Git Can't Decide: The Merge Conflict

Auto-merging works beautifully until the changes from the two sides overlap in a way Git cannot reconcile automatically. This often happens when both branches modify the same lines or nearby regions of the same file in incompatible ways, but a conflict can also arise from other situations, including certain rename, delete/modify, binary-file, or directory-level changes.

Your version may say one thing while the incoming version says another, and Git has no reliable basis for choosing the intended result. Rather than silently discard someone's work, Git stops and asks you to make the final decision.

A conflict is not necessarily an error. A merge conflict means Git could not complete part of the merge automatically. It does not mean the repository is corrupted or that Git has lost the original commits. The important thing is to inspect the conflicting files, decide on the desired result, test it when appropriate, and then tell Git the conflict has been resolved.

Here's what a content conflict can look like when it happens. Say both main and your feature branch edited the same section of index.html differently:

bash · a merge that hits a conflict
$ git merge feature/add-login Auto-merging index.html CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result.
Git tells you what happened: it auto-merged what it could, encountered a conflict in one file, and paused so you can resolve it.

Notice Git didn't fail catastrophically. It merged the paths it could resolve cleanly, recorded the unresolved path as unmerged, and left the merge in progress. You now have to resolve the remaining conflict before the merge can be completed.

Reading Conflict Markers

For a typical textual conflict, Git writes both sides into the working-tree file and surrounds the conflicting region with special markers. The exact labels can vary depending on configuration, but the familiar form looks like this:

<h1>Welcome to our site</h1> <<<<<<< HEAD <p>Please log in below.</p> ======= <p>Sign in to continue.</p> >>>>>>> feature/add-login ← the CURRENT side (HEAD) appears above the divider ← the divider between the two sides ← the INCOMING side appears below
The anatomy of a typical textual conflict. Git places the current side and incoming side in the file, separated by conflict markers, so you can decide what the final result should be.

Three markers define the usual conflict block, and once you know them they read almost like plain English:

<<<<<<< Current side <<<<<<< HEAD

The content after this marker and before the divider is normally the version from your current HEAD.

======= The divider =======

The line that separates the current side (above) from the incoming side (below).

>>>>>>> Incoming side >>>>>>> branch-name

The content after the divider and before this marker is normally the version from the branch being merged in.

Your job is to decide what the final content should be: keep the current version, keep the incoming version, combine parts of both, or write an entirely different resolution. Once you have the desired result, the conflict markers themselves must be removed from the file.

Git can also be configured to show additional information, such as the merge base, using styles such as diff3 or zdiff3. That can be useful when the difference between the two sides is difficult to understand.

Resolving conflicts in an editor

Editing those markers by hand works, but modern editors can make the process much friendlier. For example, Visual Studio Code provides actions such as Accept Current Change, Accept Incoming Change, Accept Both Changes, and Compare Changes. These are interface conveniences for selecting or editing the desired result; they do not remove the need to understand what the code should ultimately contain.

index.html · Merge Conflict 11 12 13 14 15 16 Accept Current Change | Accept Incoming Change | Accept Both Changes | Compare Changes <h1>Welcome to our site</h1> <<<<<<< HEAD (Current Change) <p>Please log in below.</p> ======= <p>Sign in to continue.</p> >>>>>>> feature/add-login (Incoming Change) </body> CURRENT (yours) INCOMING (theirs) The editor helps you choose or edit the result; you still need to verify that the final code is correct.
The same conflict shown in a typical code editor. The interface can offer actions for accepting the current side, incoming side, both changes, or comparing them. The exact wording and layout depend on the editor.
Buttons or by hand, the goal is the same. One-click actions are conveniences provided by your editor. They help construct the final file, but they are not a substitute for reviewing the resulting code. After resolving a conflict, run the relevant tests or checks before considering the merge complete.

Resolving a Conflict, Step by Step

For a normal merge conflict, the process can be remembered as four moves: find, fix, stage, commit.

Step 1: Find the conflicted files

git status is your map. During a paused merge it lists files that still need attention under "Unmerged paths":

bash
$ git status On branch main 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: index.html
Git shows the unresolved paths and the commands you can use to continue or abort the merge. "both modified" is one common conflict status; other conflict types can appear as well.

Step 2: Fix the file

Open index.html in your editor. Decide on the final content, then remove the alternatives you don't want along with all the conflict markers. Say you decide the incoming version is better, so the resolved file should simply read:

index.html · after you resolve it
<h1>Welcome to our site</h1> <p>Sign in to continue.</p>
No conflict markers and no unwanted alternative, just the clean final content you have decided to keep.
The markers must all go. A common beginner mistake is deleting the version they don't want but forgetting to remove the <<<<<<<, =======, or >>>>>>> lines. If any marker survives into the final file, it can become invalid source code or otherwise cause problems. After resolving, searching for <<< is a simple sanity check, although you should also check for the other marker forms and review the final diff.

Step 3: Stage the resolved file

Telling Git "I've handled this one" is done with the same git add command you already know. Staging a previously conflicted file records that you have resolved its unmerged state:

bash
$ git add index.html $ git status On branch main All conflicts fixed but you are still merging. (use "git commit" to conclude the merge)
Once every unmerged path has been resolved and staged, Git confirms that the conflicts are fixed and that the merge can be completed.

Step 4: Complete the merge

Finally, git commit completes the merge. For a normal merge that is in progress, Git prepares a merge commit message that you can edit if necessary:

bash
$ git commit # Git opens your editor with a merge message. # Edit it if necessary, then save and close. [main 9f3c1a0] Merge branch 'feature/add-login'
The conflict is resolved and the merge is complete. Your history now contains a merge commit tying the two lines of development together.

That's it for the basic workflow. The overlapping change is settled, the unresolved index entries have been resolved, and the merge commit records the resulting state and its parents.

The Escape Hatch: git merge --abort

Sometimes you start a merge, see a wall of conflicts, and realise you're not ready, maybe because you merged the wrong branch or need to investigate something first. You can usually cancel an in-progress merge with:

bash · bail out of an in-progress merge
$ git merge --abort # Abort the in-progress merge and attempt to restore # the pre-merge state.
git merge --abort is the standard way to cancel an in-progress merge. It attempts to reconstruct the state that existed before the merge started.
A useful safety net, with an important caveat. In a normal merge, git merge --abort is designed to return the repository to its pre-merge state. However, Git's documentation warns that if you had uncommitted changes when you started the merge, in some cases git merge --abort may be unable to reconstruct those changes perfectly. The safest habit is to commit, stash, or otherwise protect important local work before starting a risky merge.

Fewer Conflicts, Less Pain

You can't avoid conflicts entirely (they're a natural consequence of independent changes that eventually need to be combined), but a few habits make them smaller and easier to resolve:

  • Integrate regularly. The longer a branch lives apart from its target branch, the more the histories can drift. Regularly incorporating relevant upstream changes into your branch (through merging or rebasing, depending on your team's workflow) can surface incompatibilities earlier.
  • Keep branches focused and reasonably short-lived. A small branch that changes a limited area of the codebase is generally easier to integrate than a large branch that has been isolated for weeks.
  • Communicate about overlapping work. If two people know they are both about to make substantial changes to the same part of a codebase, they can coordinate the work and reduce unnecessary overlap.
  • Avoid unrelated formatting changes. Reformatting or rewriting large portions of a file while also making a functional change can make later conflicts much harder to understand.
  • Consider a merge tool. For larger conflicts, git mergetool can launch a configured visual merge tool. These tools can show the current side, incoming side, and sometimes the merge base, making complicated conflicts easier to inspect.
  • Test after resolving. A merge can be syntactically valid while still being logically wrong. Conflict resolution is a code change, so run the relevant tests, linters, builds, or other checks before considering the merge finished.

The Core Mental Model

Merging feels intimidating until the whole process collapses into a single decision flow. Git only ever asks two questions, and every path ends somewhere predictable:

One merge, start to finish git merge <branch> Have the two branches diverged? no yes FAST-FORWARD Git slides the branch pointer forward. No merge commit; the history stays linear. THREE-WAY MERGE Compare both branch tips against the merge base. Can Git reconcile the changes automatically? yes no AUTO-MERGE Git combines the changes for you, no input needed. CONFLICT Git pauses and asks you to decide. Not an error. find → fix → git add → git commit MERGE COMMIT ties both histories · two parents Escape hatch: git merge --abort cancels an in-progress merge and attempts to restore the pre-merge state.
The whole article in one decision flow. Git either fast-forwards or runs a three-way merge; a three-way merge either completes automatically or pauses as a conflict for you to resolve. Both three-way paths end in a merge commit.

Once conflicts stop feeling like mysterious failures and start feeling like a precise question from Git ("these two histories disagree here; what should the final result be?"), merging becomes a routine part of working with version control.

And that brings the whole idea full circle: a branch gives you the freedom to work in isolation, and merging is how you bring that work safely back home, conflicts and all.

Main References

  1. Chacon, S. & Straub, B.Pro Git (2nd ed.), "Basic Branching and Merging"git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging
  2. The Git ProjectOfficial Reference: git-mergegit-scm.com/docs/git-merge
  3. The Git ProjectOfficial Reference: git-merge-basegit-scm.com/docs/git-merge-base
  4. The Git ProjectGit FAQ: merge behaviour and the ort strategygit-scm.com/docs/gitfaq
  5. Visual Studio CodeResolve merge conflictscode.visualstudio.com/docs/sourcecontrol/merge-conflicts
  6. GitHub DocsResolving a merge conflict using the command linedocs.github.com/en/pull-requests/…/resolving-a-merge-conflict-using-the-command-line
← Previous article
← Back to all articles