Table of Contents

Introduction

Of all the everyday Git commands, rebase is the one people are most often told to be afraid of. The fear is half right. Rebase does rewrite history, and there are situations where that causes real trouble. But what it does is simple once you watch it: it replays commits, one at a time, onto a new starting point.

In this article, we'll:

  1. Watch git rebase main replay three commits, one by one
  2. See why each replayed commit has a different hash from the original
  3. Cover the rule for when rebasing is safe, and how to get the original commits back

What is git rebase?

git rebase <upstream> takes the commits on your current branch that <upstream> doesn't have, and re-applies each one, in order, on top of <upstream>'s tip. Each re-applied commit is a new commit: same change, same message, same author, but a different parent, and so a different hash. When it's done, your branch points at the last new commit, and the originals are no longer on the branch.

The result reads as if you had started your branch from where <upstream> is now.

Watch it happen

Our sample repo has feature checked out; main has gained two commits since the split. Here's git rebase main with feature checked out, one commit at a time:

  1. Before: HEAD is attached to feature at 1117a34, whose three commits branch off 96c4fc2. main is two commits further along at 8c02d5b.
  2. Git replays "Add search box" onto 8c02d5b, the tip of main, as a new commit with the same changes and message but a new id.
  3. "Fix typo in search box" is replayed on top of that as another new commit.
  4. "Add search tests" is replayed on top of that.
  5. feature moves to the last new commit, and HEAD moves with it. The three original commits are now reachable only from the reflog.

Before and after

Before, feature and main diverge at 96c4fc2. After, feature is a straight line of three commits sitting on top of main's tip. Git reports the replay as it goes:

The raw git output, if you want to read along in text

git log --oneline --graph --all

before

* 1117a34 (HEAD -> feature) Add search tests
* e5869f0 Fix typo in search box
* fc19889 Add search box
| * 8c02d5b (main) Update dependencies
| * a0b2db3 Add user settings page
|/  
* 96c4fc2 Fix header layout
* ae65976 Add login page
* 3e1ffe4 Add project skeleton
* 4114b2c Initial commit

after

* 8e95904 (HEAD -> feature) Add search tests
* 6c2bb67 Fix typo in search box
* 5ae3154 Add search box
* 8c02d5b (main) Update dependencies
* a0b2db3 Add user settings page
* 96c4fc2 Fix header layout
* ae65976 Add login page
* 3e1ffe4 Add project skeleton
* 4114b2c Initial commit

what git printed

Rebasing (1/3)
Rebasing (2/3)
Rebasing (3/3)
Successfully rebased and updated refs/heads/feature.

Look at the hashes in the after log. 1117a34, e5869f0 and fc19889 are gone from feature, replaced by three new ones. A commit's hash covers its parent, and every one of these has a new parent.

The one rule

Rebase rewrites your branch. If nobody else has the old commits, that's fine. If the branch has been pushed and someone has pulled it, they now have commits that your branch no longer contains, and the next time you both push there will be a mess to untangle. Pro Git's chapter on rebasing walks through one.

So: rebase branches that are yours alone, freely. Merge branches that other people have. When you do need to push a rebased branch, git push --force-with-lease at least refuses to overwrite work you haven't seen.

I rebase my own feature branches onto main constantly, usually right before opening a pull request, so the diff is against current code and the history reads cleanly. The one time I rebased a branch a colleague was also committing to, we spent a morning sorting out duplicate commits. That was enough to make the rule stick.

Is it safe?

Caution git-sim pre-flight

Replays 3 commit(s) from feature onto main — every replayed commit gets a NEW hash.

The way back

  • Original commits stay in the reflog: git reset --hard 1117a34

The originals are not deleted. They're unreachable from feature, but they stay in the object database and the reflog knows them, so a rebase you regret can be undone for weeks. What can't be recovered is other people's confidence, if the branch was shared.

How to undo it

The reflog has the pre-rebase tip:

git reflog
8e95904 HEAD@{0}: rebase (finish): returning to refs/heads/feature
...
1117a34 HEAD@{5}: checkout: moving from main to feature
git reset --hard 1117a34

puts feature back on the original three commits. ORIG_HEAD points there too, right after a rebase.

Try it on your repository

pip install git-sim
git-sim rebase main

git-sim replays the commits on your own graph, one by one, and tells you whether any of them will conflict, before anything is rewritten.

Common questions

What does git rebase do?

It re-applies your branch's commits, one at a time, on top of another branch's tip, creating new commits with new hashes. Your branch then points at the last new commit. The git rebase documentation covers the options this page skips, such as --autosquash and --rebase-merges.

Why does git rebase change commit hashes?

A commit's hash covers its parent. Each replayed commit has a different parent than the original, so its hash is different even though the change and message are the same.

When should I not rebase?

When the branch has been pushed and others have based work on it. Rewriting shared history leaves them with commits your branch no longer has.

How do I undo a git rebase?

git reflog shows the branch tip from before the rebase, and git reset --hard <that-sha> restores it. git reset --hard ORIG_HEAD does the same immediately after a rebase.

Summary

In this article, we watched git rebase replay three commits onto main one at a time, saw the originals drop off the branch and every new commit get a new hash, and covered the one rule about shared branches.

Next steps

git rebase -i lets you edit the commits as they're replayed. git merge is the alternative that never rewrites anything.