Merging & Rebasing
Two strategies for combining branches — merge preserves history as-is, rebase rewrites it for a linear timeline.
Recipe
Quick-reference recipe card — copy-paste ready.
# Merge a feature branch into main
git checkout main
git merge feature/auth-page
# Rebase a feature branch onto latest main
git checkout feature/auth-page
git rebase main
# Interactive rebase — squash, reorder, edit commits
git rebase -i main
# Abort a conflicted merge or rebase
git merge --abort
git rebase --abortWhen to reach for this: Every time you need to integrate changes from one branch into another.
Working Example
Merge Workflow
# Update main
git checkout main
git pull origin main
# Merge feature branch
git merge feature/user-profile
# If conflicts occur, resolve them:
# 1. Open conflicted files (marked with <<<<<<< / ======= / >>>>>>>)
# 2. Edit to keep the correct code
# 3. Stage resolved files
git add src/components/UserProfile.tsx
# 4. Complete the merge
git commitWhat this demonstrates:
- Always pull the latest
mainbefore merging - Git marks conflicts with
<<<<<<<,=======,>>>>>>>markers - After resolving conflicts, stage and commit to complete the merge
Rebase Workflow
# Start on your feature branch
git checkout feature/dashboard
# Rebase onto latest main
git fetch origin
git rebase origin/main
# If conflicts occur during rebase:
# 1. Resolve the conflict in the file
# 2. Stage the resolved file
git add src/app/dashboard/page.tsx
# 3. Continue the rebase
git rebase --continue
# Force push after rebase (required since history changed)
git push --force-with-leaseWhat this demonstrates:
git fetch+git rebase origin/mainavoids needing to checkout main first- Rebase replays your commits one at a time on top of the target branch
--force-with-leaseis safer than--force— it refuses to push if the remote has commits you haven't seen
Deep Dive
Merge vs Rebase — When to Use Each
| Merge | Rebase | |
|---|---|---|
| History | Preserves all commits and creates a merge commit | Rewrites commits for a linear history |
| Conflicts | Resolve once in the merge commit | Resolve per-commit during replay |
| Shared branches | Safe — never rewrites published history | Dangerous on shared branches — rewrites commits others may have based work on |
| Best for | Main branch integrations, release branches | Keeping feature branches up to date, cleaning up before PR |
Rule of thumb: Rebase your own feature branches. Merge into shared branches.
Interactive Rebase
Interactive rebase lets you clean up commits before merging a PR.
# Rebase the last 4 commits
git rebase -i HEAD~4This opens an editor with your commits:
pick abc1234 feat: add dashboard layout
pick def5678 fix: typo in dashboard
pick ghi9012 feat: add chart component
pick jkl3456 fix: chart responsive issue
Common operations:
# Squash a fix into the previous commit
pick abc1234 feat: add dashboard layout
squash def5678 fix: typo in dashboard
pick ghi9012 feat: add chart component
squash jkl3456 fix: chart responsive issue
# Reword a commit message
reword abc1234 feat: add dashboard layout
# Reorder commits (just move lines)
pick ghi9012 feat: add chart component
pick abc1234 feat: add dashboard layout
# Drop a commit entirely
drop def5678 fix: typo in dashboard
Resolving Merge Conflicts
When Git cannot auto-merge, it marks the file:
<<<<<<< HEAD
export function Header({ title }: { title: string }) {
return <h1 className="text-2xl font-bold">{title}</h1>;
=======
export function Header({ title, subtitle }: HeaderProps) {
return (
<header>
<h1 className="text-3xl font-bold">{title}</h1>
<p className="text-muted-foreground">{subtitle}</p>
</header>
);
>>>>>>> feature/header-redesign
}Steps to resolve:
- Remove all conflict markers (
<<<<<<<,=======,>>>>>>>) - Keep the code you want (or combine both changes)
- Stage the file and continue the merge/rebase
Fast-Forward vs No-Fast-Forward
# Fast-forward merge (no merge commit, linear history)
git merge --ff-only feature/small-fix
# Force a merge commit even when fast-forward is possible
git merge --no-ff feature/auth-page- Fast-forward moves the branch pointer forward — no merge commit, clean history. Only possible when there are no divergent commits on the target branch.
- No-fast-forward always creates a merge commit — useful for preserving the fact that a feature branch existed.
Gotchas
Things that will bite you. Each gotcha includes what goes wrong, why it happens, and the fix.
-
Rebasing a shared branch — If others have based work on your branch and you force-push a rebase, their history diverges. Fix: Only rebase branches that are yours alone. Use merge for shared branches.
-
Force push destroying work —
git push --forceoverwrites the remote unconditionally. Fix: Always usegit push --force-with-leasewhich refuses if someone else pushed since your last fetch. -
Conflict fatigue during rebase — A rebase across many commits can surface the same conflict repeatedly. Fix: Use
git rerere(reuse recorded resolution) to auto-resolve repeated conflicts:git config rerere.enabled true. -
Lost commits after rebase — Commits seem to disappear after a botched rebase. Fix:
git reflogshows all recent HEAD positions. Find the commit hash andgit checkout -b recovery <hash>. -
Merge commit noise — Frequent merge commits from pulling
maininto your feature branch clutter the history. Fix: Usegit pull --rebaseinstead, or configure it globally:git config pull.rebase true.
Alternatives
Other ways to solve the same problem — and when each is the better choice.
| Alternative | Use When | Don't Use When |
|---|---|---|
git cherry-pick | You need one specific commit from another branch | You need all changes from a branch |
Squash merge (gh pr merge --squash) | Feature branch has messy WIP commits | You want to preserve individual commit history |
git merge --squash | Combine all changes into one commit locally | The branch has meaningful atomic commits worth keeping |
FAQs
When should I use merge vs rebase?
- Rebase your own feature branches to stay up to date with
main - Merge into shared branches like
mainordevelop - Never rebase a branch that others have based work on
What does --force-with-lease do, and why is it safer than --force?
--force-with-leaserefuses to push if the remote branch has commits you haven't fetched--forceoverwrites the remote unconditionally, potentially destroying teammates' work- Always use
--force-with-leaseafter a rebase
How do I squash multiple commits into one using interactive rebase?
git rebase -i HEAD~4
# Change "pick" to "squash" (or "s") on the commits you want to fold in
# Save and close the editor, then edit the combined commit messageWhat do the <<<<<<<, =======, and >>>>>>> markers mean?
<<<<<<< HEADmarks the start of your current branch's version=======separates the two conflicting versions>>>>>>> branch-namemarks the end of the incoming branch's version- Delete all three markers and keep the code you want
Gotcha: Why do I keep resolving the same conflict during a rebase?
- Rebase replays commits one at a time, so the same conflict can appear multiple times
- Enable
git rerereto auto-resolve repeated conflicts:
git config --global rerere.enabled trueWhat is the difference between --ff-only and --no-ff merges?
--ff-onlymoves the branch pointer forward without a merge commit (linear history)--no-ffalways creates a merge commit, preserving that a feature branch existed--ff-onlyfails if branches have diverged
Can I recover commits lost after a bad rebase?
git reflog
# Find the commit hash before the rebase started
git checkout -b recovery <hash>- The reflog keeps a record of all HEAD movements for ~90 days
Gotcha: I rebased a shared branch and my teammate's history diverged. What now?
- Your teammate should run
git fetch originthengit reset --hard origin/branch-name(if they have no local-only commits) - Going forward, only rebase branches that are yours alone
How do I abort a merge or rebase that has gone wrong?
git merge --abort
# or
git rebase --abort- Both commands return your branch to the state before the operation started
In a TypeScript monorepo, should I squash merge feature PRs?
- Squash merge is ideal when the feature branch has many WIP or fix-up commits
- It produces one clean commit on
main, makinggit bisecteasier - Avoid squash if individual commits carry meaningful, atomic changes worth preserving
How does git pull --rebase differ from a regular git pull?
git pull=git fetch+git merge(creates a merge commit if branches diverged)git pull --rebase=git fetch+git rebase(replays your local commits on top of remote)- Use
--rebaseto avoid noisy merge commits when updating a feature branch
How do I resolve a TypeScript conflict where both branches added different type definitions?
- Open the conflicted
.tsfile and combine both type definitions if they are compatible - Run
npx tsc --noEmitafter resolving to verify the merged types compile - Stage the file and continue the merge or rebase
Related
- Essential Git Commands — Staging, committing, branching basics
- GitHub CLI — Merge PRs and manage branches from the terminal
- Git Utilities — Cherry-pick, stash, bisect, and reflog