Definition
- Rebase -- replays your branch's commits one by one on top of a new base commit, producing new commit objects with new hashes.
- Linear history -- a straight line of commits with no merge commits, easier to read and bisect.
- Interactive rebase -- git rebase -i lets you squash, reword, reorder, or drop commits before sharing.
- History rewrite -- because rebase creates new commits, the old commit identities are replaced, which is dangerous on shared branches.
- force-with-lease -- a safer alternative to force-push that refuses to overwrite if the remote changed since your last fetch.
- Reflog -- a local log recording every position HEAD has pointed to, used to recover lost commits after a bad rebase.
Imagine you started drafting ledger corrections at National Bank of India based on Monday's official ledger. By Friday the official ledger has moved on. Rebasing is like rewriting your corrections fresh, as if you had started from Friday's up-to-date ledger, so they slot cleanly on top with no merge note. The danger is that if you rewrite entries a colleague has already copied, their copy no longer matches the official record and the books disagree.
Understanding the Concepts
What is rebase and how does it work?
When your feature branch and main both have new commits, they diverge. Merge joins them with a merge commit. Rebase instead takes your commits, sets them aside, fast-forwards to the tip of main, and re-applies your commits one at a time on top. The result looks as if you started from the current main all along -- a single straight line.
# Before rebase:
# main: A -- B -- C
# feature: \-- D -- E
git checkout feature/emi-calculation
git rebase main
# After rebase:
# main: A -- B -- C
# feature: -- D' -- E'
# D' and E' are NEW commits with new hashes
The crucial technical fact is that rebase creates brand-new commit objects. A commit's hash depends on its content and its parent, so when the parent changes, the hash changes.
Why does rebase give a clean history?
After rebasing feature branches onto main before merging, git log reads like a straight story with no tangle of merge commits. This makes the history easier to read, easier to bisect when hunting a bug, and easier to audit.
# History with merge commits (tangled)
git log --oneline --graph
* f4e5d6 Merge branch 'feature/emi-calculation'
|\
| * c3d4e5 add EMI edge case test
| * b2c3d4 add EMI calculation
* | a1b2c3 fix transfer rounding
|/
* 901234 initial commit
# History with rebase (clean linear)
git log --oneline --graph
* d5e6f7 add EMI edge case test
* c4d5e6 add EMI calculation
* a1b2c3 fix transfer rounding
* 901234 initial commit
How does interactive rebase work?
Interactive rebase (git rebase -i) lets you reshape your local commits before sharing. You can squash messy work-in-progress commits into one meaningful commit, reword confusing messages, reorder commits, or drop a commit that should not be there.
# Squash last 3 commits into one
git rebase -i HEAD~3
# Editor opens:
pick abc1234 WIP: start EMI formula
pick def5678 fix: typo in EMI
pick ghi9012 add BigDecimal rounding
# Change to:
pick abc1234 WIP: start EMI formula
squash def5678 fix: typo in EMI
squash ghi9012 add BigDecimal rounding
# Save and close -- Git combines into one commit
package com.bank.loans;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* EMI calculator -- this is the final clean version
* after interactive rebase squashed 3 messy commits.
*/
public class EmiCalculator {
public BigDecimal calculateEmi(
BigDecimal principal, BigDecimal annualRate, int months) {
BigDecimal r = annualRate
.divide(BigDecimal.valueOf(1200), 10, RoundingMode.HALF_UP);
BigDecimal onePlusR = BigDecimal.ONE.add(r);
BigDecimal power = onePlusR.pow(months);
return principal.multiply(r).multiply(power)
.divide(power.subtract(BigDecimal.ONE), 2, RoundingMode.HALF_UP);
}
}
[feature/emi-calculation xyz789] feat: add EMI calculator with BigDecimal rounding
1 file changed, 18 insertions(+)
When should you use rebase?
Rebase your own local, unshared feature branch onto the latest main to stay current and keep history linear. Use interactive rebase to clean up local commits before pushing and opening a pull request. Do NOT rebase commits that other people have already pulled.
# CORRECT -- rebase your own private branch onto main
git checkout feature/emi-calculation
git fetch origin
git rebase origin/main
# CORRECT -- clean up local commits before first push
git rebase -i HEAD~3
# WRONG -- rebase a shared branch that others pulled
git checkout main
git rebase feature/something # NEVER do this
What are the risks of rewriting history?
Because rebase replaces commits with new ones, if you rebase a branch that a teammate has already based work on, their history still points at your old commits, which no longer exist on the remote. When they try to integrate, Git sees two divergent histories and produces confusing conflicts.
# Rajesh rebases and force-pushes a shared branch
git rebase origin/main
git push --force origin shared-branch
# Priya still has the old commits
git pull origin shared-branch
Auto-merging src/main/java/com/bank/loans/EmiCalculator.java
CONFLICT (content): Merge conflict in EmiCalculator.java
Automatic merge failed; fix conflicts and then commit the result.
# Priya sees duplicated commits and confusing history
How does force-with-lease protect you?
After rebasing, you must force-push because your local branch diverged from the remote. A raw --force overwrites whatever is on the remote. --force-with-lease is safer because it checks that the remote has not changed since your last fetch.
# WRONG -- raw force can overwrite teammates' work
git push --force origin feature/emi-calculation
# CORRECT -- lease refuses if remote changed
git push --force-with-lease origin feature/emi-calculation
# If Priya pushed to the branch since your last fetch:
! [rejected] feature/emi-calculation -> feature/emi-calculation
(stale info; use 'git fetch' to update)
How does reflog recover from a bad rebase?
Every time HEAD moves, Git records the position in the reflog. After a bad rebase, the old commits are not deleted immediately -- they are unreferenced. You find the entry before the bad rebase and reset back.
# After a bad rebase, find the pre-rebase state
git reflog
abc1234 HEAD@{0}: rebase (finish): returning to refs/heads/feature/emi
def5678 HEAD@{1}: rebase (start): checkout origin/main
ghi9012 HEAD@{2}: commit: add EMI edge case test <-- this is pre-rebase
# Recover by resetting to the pre-rebase commit
git reset --hard HEAD@{2}
What are common mistakes?
The most common mistakes are rebasing shared branches, using --force instead of --force-with-lease, rebasing after already pushing and having teammates pull, and not knowing that reflog can recover from almost any mistake.
# WRONG -- rebasing a branch Priya already pulled
git rebase origin/main # on a shared branch
git push --force
# CORRECT -- only rebase private branches, use lease
git rebase origin/main # on your private branch
git push --force-with-lease
What should a senior engineer know?
A senior engineer knows that many banking teams forbid rebasing shared branches entirely and allow it only on personal feature branches before the first review. They know that reflog has an expiry and unreferenced commits are eventually garbage-collected. They set team conventions: rebase for clean personal history, merge for integrating shared work, and always use force-with-lease.
Visual Overview
Merge vs Rebase ─────────────── Before (branches diverged): main: A ── B ── C feature: \── D ── E After MERGE: main: A ── B ── C ──── M (merge commit, two parents) feature: \── D ── E ──/ History: branching graph with merge commit After REBASE: main: A ── B ── C feature: ── D' ── E' (new commits, new hashes) History: clean straight line Interactive Rebase: ┌─────────────────────┐ ┌─────────────────────┐ │ Before │ │ After │ │ abc WIP start EMI │ │ xyz feat: add EMI │ │ def fix typo │────▶│ calculator with │ │ ghi add rounding │ │ BigDecimal │ │ (3 messy commits) │ │ (1 clean commit) │ └─────────────────────┘ └─────────────────────┘ Force-with-lease Safety: ┌──────────┐ push --force ┌──────────┐ │ Rajesh │──────────────────▶ │ Remote │ overwrites blindly └──────────┘ └──────────┘ ┌──────────┐ push --force ┌──────────┐ │ Rajesh │──── with-lease ──▶ │ Remote │ checks first, └──────────┘ (safe) └──────────┘ rejects if changed
Key Takeaway
Rebasing takes the commits on your branch and replays them one at a time on top of a new base commit, usually the latest main. The crucial point is that it creates brand-new commit objects because a commit's hash depends on its content and its parent. The result is a clean, linear history with no merge commits, which is much easier to read, bisect, and audit.
Interactive rebase is the second powerful use. Before pushing and opening a PR, you can squash messy work-in-progress commits into one meaningful commit, reword messages, or drop mistakes. In a bank, where every commit should map cleanly to a reviewed change, this cleanup makes review and the audit trail much better.
The danger is clear: because rebase replaces commits, if you rebase a branch a teammate has already pulled, their history diverges from the remote. Always use --force-with-lease instead of --force when pushing a rebased branch, and if a rebase goes wrong, git reflog shows where HEAD was before the rebase so you can reset back.
Remember one sentence: rebase your own private branch for a clean linear history, never rebase commits others have pulled, and always use force-with-lease.
---