Rebase Squash Example
What Will We Learn?
Practically Learn Commit Squashing Using Rebase Squash.
Don’t Dare to Miss How Commit Squashing Happens.
Commit Squashing
Commit squashing is the process of combining two or more Git commits into a single commit. It is commonly used to clean up a branch before merging it into the main branch, so that the Git history contains fewer and more meaningful commits.
Use Case of Commit Squashing
- Keeping Git history clean — For example, 10 small commits made while developing a feature can be combined into one
Add user logincommit. - Removing unnecessary commits — Temporary commits such as
Fix typo,Debug, orTry againcan be combined into the final meaningful commit. - Making code reviews easier — Instead of reviewing many small commits, a reviewer can review one complete commit containing the entire feature.
- Grouping related changes — For example, commits for creating a login page, adding authentication, and fixing login errors can be squashed into one
Implement user authenticationcommit. - Making changes easier to revert — If the entire feature needs to be removed later, you can revert the single squashed commit instead of reverting multiple commits.
- Maintaining a professional commit history — Before merging a feature branch, developers can squash development commits so the main branch contains a clear and understandable history.
Example of Commit Squashing
The Setup
To follow along, setup running the following commands:
mkdir rebase-squash
cd rebase-squash
git init
echo A > file; git add file; git commit -m "A"
echo B > file; git add file; git commit -m "B"
echo C > file; git add file; git commit -m "C"
echo D > file; git add file; git commit -m "D"Squash Using Interactive Rebase
Identifying Base Commit
Before squashing commits, you need to identify how many commits you want to squash. See the commit history.
git log --onelineFor our example, it looks similar to following:
6ec3da2 (HEAD -> master) D
7b12a8c C
4806848 B
63a7352 AStart Interactive Rebase For Squashing
Let’s say we want to squash top 3 commits. Hence we need to choose the base commit just below them which is 63a7352. You can also use HEAD~3 that points to the same commit. Actually, for Top N or Last N, HEAD~N reads better:
git rebase -i HEAD~3Apply squash Command
Interactive rebase opens Git’s default text editor which is actually a way of asking you to choose which actions you want to preform. At first, it should look similar to following:
pick 4806848 B
pick 7b12a8c C
pick 6ec3da2 D
# Rebase 63a7352..6ec3da2 onto 63a7352 (3 commands)Because we want to squash all three into one, we will pick the first one (to squash onto) and squash the rest. Edit and save file as:
pick 4806848 B
squash 7b12a8c C
squash 6ec3da2 D
# Rebase 63a7352..6ec3da2 onto 63a7352 (3 commands)Git now apply the commands and squash three commits into one.
Edit Commit Message
Git will again open editor to save the squashed commits with a commit message:
# This is a combination of 3 commits.
# This is the 1st commit message:
B
# This is the commit message #2:
C
# This is the commit message #3:
DYou can keep the default or write more meaningful message.
Squashing Completed
Git commits with the above commit message to end the process.
What Happens During Commit Squshing (Rebase)
The easiest way to understand the rebasing process for squashing is to simulate exactly what Git does, one step at a time.
Your original history is:
63a7352 A
|
4806848 B
|
7b12a8c C
|
6ec3da2 D <-- HEADAnd the file at each commit is:
| Commit | file contains |
|---|---|
| A | A |
| B | B |
| C | C |
| D | D |
Your todo is:
pick 4806848 B
squash 7b12a8c C
squash 6ec3da2 DThe crucial thing is that rebase starts by checking out the base (A) and then replays the selected commits as patches.
Step 1 — Rebase identifies the base
You started the rebase:
git rebase -i HEAD~3Git identifies:
base = 63a7352 A
commits to replay = B, C, DSo conceptually Git starts at A or HEAD~3:
and file contains:
ASince this is the base commit, there is no conflict yet.
Step 2 — Git takes commit B (first one in the list)
Git doesn’t primarily think:
“Commit B has file content
B; let’s putBinto the file.”
Instead, it looks at the change introduced by B.
B’s parent is A:
A <- BSo Git calculates approximately:
git diff A BThe result is:
-A
+B
Current state (the content of file in current commit A:):
APatch:
-A
+B
Git looks at the current file and says:
“I need to find
Aand replace it withB.”
It finds exactly that:
current:
ATherefore:
A
↓ apply -A +B
BNo conflict.
At this point the rebase has effectively produced:
A → B'Notice that B' is a new commit, not the original 4806848.
Step 4 — Now Git processes squash C (second on the list)
Your todo says:
squash 7b12a8c CGit looks at the original C commit.
C’s parent is B:
B <- CSo Git calculates C’s patch:
-B
+C
What’s the current state?
It’s:
Bbecause we just applied B.
So Git applies:
current:
B
patch:
-B
+CThe B matches perfectly.
Therefore:
B
↓ apply -B +C
CStill no conflict.
But because this was squash, Git doesn’t create a separate final commit for C. It combines C’s change with the commit being constructed from B.
Step 5 — Now Git processes squash D
D’s parent is C:
C <- DTherefore D’s patch is:
-C
+D
What’s the current state?
CSo Git applies:
current:
C
patch:
-C
+DAgain, exact match.
Result:
C
↓ apply -C +D
DNo conflict.
What does the final result look like?
You’ve effectively done:
A
↓ B's patch
B
↓ C's patch
C
↓ D's patch
DBut because C and D were marked squash, Git combines everything into one new commit.
So your final history becomes conceptually:
A ── B+C+DAnd file contains:
DThe final squashed commit’s overall diff relative to A is:
-A
+D
The Important Mental Model
Think of each commit as a patch between its parent and itself:
A → B
-A
+BB → C
-B
+CC → D
-C
+DDuring the rebase, Git applies those patches sequentially:
Start:
A
Apply B:
-A
+B
↓
B
Apply C:
-B
+C
↓
C
Apply D:
-C
+D
↓
D
flowchart LR
subgraph Start
A
end
subgraph After_B
B
end
subgraph After_C
C
end
subgraph After_D
D
end
Start -.->|"Apply<br>-A<br>+B"| After_B
After_B -.->|"Apply<br>-B<br>+C"| After_C
After_C -.->|"Apply<br>-C<br>+D"| After_D
Every patch is being applied to exactly the state that its original parent had.
That’s why there is no conflict.
Now here’s when a conflict WOULD happen
Suppose your history were:
A
|
X
|
Bwhere X changed the file to:
XNow suppose you tried to replay B’s patch:
-A
+B
onto X.
Git would have:
current file:
Xbut the patch is looking for:
-A
+BThere is no A to replace.
That’s where Git may say:
CONFLICT (content): Merge conflict in fileSo the key question isn’t:
“Does the commit I’m replaying modify the same file as the base?”
Instead ask:
“Can Git apply this commit’s patch cleanly to the current state?”
In your case:
B's patch expects A → current is A ✓
C's patch expects B → current is B ✓
D's patch expects C → current is C ✓Therefore:
NO CONFLICTAnd that’s also why changing pick to squash doesn’t itself introduce a conflict. squash still applies the commit’s patch; it just tells Git to combine the resulting commit with the previous one.
Little BUT Important Nuances
Squashing commits can make your Git history cleaner, but there are a few things to watch out for:
You might lose useful history
- Squashing combines several commits into one, so the smaller steps and their individual messages are no longer visible.
- If those commit messages explain important changes, it may be better to keep them.
- Before squashing, ask yourself whether you’ll need that detailed history later.
You may run into conflicts
- A rebase can sometimes cause merge conflicts, especially when the commits touch similar parts of the code.
- If Git stops because of a conflict, fix the affected files first.
- Then stage the resolved files and continue the rebase.
- If there are several conflicts, the process can take a bit of extra time, so don’t rush through it.
Squashing lots of commits can get messy
- Interactive rebase isn’t always easy to manage when you’re dealing with a huge number of commits.
- If you have many commits to combine, it can be easier to do them in smaller batches.
- You can also use Git tools or IDE features that make rebasing easier to handle.