git merge
git merge combines the changes from one branch into another, producing a unified history that contains the work from both. It is the standard way to bring a finished feature, a bug fix, or a teammate’s contribution back into a shared branch like main. Understanding exactly what merge does under the hood — whether it can simply slide a pointer forward or has to build a brand-new commit with two parents — is essential for reading your project’s history correctly and for resolving the conflicts that inevitably show up.
Overview: How git merge Works
To understand git merge, you first need to understand what a branch actually is. A branch in Git is nothing more than a lightweight, movable pointer (a 41-byte file under .git/refs/heads/) that stores the SHA-1 hash of a single commit. HEAD is itself normally a pointer to whichever branch you currently have checked out — it points to the branch, which points to the commit. Every commit object stores a pointer to a tree object (a snapshot of your project’s directory structure), one or more parent commit hashes, an author, a committer, and a message. Ordinary commits have exactly one parent; a merge commit is simply a commit with two (or more) parents.
When you run git merge <branch> while on some target branch (say main), Git looks at three commits: the tip of your current branch, the tip of the branch you’re merging in, and their merge base — the most recent commit that is an ancestor of both. Git then decides which of two merge strategies applies:
Fast-forward merge
If the merge base is the same commit as the tip of your current branch (meaning main has not moved since feature/login-page branched off it), there is no divergent history to combine. Git simply moves the main pointer forward to match the tip of the feature branch. No new commit is created, and the resulting history looks exactly as if all that work had been committed directly on main.
Three-way (true) merge
If both branches have new commits since the merge base — the common case when a team is working in parallel — Git cannot just move a pointer. Instead it performs a three-way merge: it compares the merge base’s tree against each branch tip’s tree, combines the changes from both sides, and creates a brand-new merge commit whose tree reflects the combined result and which has two parents: the previous tip of main and the tip of the merged-in branch. This preserves the full, true shape of history — you can see exactly when and how the two lines of work rejoined.
Git’s default merge algorithm is the ort strategy (older Git versions used recursive); both compute file-level three-way diffs and only stop to ask you for help when the same lines of the same file were changed differently on both sides — a merge conflict.
Syntax
git merge <branch>
git merge --no-ff <branch>
git merge --ff-only <branch>
git merge --squash <branch>
git merge --abort
git merge --continue
| Flag | Meaning |
|---|---|
<branch> |
The branch whose history you want to merge into the current branch |
--no-ff |
Always create a merge commit, even when a fast-forward is possible; keeps a visible record that a feature branch existed |
--ff-only |
Only merge if a fast-forward is possible; refuses (fails) instead of creating a merge commit |
-m "<message>" |
Supply the merge commit message directly instead of opening an editor |
--squash |
Combine all changes from the other branch into your working tree/index as a single set of edits, but do not commit and do not record it as a merge (no second parent) |
--abort |
Cancel an in-progress merge that has conflicts and return to the pre-merge state |
--continue |
Finish a merge after you’ve manually resolved conflicts and staged the results |
Examples
Example 1: A fast-forward merge
git switch main
git switch -c feature/login-page
echo "login form skeleton" > login.txt
git add login.txt
git commit -m "feat: add login page skeleton"
git switch main
git merge feature/login-page
Updating a1b2c3d..e4f5a6b
Fast-forward
login.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 login.txt
Because main had not received any new commits since feature/login-page was created, Git could not find any divergence to reconcile. It simply moved the main pointer up to the same commit that feature/login-page points to. No merge commit was created — the history is perfectly linear.
Example 2: A three-way merge with –no-ff
git switch main
git switch -c feature/dark-mode
echo "body { background: #111; }" > theme.css
git add theme.css
git commit -m "feat: add dark mode stylesheet"
git switch main
echo "# Changelog" > CHANGELOG.md
git add CHANGELOG.md
git commit -m "docs: start changelog"
git merge --no-ff feature/dark-mode -m "merge: bring in dark mode feature"
Merge made by the 'ort' strategy.
theme.css | 1 +
1 file changed, 1 insertion(+)
create mode 100644 theme.css
Here main gained its own commit (the changelog) after feature/dark-mode branched off, so a fast-forward was impossible — the two branches had diverged. Git created a new merge commit with two parents (the changelog commit and the dark-mode commit) whose tree contains both changes. The --no-ff flag forced a merge commit even if a fast-forward would otherwise have worked, which is useful for keeping a visible record of every feature branch in the log.
Example 3: A merge conflict and how to resolve it
git switch main
git merge feature/pricing-update
Auto-merging pricing.md
CONFLICT (content): Merge conflict in pricing.md
Automatic merge failed; fix conflicts and then commit the result.
Git could not automatically reconcile pricing.md because both branches edited the same lines. Opening the file shows conflict markers:
<<<<<<< HEAD
Basic plan: $9/month
=======
Basic plan: $12/month
>>>>>>> feature/pricing-update
The section between <<<<<<< HEAD and ======= is what your current branch has; the section between ======= and >>>>>>> feature/pricing-update is what the incoming branch has. Edit the file to the version you want, removing the markers entirely, then stage and commit:
git add pricing.md
git commit -m "merge: resolve pricing conflict with feature/pricing-update"
Committing after conflict resolution finishes the merge and produces the merge commit. If you’d rather back out entirely and try again later, run git merge --abort before committing to restore the pre-merge state exactly.
How Git Performs a Merge, Step by Step
1. Git resolves <branch> to a commit hash and finds the merge base (the common ancestor) by walking both branches’ parent chains.
2. Git checks whether the current branch’s tip is the merge base. If so, it performs a fast-forward: it updates the branch ref (and therefore HEAD, which points to that branch) to the other commit’s hash, then checks out that tree into your working directory and index. No commit object is created.
3. If the tips have diverged, Git computes two diffs: merge-base-to-current-tip and merge-base-to-other-tip. It applies both sets of changes together. Where the diffs touch different files or different lines, the merge proceeds automatically. Where they touch the same lines, Git writes conflict markers into the affected file(s) and pauses, leaving the repository in a special mid-merge state (Git records this in .git/MERGE_HEAD).
4. Assuming no conflicts (or after you resolve them and run git add), Git writes a new tree object representing the combined snapshot, then a new commit object pointing to that tree with two parent hashes: the previous tip of the current branch and the tip of the merged branch.
5. The current branch pointer is updated to this new commit hash, and MERGE_HEAD is removed. The working tree and index now reflect the merged snapshot.
Common Mistakes
Mistake: Merging into the wrong branch because you forgot to check which branch you’re on.
git merge feature/login-page
If you run this while still on feature/dark-mode instead of main, you’ll merge the login work into the wrong branch. Always confirm your current branch first with git branch --show-current or check the prompt, and use git switch main before merging into it.
Mistake: Committing a file that still contains conflict markers.
It’s easy to run git add . and commit without actually scanning the resolved files, leaving <<<<<<< HEAD lines baked into your source code. Before committing a conflict resolution, search the changed files for <<<<<<<, or run your test suite — broken syntax from leftover markers usually fails immediately.
Mistake: Merging a stale local copy of a remote branch.
git merge origin/main
If you haven’t run git fetch recently, your local origin/main ref may be far behind the real remote, so you merge outdated work and later discover you missed recent commits. Run git fetch origin immediately before merging a remote-tracking branch.
Mistake: Treating --squash merges as regular merges.
A squash merge (git merge --squash feature/x) stages all the feature branch’s changes but does not create a merge commit or link the branch’s history as a parent — you still must run git commit yourself, and Git will no longer know these commits were ever merged. This can make later merges of the same branch produce confusing, unexpected conflicts.
Best Practices
- Keep your working tree clean (commit or stash changes) before starting a merge — Git will refuse to merge over uncommitted changes that would be overwritten.
- Run
git fetchbefore merging a remote branch so you’re working from up-to-date data. - Use
--no-ffon feature branches merged intomainif your team wants an explicit record of every feature in the log; use fast-forward merges for tiny, linear housekeeping branches. - Write merge commit messages that explain what was merged and why, following the same Conventional Commits style (
merge: integrate dark mode feature) you use for regular commits. - Resolve conflicts carefully — read both sides, don’t just pick one blindly, and re-run tests afterward.
- If a merge goes badly, remember
git merge --abortworks only while the merge is still in progress (before you commit); after committing, usegit revert -m 1 <merge-commit>to undo it instead. - Never rewrite (rebase or force-push) a branch that others have already pulled and merged into their own work — merging works with existing history, so keep that history stable once it’s shared.
Practice Exercises
Exercise 1: Create a new repository, make a commit on main, then create feature/readme-update and commit a change to README.md there without touching main again. Merge it back into main and predict beforehand whether it will fast-forward or create a merge commit. Check your prediction with git log --oneline --graph.
Exercise 2: Starting from the same repository, make a new commit directly on main after creating the feature branch, so the two branches diverge. Merge the feature branch in and observe the merge commit Git creates. Inspect it with git show <merge-commit-hash> and identify its two parent hashes.
Exercise 3: Deliberately create a conflict: on main, edit line 1 of a file and commit; on a new branch from before that commit, edit the same line differently and commit. Merge the branch into main, resolve the conflict markers by hand, stage the file, and complete the merge commit. Then try git merge --abort on a fresh conflict to see how it restores the pre-merge state instead.
Summary
git merge <branch>combines the history of another branch into your current branch.- A fast-forward merge just moves the branch pointer forward when there’s no divergent history; no new commit is made.
- A three-way merge creates a new commit with two parents when both branches have diverged from their common ancestor.
--no-ffforces a merge commit even when a fast-forward is possible;--ff-onlyrefuses to merge unless a fast-forward is possible.- Conflicts happen when both branches changed the same lines; Git marks them with
<<<<<<</=======/>>>>>>>and pauses until you stage a resolution and commit. git merge --abortcancels an in-progress conflicted merge; once committed, usegit revert -m 1instead.- Always fetch before merging a remote branch, and never rewrite history on a branch others already depend on.
