You've just finished a new feature. You look at your Git history. It probably looks something like this:
f4a3c2e (HEAD -> feature/new-login) address PR comments
a1b3d4e fix typo
e5f6g7h wip
c8d9e0f add initial login form
This is a diary. It shows every step misstep and minor correction you made. While it’s an accurate record of your work it’s not useful. It’s noise.
When a teammate reviews this they have to wade through the noise to understand the substance. Six months from now when you’re using git blame
to figure out why a line of code exists a commit message like “wip” tells you nothing. You've made your own life harder.
The goal of a project’s history is not to be a diary. It’s to be a clear logical story of how the product was built. Each commit should be a single logical change with a clear message.
The secret is to clean up your history before you merge it. Your local feature branch is your workshop. It’s fine for it to be messy while you’re working. But before you show the finished piece to others you clean it up.
The tool for this is interactive rebase. It sounds intimidating but it’s just a way to rewrite your recent history.
Let’s fix that messy log. You’re on your branch feature/new-login
which you branched from main
. You run this command:
# Rebase interactively against the main branch
git rebase -i main
This opens your text editor with a file that looks something like this:
pick c8d9e0f add initial login form
pick e5f6g7h wip
pick a1b3d4e fix typo
pick f4a3c2e address PR comments
# Rebase 72f4a3a..f4a3c2e onto 72f4a3a (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like “squash”, but discard this commit’s log message
# ... and a few others
This is a script. Git is showing you the commits on your branch and asking you what to do with them. By default it will pick
each one which changes nothing. But we want to make changes.
The “wip” “fix typo” and “address PR comments” commits aren’t important stories. They are just small adjustments to the main change which was adding the login form. We should combine them.
The two main commands for this are squash
and fixup
. Both meld a commit into the one before it. The only difference is that squash
prompts you to combine the commit messages while fixup
just discards the message of the commit being squashed. For minor fixes fixup
is perfect.
Let’s change the script. We want to combine everything into that first commit. We will change pick
to fixup
or f
for short for the last three commits.
pick c8d9e0f add initial login form
f e5f6g7h wip
f a1b3d4e fix typo
f f4a3c2e address PR comments
Save and close the editor. Git will now replay the commits combining them as you instructed.
Now if you run git log --oneline
you’ll see just one new commit:
b2c3d4e (HEAD -> feature/new-login) add initial login form
This is much better. But the commit message is still a bit vague.
We can do better. Let’s re-run the interactive rebase.
git rebase -i main
This time instead of using fixup
let’s use reword
on the first commit and squash
on the others. This will give us a chance to write a great new commit message for the combined change.
Change the script to this:
r c8d9e0f add initial login form
s e5f6g7h wip
s a1b3d4e fix typo
s f4a3c2e address PR comments
When you save and close Git will first reword
the initial commit. It will open your editor with the message “add initial login form”. Let’s leave that for now.
Next it will squash
the other commits. It will then open your editor again this time with a combination of all the commit messages and ask you to create the final message for the new single commit.
This is your chance to write a good commit message. A good message has a short summary line followed by a blank line and then a more detailed explanation if needed.
feat: Add user login form
- Creates the UI and state management for the new user login page.
- Validates email and password fields.
- Submits credentials to the authentication endpoint.
- Handles success and error responses from the server.
Save that message. Now your git log
looks perfect. One commit that represents one logical feature with a message that explains what and why.
Interactive rebase is a form of rewriting history. This is powerful but you must follow one rule:
Do not rebase branches that other people are using.
Never rebase main
or develop
or any other shared branch. Only rebase your own feature branches before they are merged. Rebasing a shared branch forces everyone else to do complicated work to fix their own local copies. It’s the fastest way to make your team angry.
What if you've already pushed your feature branch to the remote to open a pull request? If you rebase it locally your local branch and the remote branch will have diverged. Git will tell you to pull
. Do not do this. It will try to merge the old history back in undoing all your cleanup work.
Instead you need to force push. But a plain git push --force
is dangerous. It can overwrite work if someone else pushed to your branch. The safer command is:
git push --force-with-lease
This command tells Git to only force the push if the remote branch is in the state you expect it to be. It’s a small safety net that prevents you from accidentally overwriting changes.
Cleaning your Git history is a simple habit that pays huge dividends. It makes your work easier for others to understand and easier for your future self to debug. Take the few extra minutes before you merge.
— Rishi Banerjee
September 2025