git rebase Basics

git rebase takes a sequence of commits and replays them, one by one, on top of a different base commit. Instead of tying two branches together with a merge commit, rebase rewrites your branch so it looks like you started your work from a newer point in history, producing a clean, linear commit log. It is one of the most useful tools in Git for keeping history readable — and one of the most dangerous, because it does not add history, it rewrites it. This lesson explains what actually happens to commits, branches, and HEAD during a rebase, how to resolve conflicts mid-rebase, and the golden rule that keeps rebase from destroying other people’s work.

Overview / How it works

To understand rebase you first need to understand what a commit really is. Every commit object in Git stores four things: a pointer to a tree (a snapshot of the project’s file structure at that moment), a pointer to its parent commit (or two parents, for a merge commit), the author/committer metadata, and the commit message. Git hashes all of that content with SHA-1 to produce the commit’s ID. A branch like main or feature/login-page is nothing more than a small file containing a commit SHA — a movable pointer. HEAD is usually a pointer to a branch (a “symbolic ref”), which is how Git knows which branch moves forward when you commit.

Because a commit’s hash is derived partly from its parent’s hash, changing a commit’s parent changes that commit’s own hash — and therefore the hash of every commit that comes after it. This is exactly what rebase exploits. When you run git rebase main on a feature branch, Git does not modify your existing commits; it creates brand-new commits with the same changes (same tree diffs, same messages by default) but with new parents and therefore new SHAs. The old commits are not deleted immediately — they become unreachable from any branch, sit in the repository as dangling objects, and are recoverable through git reflog for a period (typically 90 days) before Git’s garbage collector prunes them.

Compare this to git merge, which creates one new commit with two parents, preserving the exact history of both branches as it happened. Rebase instead produces a history that looks as if you had written your feature branch’s commits starting from the newer main all along — there is no merge commit, and the log reads top to bottom as a single straight line. During the operation, Git temporarily detaches HEAD and moves it commit by commit as each patch is replayed, landing your branch pointer on the final new commit once the rebase finishes successfully.

Syntax

git rebase [-i] [--onto <newbase>] <upstream> [<branch>]
git rebase --continue
git rebase --abort
git rebase --skip
  • <upstream> — the base commit or branch you want to replay your commits onto (commonly main).
  • [<branch>] — optional; if given, Git switches to this branch first, then rebases it. If omitted, the current branch is rebased.
  • -i, –interactive — opens an editable list of the commits being replayed, letting you reorder, reword, squash, or drop them.
  • –onto <newbase> — replays commits onto a different commit than the one Git would otherwise use as the base, useful for moving a branch off one parent onto another.
  • –continue — resumes a paused rebase after you have resolved a conflict and staged the fix.
  • –abort — cancels the rebase entirely and restores the branch to exactly where it was before you started.
  • –skip — skips the commit currently causing a conflict entirely, discarding its change.

Examples

Example 1: A simple rebase onto an updated main

Your feature branch was created a few days ago, and main has since gained new commits from teammates. Before opening a pull request, you rebase to bring your branch up to date:

git switch feature/login-page
git rebase main

Output:

Successfully rebased and updated refs/heads/feature/login-page.

Git found the common ancestor of feature/login-page and main, temporarily set your commits aside, moved your branch pointer to the tip of main, and replayed each of your commits as new commits on top. Because there were no overlapping changes, every commit applied cleanly and the rebase finished in one step.

Example 2: Cleaning up commits with interactive rebase

Your branch has three messy work-in-progress commits you want to squash into one clean commit before review:

git switch feature/login-page
git rebase -i HEAD~3

This opens your editor with a list like:

pick a1b2c3d feat: add login form
pick e4f5g6h fix: correct label typo
pick i7j8k9l style: adjust input spacing

Changing the last two lines from pick to squash (or s) tells Git to fold those commits into the one above, then prompts you to write a combined commit message. After saving, your three commits become one: feat: add login form, with a single clean diff replacing the three original patches.

Example 3: Rebase with a conflict

Two people touched the same file. Rebasing your branch onto main hits a conflict partway through:

git switch feature/payment-retry
git rebase main

Output:

Auto-merging src/payments/retry.js
CONFLICT (content): Merge conflict in src/payments/retry.js
error: could not apply 9f2e1ab... fix: add retry backoff
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <pathspec>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".

Git pauses mid-replay and leaves conflict markers in the affected file so you can decide how to combine the two versions:

<<<<<<< HEAD
const MAX_RETRIES = 5;
=======
const MAX_RETRIES = 3;
>>>>>>> 9f2e1ab (fix: add retry backoff)

