CoreMesh

Git Rebase vs Merge: What Each Does, With Examples

Git rebase vs merge with examples: what each does to history, when to use which, interactive rebase basics, conflict commands and the golden rule.

Merge joins two lines of work with a new merge commit and keeps the history as it happened. Rebase takes your commits and replays them on top of another branch, so the history looks like one straight line. Both end with the same code. Only the history differs.

The short rule: rebase your own local work before you share it, merge (or fast-forward) when you integrate, and never rebase commits other people may have based work on. The rest of this page shows what that looks like in commands.

What merge does to history#

Say master and experiment have diverged. git merge performs a three-way merge between the two latest branch snapshots and their most recent common ancestor, then records the result as a new snapshot and commit. That's the merge commit, and it has two parents.

text
git checkout master
git merge experiment

Nothing is rewritten. Every existing commit keeps its identity, and the log shows that the work happened in parallel. The cost is extra merge commits that make the graph busier.

What rebase does to history#

Rebase takes the changes you committed on one branch and replays them on a different base. From the official docs, with topic checked out:

text
      A---B---C topic
     /
D---E---F---G master
text
git rebase master
text
              A'--B'--C' topic
             /
D---E---F---G master

If you want to rebase topic while on another branch, git rebase master topic is a shortcut for git checkout topic && git rebase master.

The commits are replayed one by one, similar to running git cherry-pick <commit> for each. That's why they're written A', B', C': they're new commits that carry the same changes. The old ones are abandoned.

Here's a small model you can run. It isn't Git, just the commit graph for each approach, so you can see why one is linear and the other isn't.

javascript
// Each commit lists its parents.
const base = { A: [], B: ['A'] };

// Merge: C on main, D and E on feature, then merge commit M.
const merged = { ...base, C: ['B'], D: ['B'], E: ['D'], M: ['C', 'E'] };

// Rebase: D and E replayed on top of C (new commits D2, E2).
const rebased = { ...base, C: ['B'], D2: ['C'], E2: ['D2'] };

function reachable(graph, tip) {
  const seen = [];
  const stack = [tip];
  while (stack.length) {
    const c = stack.pop();
    if (seen.includes(c)) continue;
    seen.push(c);
    stack.push(...graph[c]);
  }
  return seen;
}

function describe(name, graph, tip) {
  const all = reachable(graph, tip);
  const merges = all.filter(c => graph[c].length > 1).length;
  console.log(name, 'commits:', all.length, 'merge commits:', merges,
    'linear:', merges === 0);
}

describe('merge ', merged, 'M');
describe('rebase', rebased, 'E2');

Fast-forward: the third option#

A fast-forward merge just moves a branch pointer forward. It creates no merge commit, because nothing diverged. This is how rebase and merge combine in practice. Rebase the topic branch onto the target first, then fast-forward the target:

text
git checkout experiment
git rebase master
git checkout master
git merge experiment

The docs point out that the snapshot at the end is exactly the same as in the plain merge case. There's no difference in the end product, but the log reads as if all the work happened in series.

This is also the answer to "how to git rebase without merge commits": rebase, then fast-forward. When you rebase, the old merge commits on your branch are not replayed by default. The docs explain that the interactive rebase todo list excludes merge commits, since a developer may have merged the then-current master while working, only to rebase everything onto master later.

Rebase vs merge: when to use which#

Situation Use Why
Your feature branch is behind main and nobody else has your commits Rebase onto main Clean, linear history; no merge commit just to catch up
Branch is shared and others have pulled it Merge Rebasing would abandon commits they built on
Bringing a finished branch into main Fast-forward merge after rebase, or a regular merge Depends on whether your team wants the branch shape recorded
Contributing to a project you don't maintain Rebase onto origin/master, then submit The maintainer only has to fast-forward or apply cleanly
Tidying messy local commits before a pull request Interactive rebase Reorder or combine commits before anyone sees them

The Pro Git book frames the real disagreement well. One camp says history is a record of what happened and shouldn't be touched. The other says history is the story of how the project was made, and you wouldn't publish the first draft of a book. The book's conclusion is that it's not that simple, and every team and project differs. So "which is better" has no universal answer. Its practical compromise is the one I'd start with: rebase local changes before pushing to clean up your work, but never rebase anything you've pushed somewhere.

The golden rule of rebasing#

Do not rebase commits that exist outside your repository and that people may have based work on.

Rebasing abandons existing commits and creates new ones that are similar but different. Here's how it goes wrong, per the Pro Git example:

  1. You clone a repo and build work on top of someone's commits.
  2. That person rebases their branch and runs git push --force, overwriting the history on the server.
  3. You fetch and git pull. You get a merge commit that includes both lines of history.
  4. git log now shows two commits with the same author, date and message. If you push that, you reintroduce the rebased commits to the server.

The docs sort it into three cases:

  • Commits that never left your computer: rebase freely.
  • Commits pushed, but nobody has based work on them: still fine.
  • Commits pushed publicly that people may have built on: expect frustrating trouble.

If a teammate already force-pushed#

Git computes a checksum based just on the patch a commit introduces, called a "patch-id". If you rebase your work onto the rewritten branch, Git can often work out which of your commits are uniquely yours and skip the ones that are duplicates of rewritten ones:

text
git fetch
git rebase teamone/master

This only works if the original and rewritten commits are almost exactly the same patch. Otherwise Git can't tell they're duplicates and will add another similar commit, which will probably fail to apply cleanly.

Rebase vs pull: git pull --rebase#

A plain git pull fetches and merges, which can add a merge commit each time you sync. git pull --rebase replays your local commits on top of what was fetched instead. To make that the default:

text
git config --global pull.rebase true

If a rewrite happens on a shared branch, the docs suggest making sure everyone knows to run git pull --rebase to make the aftermath a bit easier.

