← All posts

Git Merge, Rebase & Squash — Part 3: Rebase — Rewriting History Safely

Data-Sci & Digital HealthBasic InfoTech & Computing Nexus
Git Merge, Rebase & Squash — Part 3: Rebase — Rewriting History Safely
On this page

Abstract

Managing a coherent project history in version control requires techniques that go beyond simply joining divergent lines of development. While a standard merge preserves the branching shape of the history, it can produce a complicated commit graph, and a squash collapses multiple changes. A rebase operation addresses this by recreating the commits of a current branch directly on top of a new base, facilitating a clean fast-forward merge by flattening the history into a straight line. Because the parent of the original commits changes, Git must generate new commit hashes, meaning a rebase fundamentally rewrites history rather than moving existing snapshots. Consequently, developers must adhere to the golden rule of never rebasing public or shared branches, restricting this strictly to private feature branches or local commits. When updating a remote repository, developers must utilize the --force-with-lease flag instead of a plain --force to prevent destroying a colleague's work. This article elucidates the underlying mechanics of rebasing, contrasts it with merging, and establishes essential safety protocols.


Infographic visual summary of Git Merge, Rebase & Squash — Part 3: Rebase — Rewriting History Safely
Visual summary · ภาพสรุป

In Parts 1 and 2 we saw that a merge joins two lines of history and that a squash collapses many commits into a single one. Part 3 is about rebase — the most powerful and the most demanding of the three, because it does not merely join history, it rewrites it.

By the end of this part you will understand how rebase works at the level of the commit graph, why commit hashes change, how rebase differs from merge, and — most important of all — the safety rule for which branches you may rebase and which you must never touch, together with how to bring a feature branch up to date with main correctly using --force-with-lease.

What rebase is: replaying commits onto a new base

Rebase means:

Take the commits of your current branch and recreate them on top of a new base.

Suppose we are in a situation where both the feature branch and main have moved forward independently:

          D---E  feature/login
         /
A---B---C---F---G  main

While we were building D and E on feature/login, main gained F and G. If we want our feature to sit on top of main's latest work without creating a merge commit, we use rebase, standing on the feature branch:

git switch feature/login
git rebase main

Git takes the changes in D and E and recreates them, one at a time, as new commits on top of G:

ABCFGD′E′DEfeature/loginabandoned
Rebase replays D and E onto G as new commits D′ and E′; the original D and E are abandoned.

We can then bring the feature into main as a fast-forward, because the feature now continues directly from main's tip:

git switch main
git merge --ff-only feature/login

The result:

A---B---C---F---G---D'---E'  main

The history is now a straight line — no branching, and no merge commit.

Why D becomes D′

The point that trips many people up is this: why, after a rebase, do the commits become D′ and E′ rather than the original D and E? The answer lies in how Git computes a commit hash.

A commit hash is computed from several ingredients together, including:

When rebase changes the parent of D from C to G:

before: parent(D)  = C
after:  parent(D') = G

Once the parent changes, one of the hash's ingredients changes, so the hash must change too:

D ≠ D'
E ≠ E'

This is why rebase counts as a rewrite of history — Git does not move the old commits to a new location; it creates a fresh set of commits with the same content in their place. The original commits (D, E) become commits that no branch points to any more, and are eventually garbage-collected.

Merge vs Rebase

Merge and rebase both solve the same problem — "how do I bring two lines of work together?" — but they produce completely different graphs.

Merge — keeps the real graph

A merge creates a merge commit and preserves the branching shape of the history:

ABCFGDEM
Merge keeps the branch shape: D–E rejoins main through the two-parent merge commit M.

You can still clearly see that D and E once formed a separate feature branch that was merged back at M.

Rebase — flattens into a line

A rebase moves the commits into a single straight line, with no merge commit:

ABCFGD′E′
Rebase flattens everything into one straight line — no merge commit.

Side-by-side comparison

Aspect Merge Rebase
Keeps the original commits Yes No
Commit hash Unchanged Changed
History shape Branched Linear
Creates a merge commit May create one Not from the rebase itself
Suitable for a shared branch Yes Use with caution
Conflicts Usually resolved once May be resolved commit by commit
Visibility of feature history Clear Less clear than merge

The golden rule of rebase

Do not rebase commits that other people are already building on — unless the team has agreed to it.

Because rebase creates a new set of commit hashes on top of the old ones, if someone else is referencing or has branched off the original commits, their history stops matching yours the moment you rebase. Here is an example of what you should not do:

git switch main
git rebase some-branch
git push --force

If main is a public/shared branch, doing this throws everyone's history out of sync and is hard to recover from.

Generally safe to rebase

Generally should not rebase

Rebasing a feature branch onto main

This is the most common use case for rebase: updating your feature branch so it sits on top of main's latest work before you open or merge a PR. The steps are — bring main up to date first, then rebase the feature on top of it:

git switch main
git pull

git switch feature/login
git rebase main

Once the rebase is done, the feature's commit hashes have changed. If you had pushed this branch to the remote earlier, this push is no longer a fast-forward, so it has to overwrite — but it must overwrite safely:

git push --force-with-lease

You should not use:

git push --force

The difference matters a great deal: --force-with-lease first checks whether the remote has new work from someone else that you have not seen yet; if it does, it refuses the push so you cannot accidentally overwrite their work. A plain --force overwrites immediately without checking anything, which risks silently destroying a colleague's commits. So on any branch you have already pushed, make --force-with-lease your default.

Key takeaways

0
Message for International and Thai ReadersUnderstanding My Medical Context in ThailandRead more →Message for International and Thai ReadersUnderstanding My Broader Content Beyond MedicineRead more →

Comments

No comments yet. Be the first to share your thoughts.

Sign in to comment