After editing the file to the correct value and removing the markers, stage it and tell Git to continue replaying the remaining commits:

git add src/payments/retry.js
git rebase --continue

If a conflict looks too tangled to resolve calmly, you can always back out completely and think it over:

git rebase --abort

This restores feature/payment-retry to exactly the state it was in before you ran git rebase main — no partial changes are left behind.

How it works step by step

Walking through git rebase main on a feature branch with three unique commits:

  1. Git finds the merge base — the most recent commit shared by feature/login-page and main.
  2. Git saves each commit unique to feature/login-page (everything after the merge base) internally as a patch, in order.
  3. Git detaches HEAD and moves it to the tip of main.
  4. Git applies the first saved patch on top of that new position, creating a brand-new commit object with a new SHA-1 (same tree contents, but a new parent).
  5. It repeats step 4 for each remaining patch. If a patch fails to apply cleanly, Git stops here, leaves the working tree with conflict markers, and waits for git rebase --continue, --skip, or --abort.
  6. Once every patch has been applied, Git moves the feature/login-page branch pointer to the last new commit and reattaches HEAD to that branch. The original three commits are now unreachable from any branch, though still recoverable via git reflog until garbage collected.

Common Mistakes

Mistake 1: Rebasing a branch other people have already pulled. This is Git’s golden rule of rebasing: never rebase commits that exist on a branch someone else has based their own work on. Because rebase replaces old commits with new ones (different SHAs, same content), anyone who already has the old commits will end up with a history that has diverged from yours, causing duplicated commits and confusing merges when they next pull.

git switch main
git rebase feature/experimental
git push --force origin main

Rewriting the shared main branch and force-pushing over it silently overwrites whatever your teammates had pushed in the meantime — their commits can be lost from the remote entirely. Only rebase branches that are still private to you, typically your own local feature branches before you open a pull request.

Mistake 2: Force-pushing with bare --force instead of --force-with-lease. After any rebase, the remote copy of your branch no longer matches your rewritten local history, so a normal git push is rejected and you need to force it. Bare --force overwrites the remote unconditionally, even if someone else pushed commits to that branch since you last fetched.

git push --force-with-lease origin feature/experimental

--force-with-lease checks that the remote branch still points where you last saw it before overwriting it, and refuses if it doesn’t — protecting against silently discarding a teammate’s work.

Mistake 3: Leaving conflict markers in a commit. If you stage a file and run git rebase --continue without actually removing the <<<<<<</=======/>>>>>>> markers, Git happily commits the markers as literal text, breaking the file. Always review the resolved file before staging it.

Best Practices

  • Never rebase a branch that others have already fetched, pulled, or built work on top of — only rewrite history that is still local and private to you.
  • Prefer git push --force-with-lease over bare --force whenever you push a rewritten branch.
  • Rebase your feature branch onto the latest main before opening a pull request, so the PR shows a clean, linear diff.
  • Use git rebase -i to squash noisy work-in-progress commits into meaningful, well-described commits before requesting review.
  • If a rebase turns into a mess of conflicts you don’t understand, run git rebase --abort and reconsider your approach rather than pushing through blindly.
  • Write commit messages that follow a convention such as Conventional Commits (feat:, fix:, chore:) so that squashed or reworded commits stay easy to scan in the log.
  • Use git reflog as your safety net — a botched rebase almost always leaves the original commits recoverable for a while.

Practice Exercises

Exercise 1: Create a local repository, make a commit on main, then create feature/nav-bar and add two commits to it. Switch back to main, add one more commit there, then rebase feature/nav-bar onto main. Confirm with git log --oneline --graph --all that the history is now linear and your feature commits have new SHAs.

Exercise 2: On a branch with four small commits, use git rebase -i HEAD~4 to squash the last three into the first, ending up with one commit whose message summarizes all four changes.

Exercise 3: Deliberately edit the same line of the same file on both main and a feature branch, then rebase the feature branch onto main. Resolve the resulting conflict, run git rebase --continue, and verify the file has no leftover conflict markers before your final commit.

Summary

  • git rebase replays your branch’s unique commits onto a new base, creating brand-new commits with new SHAs rather than reusing the old ones.
  • A branch is just a movable pointer to a commit; rebase moves that pointer to the newly replayed commits once the operation finishes.
  • Old commits become unreachable after a rebase but remain recoverable through git reflog for a time.
  • Conflicts pause the rebase mid-replay; resolve them, stage the file, and run git rebase --continue, or bail out completely with git rebase --abort.
  • Never rebase a branch that others have already pulled or built on — that’s the golden rule.
  • When force-pushing a rebased branch, use --force-with-lease instead of bare --force.