Interactive rebase basics#

Besides moving commits, git rebase can reorder or combine them. Add -i and give it a commit to start from. To work on the last three commits:

text
git rebase -i HEAD~3

Git opens an editor with a todo list, one line per commit, oldest first. Each line starts with a command followed by the hash and message:

text
pick deadbee The oneline of the commit
pick fa1afe1 The oneline of the next commit

The commands I'd learn first, all mentioned in the docs:

  • pick: keep the commit as is.
  • edit: stop at that commit so you can change it, or split it.
  • drop: remove the commit without any warning or error.

Deleting a line also removes a commit. If you'd like Git to catch accidental deletions, set rebase.missingCommitsCheck to warn or error. The default is ignore. For combining commits, the docs only describe it generally ("reorder or combine commits"), so check git help rebase for the exact todo command names on your version.

Splitting one commit into two#

The docs give this recipe:

text
git rebase -i <commit>^
# mark the commit with "edit"
git reset HEAD^
git add <files for the first commit>
git commit -m "first part"
# repeat add/commit until the working tree is clean
git rebase --continue

The reset HEAD^ rewinds HEAD by one and the index follows, but your working tree stays the same, so nothing is lost.

Rebasing with merge conflicts#

Conflicts during a rebase stop at the first problematic commit and leave conflict markers. You have three ways out:

text
# 1. Resolve: edit the files, then mark each as resolved
git add <filename>
git rebase --continue

# 2. Give up and return the branch to its original state
git rebase --abort

# 3. Skip the commit that caused the conflict
git rebase --skip

Use git diff to find the <<<<<< markers. Because rebase replays commit by commit, you may resolve conflicts several times, once per commit that touches the same lines. A merge shows all conflicts in one go. That's a genuine point in merge's favor for long-running branches.

Rebasing onto a different base#

You can replay commits onto something other than the branch you started from. If client was branched off server, and you want only the client changes on master:

text
git rebase --onto master server client

That means: take the patches on client since it diverged from server, and replay them as if client were based directly on master. Then fast-forward master with git checkout master and git merge client.

If you want to keep the branch structure instead of flattening it, the docs describe --rebase-merges, which recreates merge commits.

Common mistakes and gotchas#

  • Rebasing a shared branch. This is the golden rule again. If you've pushed and others pulled, merge instead.
  • Force-pushing without thinking. After rebasing pushed commits, the server rejects a normal push because history changed. That's the moment to stop and ask who else has the branch.
  • Thinking the old commits are gone. When a rebase starts, ORIG_HEAD points at the old branch tip, but other commands like git reset can change it. The previous tip is also reachable through the branch's reflog as @{1}. To undo a finished rebase, check that first.
  • Rebasing with a dirty worktree. Set rebase.autoStash to true to stash automatically and reapply afterward. The docs warn the final stash application might cause non-trivial conflicts. It defaults to false.
  • Assuming a reverted change disappears from a merge. With three-way merges, if a change is made on both branches and later reverted on one, it still appears in the merged result. Only the heads and merge base are considered, not individual commits.
  • Treating squash as the same thing. Squash merging is a separate way of landing a branch, and the sources used here don't cover it. Check your Git host's docs for how it behaves.

Quick reference#

Goal Command
Merge a branch git merge <branch>
Rebase current branch onto another git rebase <base>
Rebase a branch without checking it out git rebase <base> <topic>
Linear history without a merge commit Rebase, then git merge (fast-forward)
Rewrite recent commits git rebase -i HEAD~3
Continue / abort / skip git rebase --continue, --abort, --skip
Pull with rebase git pull --rebase
Make that default git config --global pull.rebase true
Move a branch off another branch git rebase --onto <new> <old> <branch>

Checklist before you rebase:

  1. Have these commits been pushed? If yes, does anyone else have them?
  2. Is the working tree clean, or is rebase.autoStash set?
  3. Do you know git rebase --abort is your escape hatch?
  4. Afterward, did you review git log before pushing?

Further reading: the Pro Git chapter on rebasing and the git-rebase reference.

Quick answers

Is git rebase better than merge?

Neither is better in every case. Rebase gives a cleaner, linear history; merge preserves exactly what happened. A common approach is to rebase local, unpushed work and never rebase anything you've already pushed.

Does rebase or merge produce a different final result?

No. The final snapshot is the same either way, as the Pro Git book notes. Only the history differs: rebase replays commits as new ones, merge adds a merge commit.

How do I rebase without creating merge commits?

Rebase your branch onto the target, then switch to the target and run git merge. Because the branch now sits directly on top, that merge is a fast-forward and creates no merge commit.

What do I do when a rebase hits a merge conflict?

Edit the conflicted files, run git add on each, then git rebase --continue. If you'd rather stop, git rebase --abort returns the branch to its original state, and git rebase --skip drops the conflicting commit.

What's the difference between git pull and git pull --rebase?

A plain git pull merges the fetched commits into your branch. With --rebase, your local commits are replayed on top of the fetched ones instead. You can make that the default with git config --global pull.rebase true.

Did this solve it?

One tap. No account.

Saved you an hour? Send a tip.

USDC or ETH on Base. Keeps the notes free and the examples running.

Base
QR code with the CoreMesh tip address on Base
0x9dFD2Ee86182f92464b50809C6702B999a0Cd54f
Open in wallet · 5 USDC

On a computer? Scan the code with your phone wallet, or copy the address.

USDC or ETH on Base works best. Other tokens on Base, Ethereum or Arbitrum also reach this address. Tips are optional and unlock nothing; every guide stays free.

Discussion

Start the discussion

`code` and ``` blocks work

No account needed. Comments are read before they show up, usually the same day. Your email is never asked for.

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS