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:
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.
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:
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:
main. The orange and blue lanes rejoin the green trunk, and each true merge junction is represented by a merge commit with two parents.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.
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.
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/loginandfeature/payments, are created fromdevelop. - 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
developintomain.
The important lesson is not the branch name but the principle: Git gives you the building blocks; your team decides how to organise them.
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.
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.
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:
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:
Three markers define the usual conflict block, and once you know them they read almost like plain English:
The content after this marker and before the divider is normally the version from your current HEAD.
The line that separates the current side (above) from the incoming side (below).
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.
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":
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:
<<<<<<<, =======, 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:
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:
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:
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.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 mergetoolcan 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:
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
- Chacon, S. & Straub, B. — Pro Git (2nd ed.), "Basic Branching and Merging" — git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging
- The Git Project — Official Reference: git-merge — git-scm.com/docs/git-merge
- The Git Project — Official Reference: git-merge-base — git-scm.com/docs/git-merge-base
- The Git Project — Git FAQ: merge behaviour and the ort strategy — git-scm.com/docs/gitfaq
- Visual Studio Code — Resolve merge conflicts — code.visualstudio.com/docs/sourcecontrol/merge-conflicts
- GitHub Docs — Resolving a merge conflict using the command line — docs.github.com/en/pull-requests/…/resolving-a-merge-conflict-using-the-command-line