Almost every developer hits the same moment: your branch has fallen behind main, and someone says "just rebase it." Or was it "just merge it"? You nod, search for the command, run it, and hope nothing breaks.
The short answer: both commands bring changes from one branch into another. git merge joins two histories together and keeps everything exactly as it happened. git rebase rewrites your commits so they look like they were made on top of the latest code. Merge preserves history. Rebase cleans it up.
That one-line answer is correct, but it doesn't help you decide which to use on a real project. This guide goes further. You'll see exactly what each command does to your commit graph, how conflicts behave in each case, when your team should pick one over the other, and how to recover if something goes wrong.
Why this confuses so many people
Merge and rebase solve the same problem (combining work from two branches) but they do it in opposite ways. Merge adds to history. Rebase changes history. Because both end with your code up to date, beginners often treat them as interchangeable, and then run into trouble when a rebase causes a messy git push or duplicate commits on a shared branch.
The fix is to understand what's happening underneath. Once you can picture the commit graph, the choice becomes much easier.
First, a quick picture of Git history
Git stores your project as a graph of commits. Each commit is a snapshot of your files plus a pointer to its parent commit (or two parents, for a merge). A few terms you'll see throughout this guide:
- Commit: a snapshot with a unique ID (a hash like
a1b2c3d). - Branch: a lightweight, movable label that points at a commit. Creating a branch doesn't copy any files.
- HEAD: a pointer to the commit or branch you currently have checked out.
- Parent: the commit that came directly before. History is a chain of parent links.
Here is the situation we'll use throughout the whole article. You created feature from main and made two commits. Meanwhile, a teammate added a new commit to main:
A --- B --- E (main)
\
C --- D (feature)
Commit B is the common ancestor, the last commit both branches share. Your feature branch doesn't contain E yet, and you want it to. There are two ways to do that.
What git merge does
git switch feature
git merge main
Git finds the common ancestor (B), compares it against both branch tips (D and E), combines the changes, and creates a new merge commit (M) with two parents. This is called a three-way merge, because it uses three commits: the ancestor and the two tips.
A --- B --- E ------- (main)
\ \
C --- D --- M (feature)
Nothing is rewritten. Commits C and D keep their original IDs. The graph honestly shows that the work happened on a separate line and was joined later.
The special case: fast-forward merges
If main had not moved since you branched, there would be nothing to combine. Git would simply slide the main pointer forward to the tip of feature:
Before:
A --- B (main)
\
C --- D (feature)
After `git merge feature` on main:
A --- B --- C --- D (main, feature)
No merge commit is created. This is called a fast-forward. If you want a merge commit anyway, to record that a feature branch existed, use:
git merge --no-ff feature
Pros of merge
- It never changes existing commits, so it is very safe.
- It records exactly what happened and when.
- It works on shared branches without surprising anyone.
- Undoing a merge is straightforward.
Cons of merge
- Merge commits can clutter history, especially when you keep merging
maininto your branch. - On a busy project, the graph can look like a tangle of railway tracks, which makes
git log --graphhard to read. - Commits like "Merge branch 'main' into feature" add noise without adding information.
What git rebase does
git switch feature
git rebase main
Rebase works in a few steps:
- Git finds the commits on
featurethat are not onmain(here,CandD). - It saves them temporarily as patches.
- It moves the
featurebranch to the tip ofmain(commitE). - It replays your patches one by one on top, creating new commits with new IDs, written here as
C'andD'.
A --- B --- E --- C' --- D'
| |
(main) (feature)
The history is now a straight line, as if you had started your work after E already existed. The content of your changes is the same, but the commits themselves are brand new objects. That is the key idea: rebase doesn't move commits, it recreates them.
Pros of rebase
- Clean, linear history that is easy to read.
- No extra merge commits.
git logandgit bisect(the tool for finding which commit introduced a bug) become much simpler.- Lets you tidy up messy work before sharing it.
Cons of rebase
- It rewrites history.
C'andD'are not the same commits asCandD. - Conflicts may need resolving once per replayed commit, not just once.
- It's risky on commits other people already have.
- It can feel confusing at first, especially in the middle of a conflict.
Side-by-side comparison
| Merge | Rebase | |
|---|---|---|
| History shape | Branching, with merge commits | Linear |
| Existing commits | Unchanged | Rewritten (new IDs) |
| Creates new commits | One merge commit | One new commit per replayed commit |
| Conflict handling | Resolve once | Possibly once per commit |
| Safety | Very safe | Risky on shared branches |
| Traceability | Shows exactly when branches joined | Hides that a branch ever existed |
| Best for | Shared branches, preserving context | Tidying your own local work |
Handling conflicts in each approach
Conflicts happen when both branches changed the same lines. Git can't decide which version wins, so it stops and asks you. You'll see markers like this inside the file:
<<<<<<< HEAD
const timeout = 3000;
=======
const timeout = 5000;
>>>>>>> feature
Everything between <<<<<<< and ======= is one side, and everything between ======= and >>>>>>> is the other. Edit the file to keep what you want, and delete the markers.
Conflicts during a merge
You resolve everything in one go:
git merge main
# conflict appears, fix the files
git add <file>
git commit
Conflicts during a rebase
Git replays commits one at a time, so it may stop several times, once for each commit that conflicts:
git rebase main
# conflict appears, fix the files
git add <file>
git rebase --continue
# repeat if Git stops again
This is why a long branch with many commits can feel painful to rebase. If you ever get stuck, back out safely:
git rebase --abort # return to how things were before the rebase
git merge --abort # same idea for a merge
Interactive rebase: cleaning up before you share
The most useful side of rebase is interactive mode. It lets you edit your own commits before anyone else sees them:
git rebase -i main
Git opens a list of your commits, and you choose what to do with each one:
pick: keep the commit as it is.reword: keep the changes but edit the commit message.squash: combine this commit into the previous one and merge their messages.fixup: like squash, but discard this commit's message.drop: remove the commit entirely.
A typical use: you made commits called "WIP", "fix typo", "oops forgot file" and "actually fix it". Before opening a pull request, you squash them into one meaningful commit like "Add login form validation". Reviewers get a clean history, and your future self will thank you.
Real-world team workflows
Different teams settle on different habits. The three you'll meet most often:
1. Merge everything. Every feature is merged with a merge commit. History is a faithful record of what happened. It's simple, safe, and easy for beginners.
2. Rebase, then merge. Developers rebase their feature branch onto the latest main to stay current, then merge it with --no-ff. The feature's commits stay in a neat line, and a single merge commit marks where the feature landed.
3. Squash and merge. Many platforms offer this option on pull requests. It combines all of a branch's commits into a single commit on main. History stays very tidy, although the individual commits from the branch are lost.
None of these is "correct". What matters is that your team agrees on one and sticks to it. Check your project's contributing guide before you choose.
A useful shortcut: git pull --rebase
By default, git pull fetches new commits and then merges them into your local branch. If you commit locally while a teammate pushes to the same branch, you end up with a "Merge branch 'main' of ..." commit that adds no value.
You can ask Git to rebase instead:
git pull --rebase
Your local commits are replayed on top of what you just fetched, and the history stays linear. To make this the default behavior:
git config --global pull.rebase true
When to use which
A few rules of thumb cover most situations:
- Updating your own feature branch with the latest
main? Rebase is a good choice, as long as nobody else is working on that branch. - Bringing a finished feature into
main? Merge is the usual choice, and most teams do this through a pull request. - Working on a branch with teammates? Merge. Don't rewrite history other people depend on.
- Cleaning up messy commits before a pull request? Use interactive rebase.
- Pulling the latest changes into your local branch?
git pull --rebaseavoids unnecessary merge commits. - Not sure? Merge. It's harder to get wrong.
The golden rule of rebasing
Never rebase commits that exist outside your own machine and that other people may be using.
Rebase creates new commits to replace old ones. If a teammate already has the old versions, your histories no longer match. When they next pull, Git sees two sets of nearly identical commits and fills the history with duplicates and confusing conflicts.
Rebasing a branch that only you have pushed is fine, but you'll need to update the remote afterwards, because the old commits on the remote no longer match your new ones. The safer way to do that is:
git push --force-with-lease
This refuses to overwrite the remote branch if someone else has pushed to it in the meantime. It's much safer than plain --force, which overwrites whatever is there without checking.
Common mistakes and how to recover
"I rebased and now my history looks wrong." You almost certainly didn't lose anything. Git keeps a log of everywhere your branch pointers have been:
git reflog
Find the entry from just before the rebase, then restore it:
git reset --hard <commit-id>
Use reset --hard carefully, because it discards uncommitted changes.
"I pushed a rebased branch and my teammate's work disappeared." It's still in their local repository, and in the reflog. Ask them not to force anything, then merge their version back in.
"I merged the wrong branch." If you haven't pushed yet, undo the last merge with:
git reset --hard ORIG_HEAD
If you already pushed, use git revert -m 1 <merge-commit-id> to create a new commit that undoes the merge without rewriting history.
Quick cheat sheet
| I want to... | Command |
|---|---|
Merge main into my branch | git merge main |
| Always create a merge commit | git merge --no-ff feature |
Rebase my branch onto main | git rebase main |
| Clean up my last few commits | git rebase -i HEAD~3 |
| Cancel a rebase in progress | git rebase --abort |
| Pull without a merge commit | git pull --rebase |
| Safely update a rebased remote branch | git push --force-with-lease |
| Find lost commits | git reflog |
Try it yourself
Reading about commit graphs only goes so far. The best way to understand merge and rebase is to watch the graph change as you run the commands.
The free Git Simulator on OpenLabs draws your commits, branches and HEAD as a live graph in your browser. Create a branch, add some commits, then try merge and rebase on the same setup and compare the results. Nothing to install, and you can't break anything. Experiment with fast-forward merges, conflicts and recovery without touching a real repository.
If you want to go further, explore the rest of our Computer Science labs, including sorting algorithms, networking and logic gates.
Final thoughts
Merge and rebase aren't rivals. Merge is the honest historian: it records everything that happened. Rebase is the editor: it rewrites your private drafts so the story reads clearly. Use rebase to polish your own work, use merge to combine it with everyone else's, and never rewrite history that others rely on.
Once you've watched both commands reshape a commit graph, the choice stops feeling like guesswork.



