When you want to integrate changes from one branch into another, Git gives you fundamentally different strategies. Each produces a different commit history, and choosing the right one affects readability, traceability, and collaboration.
| Strategy | Command | Preserves Branch History? | Creates Extra Commit? | Rewrites History? |
|---|---|---|---|---|
| Merge Commit | git merge feature | Yes | Yes (merge commit) | No |
| Fast-Forward | git merge --ff-only feature | No | No | No |
| Rebase | git rebase main | No | No | Yes |
A merge commit is created when Git combines two branches that have diverged — both branches have commits the other doesn’t. Git finds the common ancestor, compares both tips against it, and creates a new merge commit with two parents.
git checkout main
git merge feature
Both main and feature have progressed independently since they diverged at commit B.
Commit M is the merge commit. It has two parents: G (from main) and E (from feature). The feature branch topology is fully preserved.
git log --merges shows every integration point. Great for auditing.git log --graph.git bisect can get confused navigating merge commits.A fast-forward merge happens when the target branch has no new commits since the feature branch was created. Git simply moves the branch pointer forward — no merge commit is created.
git checkout main
git merge feature # Git auto-detects FF possibility
# or explicitly:
git merge --ff-only feature # Fails if FF is not possible
main has not moved since feature was branched off at C.
The main pointer simply jumps forward to E. The history is perfectly linear — as if the feature work was done directly on main.
git log shows a straight line of commits.git bisect works flawlessly on a linear history.main, this won’t work.main almost always has new commits, making FF impossible without a rebase first.Here, main has moved forward (F, G) since feature branched off. A fast-forward is impossible — Git must create a merge commit or you must rebase first.
Rebase replays your branch’s commits on top of the target branch, one by one. It rewrites commit hashes, creating brand-new commits with the same changes but different ancestry.
git checkout feature
git rebase main
# Then fast-forward main:
git checkout main
git merge feature # This will now be a fast-forward
The original commits C, D, E are gone. New commits C', D', E' have the same diffs but sit on top of G. Their hashes are different because their parent changed.
The end result is a perfectly linear history with no merge commit.
git bisect trivial.git push --force-with-lease, which can confuse collaborators.# NEVER do this on a shared branch:
git checkout main
git rebase feature # Rewrites main's history — breaks everyone!
Rebasing rewrites history. If other people have based work on the old commits, their branches will diverge from the rewritten history, causing chaos.
Starting point:
After git merge feature (Merge Commit):
After git rebase main + git merge feature (Rebase + FF):
After git merge --squash feature (Squash Merge):
Where S is a single commit containing all changes from C and D combined.
| Aspect | Merge Commit | Fast-Forward | Rebase + FF | Squash Merge |
|---|---|---|---|---|
| History Shape | Non-linear (branching) | Linear | Linear | Linear |
| Commit Count | All + 1 merge commit | All original commits | All (new hashes) | 1 squashed commit |
| Branch Context | Preserved | Lost | Lost | Lost |
| Safety | Safe for all branches | Safe for all branches | Unsafe on shared branches | Safe for all branches |
| Conflict Resolution | Once | N/A | Per commit | Once |
git bisect | Harder | Easy | Easy | Coarse (1 commit) |
| Code Review | Good | Good | Best | Coarse |
Interactive rebase lets you rewrite, reorder, squash, or drop commits before integrating.
git rebase -i main
This opens an editor:
pick a1b2c3d Add user model
pick e4f5g6h Fix typo in user model
pick i7j8k9l Add user validation
pick m0n1o2p WIP - debugging
You can change the commands:
pick a1b2c3d Add user model
squash e4f5g6h Fix typo in user model # Merge into previous commit
pick i7j8k9l Add user validation
drop m0n1o2p WIP - debugging # Remove this commit entirely
Before — 4 messy commits:
After interactive rebase — 2 clean commits:
Interactive rebase is invaluable for cleaning up your work before sharing it, turning a messy stream-of-consciousness into a logical, reviewable sequence.
A squash merge condenses all commits from a branch into a single commit on the target branch. It doesn’t create a merge commit — it creates a regular commit.
git checkout main
git merge --squash feature
git commit -m "Add user authentication feature"
S contains all the changes from C, D, and E but as a single commit.
release into main) — never rewrite shared history.# Example: merging a release branch
git checkout main
git merge --no-ff release/v2.1
Use
--no-ffto force a merge commit even when fast-forward is possible, preserving the branch topology.
# Example: merging a quick fix
git checkout main
git merge --ff-only hotfix/typo
# Example: updating feature branch before PR
git checkout feature/auth
git rebase main
git push --force-with-lease
main for a clean, scannable log.# Example: squash merging a PR
git checkout main
git merge --squash feature/signup-form
git commit -m "feat: add signup form with validation"
Never rebase a public/shared branch. If others have pulled it, rewriting history will cause conflicts for everyone.
Rebase local, merge shared. Use rebase to keep your personal branches up to date. Use merge commits to integrate shared work.
Use --force-with-lease, never --force. If you must force-push after a rebase, --force-with-lease ensures you don’t overwrite someone else’s work.
Clean up before sharing. Use interactive rebase (git rebase -i) to squash fixup commits and write meaningful messages before opening a PR.
Be consistent. Pick a strategy as a team and stick with it. Mixed strategies lead to an unreadable history.
When in doubt, merge. A merge commit is always safe. It never rewrites history and always preserves context.
# Safe merge with history
git merge --no-ff feature
# Fast-forward only (fails if not possible)
git merge --ff-only feature
# Rebase feature onto latest main
git checkout feature && git rebase main
# Interactive rebase to clean up last 5 commits
git rebase -i HEAD~5
# Squash merge
git merge --squash feature && git commit
# Safe force-push after rebase
git push --force-with-lease
Choose the strategy that serves your team’s workflow, not just your preference. A clean history is valuable, but so is a safe and traceable one. The best teams know when each tool is the right one.