🔧 Git & GitHub FoundationTheory · Part 6 of 7

"Rebase rewrites history to make it look like you always built on the right foundation. Reflog is the safety net that means that rewrite is never permanent."

What Rebase Does — The Mental Model

Merge preserves history as it actually happened: two branches diverged, two timelines ran in parallel, they converged into a merge commit. That's accurate, but it produces a tangled graph that's harder to read at a glance.

Rebase answers the question: "What would my branch's commits look like if I'd started from the current tip of main instead of an older commit?" It replays each of your branch's commits on top of the new base, in order, creating new commit objects with new hashes and new parent pointers.

Before rebase:
  main:    A ← B ← C
  feature: A ← D ← E

After git rebase main (run on feature):
  main:    A ← B ← C
  feature: A ← B ← C ← D' ← E'

D' and E' are new commits. They contain the same changes as D and E (the diffs are identical) but their hashes are different because their parent is now C instead of A. The original D and E still exist in the object store until Git's garbage collector runs — they're just unreachable from any branch or tag.

Why this produces a fast-forward merge. After the rebase, feature is directly ahead of main with no divergence. Running git merge feature from main results in a simple pointer advance — a fast-forward. The log shows a perfectly linear history: main's commits, then your feature commits, no merge commit, no fork-and-rejoin.

Running a Rebase

git switch feature/my-work   # be on the branch being rebased
git rebase main               # replay feature's commits onto main's tip

If any of your commits touch the same lines that main's new commits touched, Git pauses the rebase and marks conflicts exactly like a merge conflict. Resolve the file, then:

git add <resolved-file>
git rebase --continue          # replay the next commit

If the conflict is too complex or you changed your mind:

git rebase --abort             # returns everything to pre-rebase state

After the rebase completes, merge into main:

git switch main
git merge feature/my-work      # fast-forward: no merge commit created

Interactive Rebase — Rewriting Your Own Commits

The most practical use of interactive rebase is cleaning up a messy sequence of commits before pushing or merging. It lets you edit the last N commits in any order.

git rebase -i HEAD~3   # opens an editor with your last 3 commits listed

The editor shows each commit prefixed by an action keyword. The available actions:

  • pick — keep the commit as-is (the default)
  • reword — keep the commit's changes, but edit its message
  • squash — fold this commit into the previous one; opens editor for a combined message
  • fixup — like squash but discards this commit's message entirely, keeping the previous one
  • drop — remove this commit from history completely

The commits are listed oldest-first (top = oldest, bottom = newest). Reorder lines to reorder commits. Save and close the editor to execute.

The practical workflow: pick your meaningful commits, fixup the "fix typo" and "oops, forgot to save" commits that followed them. Before pushing, your three-session afternoon of work looks like two clean, intentional commits.

The Golden Rule — Never Rebase Shared History

Rebase rewrites commit hashes. Every teammate who has pulled commits D and E from the remote now has commits that no longer exist by those hashes in your repository. When they next pull or push, Git sees two diverged histories and creates a confusing tangled merge — or rejects the push entirely.

The rule is simple: never rebase commits that have already been pushed to a shared branch. Rebase is for cleaning up your private, local, not-yet-shared work. Once commits are on origin/main or any branch your teammates have pulled from, treat them as immutable.

If you've already pushed a branch and then rebased it locally, pushing requires:

git push origin feature/my-work --force-with-lease

--force-with-lease checks that the remote branch hasn't received new commits from someone else since your last fetch. If it has, the push is rejected. This is strictly safer than --force, which will overwrite whatever is on the remote regardless. On a solo feature branch that only you push to, --force-with-lease is the right tool for post-rebase pushes.

git reflog — The Time Machine You Didn't Know You Had

Git keeps a private log of every position HEAD has occupied, regardless of whether a branch pointer tracked it. This is the reflog. It records: branch switches, commits, rebases, resets, merges, cherry-picks — every movement of HEAD.

git reflog

Output:

c3f9a14 HEAD@{0}: reset: moving to c3f9a14
7e2b3d8 HEAD@{1}: commit: Add scratch notes
c3f9a14 HEAD@{2}: checkout: moving from feature to main
4a1d9f2 HEAD@{3}: commit: Update todos.txt with progress

HEAD@{N} is the notation: 0 is the current position, 1 is the previous, and so on. The reflog is local to your machine and is not pushed to any remote. Entries expire after 90 days by default.

Recovery Patterns With Reflog

Scenario 1: You ran git reset --hard to an older commit and lost some commits.

The commits aren't deleted — they're just unreachable from the branch. Find them in reflog:

git reflog           # find the hash of the lost commit (e.g., 7e2b3d8)
git reset --hard 7e2b3d8   # move HEAD back to that commit

Scenario 2: You deleted a branch without merging it.

git branch -D experiment   # branch gone, commits orphaned
git reflog                 # find the tip commit of the deleted branch
git switch -c experiment-recovered 7e2b3d8   # recreate branch at that commit

In both scenarios, the key insight is the same: Git only truly loses objects after the garbage collector runs and the reflog entries for them have expired (90 days). Until then, every commit you've ever made is findable via git reflog.

This is why the common advice "I accidentally deleted my branch / reset my work" is almost always recoverable in Git. The question is always: can you find the hash before it expires?

🎯 Quick Check

Q1: After a rebase, you try to push your feature branch and Git rejects it. Why, and what's the safe fix?

Show Answer

Rebase created new commit objects with new hashes. The remote still has the old commits. Git sees the remote and local as diverged histories and rejects a normal push. The safe fix is git push origin feature/my-work --force-with-lease. This overwrites the remote branch but first checks that no one else pushed new commits to it since your last fetch — preventing accidental overwrites of teammates' work.

Q2: What is HEAD@{2} in reflog output?

Show Answer

It's where HEAD was two movements ago — not two commits ago, but two HEAD transitions ago. A "movement" is any operation that changed where HEAD pointed: a commit, a branch switch, a reset, a rebase step. HEAD@{0} is now, HEAD@{1} is one operation back, HEAD@{2} is two operations back.

Q3: A teammate has already pulled your feature branch. Can you safely rebase and force-push it?

Show Answer

No. Your teammate has the old commit hashes. When you force-push the rebased branch, those hashes no longer exist at the remote. Your teammate's next pull will result in a confusing diverged history that requires a merge to untangle — the opposite of the clean linear history rebase was supposed to provide. Rebase is only safe on branches that no one else has fetched.

Key Takeaways

  • Rebase replays your branch's commits on top of the new base, producing new hashes — identical diffs, different parents
  • After rebase, merging into main is a fast-forward: no merge commit, linear history
  • Interactive rebase (-i HEAD~N) lets you pick/squash/reword/drop your last N commits before sharing
  • Never rebase commits already on a shared branch — it rewrites history that teammates have pulled
  • --force-with-lease is the safe alternative to --force for post-rebase pushes on solo branches
  • Reflog records every HEAD position for 90 days — almost any "lost" commit or branch is recoverable
🛠
Hands-On Practice
Ready to practice what you just read?
Go to Tutorial →