What Is Merging?
Merging is the process of combining the changes from one branch into another. The typical flow is:
- Switch to the target branch (the branch you want to merge INTO, usually
main). - Run
git merge <source-branch>. - Git figures out the best strategy automatically.
# Standard merge workflow
git switch main # 1. switch to target branch
git merge feature/login # 2. merge source branch into it
git branch -d feature/login # 3. delete merged branch (optional but recommended)
Fast-Forward Merge
A fast-forward merge happens when the target branch hasn't had any new commits since the feature branch was created. In other words, the feature branch's history includes all commits from the target branch — they haven't diverged.
Git doesn't need to create a new commit. It simply moves the target branch pointer forward to match the feature branch.
Before merge
C1 ← C2 ← C3
↑
main (HEAD)
↖ C4 ← C5
↑
feature/login
main has not moved since feature/login was created from C3.
After: git merge feature/login (fast-forward)
C1 ← C2 ← C3 ← C4 ← C5
↑
main (HEAD) ← pointer moved forward
↑
feature/login
No new commit created. main just "fast-forwarded" to where feature/login was.
git switch main
git merge feature/login
# Output:
# Updating a3f8c2d..b7e9f1a
# Fast-forward
# login.html | 45 +++++++++++++++++++++
# 1 file changed, 45 insertions(+)
3-Way Merge
A 3-way merge happens when both branches have new commits since they diverged. Neither branch's history is a superset of the other. Git needs to combine two diverged lines of development.
Git finds the common ancestor commit, then combines changes from both tips, creating a new merge commit with two parents.
Before merge (diverged branches)
C4 ← C5
/ ↑
C1 ← C2 ← C3 feature/login
\
C6 ← C7
↑
main (HEAD)
Both branches have commits after their common ancestor C3.
After: git merge feature/login (3-way)
C4 ← C5
/ \
C1 ← C2 ← C3 M8
\ / ↑
C6 ← C7 ↗ main (HEAD)
M8 is the merge commit. It has TWO parents: C5 and C7.
The commit message is auto-generated: "Merge branch 'feature/login'"
git switch main
git merge feature/login
# Output:
# Merge made by the 'ort' strategy.
# login.html | 45 +++++++++++++++++++++
# 1 file changed, 45 insertions(+)
# View the merge commit with its two parents
git log --oneline --graph
# * b7e9f1a (HEAD -> main) Merge branch 'feature/login'
# |\
# | * a3f8c2d Add login validation
# | * 9d7e6f5 Add login form
# * | c1b2a3e Fix typo in header
# |/
# * f8e7d6c Initial commit
What Is a Merge Commit?
A merge commit is a special commit that has two (or more) parent commits. It represents the point where two lines of history joined. The merge commit itself usually doesn't change any files — its purpose is to record the join point in history.
The --no-ff Flag (Always Create a Merge Commit)
By default, if a fast-forward is possible, Git will do it (no merge commit). The --no-ff (no fast-forward) flag overrides this and always creates a merge commit, even when one isn't strictly necessary.
# Force a merge commit even when fast-forward is possible
git merge --no-ff feature/login
# A text editor opens for the merge commit message
# (or use -m to provide it inline)
git merge --no-ff -m "Merge feature/login: add user authentication" feature/login
When you look at a project's history after many fast-forward merges, it appears as a single straight line — you can't tell where feature branches began and ended. With --no-ff, each feature branch leaves a visible "bump" in the history graph. Many teams enforce --no-ff on main so the history clearly shows when each feature was merged. GitHub's "Create a merge commit" option does this by default.
Squash Merge
git merge --squash combines all commits from the feature branch into a single staged change, then lets you commit it as one clean commit. The feature branch's individual commits don't appear in the target branch's history.
git switch main
git merge --squash feature/login
# Git stages all changes but does NOT commit yet
git commit -m "Add user login feature"
# Now the 5 messy "WIP", "fix typo" commits from the feature branch
# appear as ONE clean commit on main
| Strategy | Creates merge commit? | Feature branch commits visible? | Best for |
|---|---|---|---|
| Fast-forward | No | Yes (inline) | Solo work, simple linear history |
| 3-way merge (default) | Yes (when diverged) | Yes (in branch) | Preserving full branch topology |
| --no-ff | Always | Yes (in branch) | Teams wanting visible branch history |
| --squash | No (you commit manually) | No (squashed into one) | Cleaning up messy WIP commits |
After a Successful Merge
# Verify the merge succeeded
git log --oneline --graph -5
# Delete the now-merged feature branch (local)
git branch -d feature/login
# Delete the remote branch (if it was pushed)
git push origin --delete feature/login
# Check which branches have been merged into current branch
git branch --merged
# * main
# (feature/login is gone — deleted above)
# Check which branches have NOT been merged yet
git branch --no-merged
Merging preserves the full branching topology — you can see exactly where branches diverged and joined. Rebasing rewrites history to create a linear sequence. Neither is universally better; they suit different workflows. See the Rebase lesson for a full comparison.
📋 Summary
- Merging combines changes from one branch into another. Always merge INTO the target branch:
git switch mainthengit merge feature/x. - Fast-forward merge: no divergence — branch pointer just moves forward. No new commit. Clean linear history.
- 3-way merge: branches have diverged — Git creates a new merge commit with two parents.
git merge --no-ff— always create a merge commit, even when fast-forward is possible. Preserves visible branch history.git merge --squash— squash all feature commits into a single staged change for one clean commit.- After merging, delete the feature branch:
git branch -d feature/x.
FAQ
If the target branch hasn't moved since you created the feature branch (i.e., no new commits on main since you branched off), Git will fast-forward. If both branches have new commits since they diverged, Git will do a 3-way merge. You can check by running git log --oneline --graph --all — if the branch histories share a linear path, it's a fast-forward candidate.
Yes. If you haven't pushed yet: git reset --hard ORIG_HEAD — Git saves the pre-merge HEAD in ORIG_HEAD automatically. If you have pushed: create a revert commit with git revert -m 1 <merge-commit-hash> (the -m 1 flag specifies the first parent, i.e., the target branch). Avoid using reset --hard on commits that have been pushed to a shared branch.
"ort" (Ostensibly Recursive's Twin) is Git's default merge strategy since Git 2.34. It replaced the older "recursive" strategy with a faster, more correct implementation. You don't normally need to think about it — Git picks the right strategy automatically. The output "Merge made by the 'ort' strategy" just tells you which algorithm was used.
It depends on your team's preference. Squash merge (--squash) gives a cleaner main branch history — one logical commit per feature — which makes git log and git bisect easier to use. Regular merge (with --no-ff) preserves the full context of every commit from the feature branch. GitHub lets you configure the default per-repository. Many teams use squash for small features and regular merge for larger ones.