Merge Branches with git merge and Survive the Conflicts
The feature branch is ready, the target branch has moved on, and git merge is what brings the two together, cleanly or with a conflict to resolve. By the end of this guide you will be able to merge one branch into another, tell whether Git made a fast-forward or a merge commit, and recover cleanly when it cannot combine the two automatically. The examples use Git 2.43.0, installed here as part of the Git suite. Allow 10 to 20 minutes for a routine merge, longer if you need to review conflicting changes properly.
The route
Jump straight to the step you need, or tick off Done means at the end.
Before you start
You need a Git repository with a target branch and a branch containing changes to bring into it. Swap target-branch and feature-branch below for real names. Merging changes the current branch, its index, and often its working tree, so do not start with valuable uncommitted work sitting around: commit it, stash it, or use a clean worktree first.
Check the version and current state before you change anything:
git --version
git status --short --branch
git branch --show-current
A safe checkpoint is a clean status report, apart from the branch line. Modified or untracked files listed? Stop and decide what to do with them first. Git normally refuses an overlapping merge, but git merge --abort cannot always reconstruct uncommitted changes that were already sitting there when the merge began.
1. Select the branch that will receive the changes
Switch to the target branch: the one whose history and files are about to change.
git switch target-branch
git status --short --branch
Expected output includes ## target-branch and nothing else. If the branch only exists on a remote, check it first with git branch --all, then create a local branch the way your repository normally does that. Never guess a remote name.
2. Inspect the incoming branch
Review what the merge would bring in before you run it. The first command shows commits reachable from the incoming branch but not the target; the second and third show the file changes:
git log --oneline --decorate target-branch..feature-branch
git diff --stat target-branch...feature-branch
git diff target-branch...feature-branch
The three-dot diff compares each branch against their common ancestor, which is usually the review you actually want for a proposed merge. An empty log can mean the incoming branch is already included, and a later merge would just report Already up to date.
3. Choose the merge shape
For an ordinary merge, ask Git to bring the branch in:
git merge feature-branch
Git 2.43.0 fast-forwards when the target branch is an ancestor of the incoming branch: that just moves the pointer, no merge commit involved. Diverged histories get a commit with both tips as parents instead, using this version's default single-head strategy, ort.
Reach for an explicit policy when the history shape matters:
# Refuse to create a merge commit; useful for a linear-only branch.
git merge --ff-only feature-branch
# Always record a merge commit, even when fast-forwarding is possible.
git merge --no-ff feature-branch
--ff-only exits non-zero and leaves the merge undone the moment branches have diverged. --no-ff does rewrite history, so use it only when your project's history policy calls for an explicit branch boundary. Verify the result with:
git status --short --branch
git log --graph --oneline --decorate -8
4. Pause before committing when required
Want to inspect or edit a non-fast-forward result before the commit lands? Start with --no-commit:
git merge --no-commit feature-branch
git diff --cached
git status
- Looks right? Finish with
git commit, orgit merge --continueif the operation is mid conflict-resolution. - Changed your mind? Stop here and run
git merge --abortwhile the merge is still in progress.
There is a subtle boundary here: --no-commit cannot pause a fast-forward, because there is no merge commit to hold open. Pair it with --no-ff when you need a genuine inspection point:
git merge --no-ff --no-commit feature-branch
5. Resolve a conflict without guessing
When Git cannot combine both sides automatically, it leaves HEAD where it was, updates the clean paths, and marks the conflicted files. Get the exact list:
git status
git diff --name-only --diff-filter=U
Open each unmerged file and make a deliberate call between the sections marked by <<<<<<<, ======= and >>>>>>>, then remove every marker. The upper section is normally the target branch, the lower the incoming branch. Original context unclear? Compare the three index stages:
git show :1:path/to/file
git show :2:path/to/file
git show :3:path/to/file
Stage a file only after checking its contents:
git diff -- path/to/file
git add path/to/file
git diff --cached -- path/to/file
Repeat until git diff --name-only --diff-filter=U prints nothing, then finish it off:
git merge --continue
If an editor opens, keep or revise the merge message and save it. Hooks or the message causing a failure? Fix the reported problem and run the command again. Do not reach for --no-verify casually: it bypasses the pre-merge and commit-message hooks that are usually there for a reason.
6. Abandon or park the merge
Warning
These commands alter the in-progress operation. Use them only once you have decided whether the current work should be discarded or kept.
To return to the pre-merge state:
git merge --abort
git status --short --branch
This tries to reconstruct the state from before the merge, but it is not a guarantee for dirty worktrees, especially if files changed after the merge started. Need to leave the index and working tree exactly as they are while forgetting the merge metadata instead? Use git merge --quit. Any autostash goes to the stash list under --quit, but gets applied automatically under --abort.
Common traps
- Unexpected fast-forward. No merge commit means
--no-commithad nothing to pause. Repeat with--no-ff --no-commitonly if an explicit merge commit is actually appropriate. - Unrelated histories. Git refuses branches with no common ancestor by default.
--allow-unrelated-historiesis a deliberate exception for independently started projects, not a routine conflict switch. - Squash is not a merge.
git merge --squash feature-branchprepares the working tree and index without movingHEADor recordingMERGE_HEAD. You still make a separate commit yourself, and--squashcannot be combined with--commit. - Annotated tags. Merging one can create a merge commit even when a fast-forward is possible. Use
--ff-onlywhen refusing that extra commit is the safety check you actually want.
Done means
- Status clean:
git status --shortis empty, unless you intentionally kept unrelated work. - No conflicts left:
git diff --name-only --diff-filter=Uprints nothing. - Graph checked:
git log --graph --oneline --decorateshows the fast-forward or merge commit you intended. - Result reviewed: you have checked the resulting diff and know whether a merge commit was created.