Lewati ke konten
Rama's logo Qisthi Ramadhani
Go back

My Git Commit Catastrophes: A Definitive Guide to Squash, Revert, and Undo Your Blunders

Let me paint you a picture. It’s 4:57 PM on a Friday. The sun is streaming through the window, mocking my pasty, screen-tanned skin. The weekend, a mythical land of non-Laravel-related activities, is so close I can almost taste the nasi goreng. All that stands between me and freedom is one. final. push.

It was a “tiny” bug fix. A simple tweak to a Laravel API endpoint that the React front-end team was breathing down my neck for. In my haste, fueled by caffeine and visions of kicking back with a good book, I performed the sacred, thoughtless ritual: git add .. Followed by the triumphant, yet ultimately foolish, git commit -m "final fix before weekend".

I hit enter. The command executes. A moment of silence. And then, the cold, creeping dread that washes over you when you realize you’ve done something truly, epically stupid.

My eyes scanned the pre-commit hook output. A flash of a filename I didn’t recognize. My heart sank faster than a poorly optimized database query. I had, in my glorious haste, not only committed the fix but also:

I hadn’t pushed. Thank goodness, I hadn’t pushed. But my local commit history was now a Jackson Pollock painting of chaos and regret. My beautiful, clean Git log was tainted. It was a digital crime scene, and I was both the victim and the perpetrator.

We’ve all been there, right? That heart-stopping moment when you commit the wrong thing. Maybe it’s a secret key, a massive file, or just a series of commits with messages like “WIP,” “asdfghjkl,” and “PLEASE WORK.”

If this sounds familiar, grab a coffee (or something stronger), and settle in. You’re in the right place. Over the years, I’ve turned these moments of panic into a finely honed set of skills. Git isn’t just a version control system; it’s a time machine, a story-telling tool, and a safety net, all rolled into one. And today, I’m going to share my hard-won secrets. We’re going to transform you from a Git user into a Git whisperer. We’ll explore how to undo a git commit, how to remove a file from a git commit (even one buried deep in your history), and the elegant art of the git commit squash.

This isn’t just a technical manual. This is a survival guide.

The “Oh Crap!” Moment: How to Undo a Git Commit Before You Push

So, you’ve made a bad commit. It’s sitting there on your local machine, a ticking time bomb of embarrassment. The git push command is beckoning, whispering sweet, destructive nothings in your ear.

DO. NOT. PUSH.

As long as you haven’t shared your mistake with the world (i.e., your remote repository), you’re in what I call the “Safe Zone.” In the Safe Zone, you can rewrite history with reckless abandon. Nobody has to know about the cat video. It can be our little secret.

Your primary weapon in this scenario is git reset. But git reset is like a powerful magic wand with a few different settings. Waving it around without understanding it can turn your codebase into a toad. Let’s break down the spells.

The Soft Reset: Your Gentle Time Machine (git reset --soft)

Think of git reset --soft as the most forgiving form of time travel. It’s like saying, “You know what? I didn’t like how I packed that suitcase. Let me just open it up, but I’ll leave everything piled right here on the bed.”

What it does: It moves the HEAD pointer back to a previous commit (e.g., HEAD~1 to go back one commit), but it does not touch your staging area (the index) or your working directory (the files on your disk).

When to use it:

Let’s walk through a scenario.

I just made this commit:

git commit -m "Feat: Add nw user profle page"

Oh, the humanity. “nw”? “profle”? I was clearly typing too fast. My tech lead would have a field day with that one during code review. I need to fix this git commit message change immediately.

Here’s the magic spell:

# Move back one commit, but keep all my changes staged
git reset --soft HEAD~1

After running this, if you type git status, you’ll see something beautiful:

On branch feature/new-profile
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   app/Http/Controllers/ProfileController.php
        new file:   resources/views/profile/show.blade.php
        ...and so on

Look at that! All my files are still staged, exactly as they were. The bad commit is gone from my history, but my work is safe. Now, I can simply re-commit with the correct message.

# Now I can make a git commit with message that's actually readable
git commit -m "feat: Add new user profile page"

Voilà! The typo is gone. My dignity is restored. The timeline is clean. This is the simplest way to undo git commit actions locally. It’s my go-to for quick message fixes.

The Mixed Reset: Stripping It Down (git reset --mixed)

Now, let’s say my mistake was a bit more complex. I didn’t just flub the message; I accidentally added a file that shouldn’t be there. Maybe it was a test.log file I was using for debugging.

This is where git reset --mixed comes in. This is the default mode, so if you just type git reset HEAD~1, this is what you’ll get.

What it does: It moves the HEAD pointer back and it unstages all your files. Your working directory (the files on disk) remains untouched.

The Suitcase Analogy: You open the suitcase, and you put everything back in the closet. The clothes are all still there, but you have to pick and choose what you want to pack again.

When to use it:

Let’s see it in action.

Imagine this commit log:

* 2a3b4c5 (HEAD -> feature/user-auth) feat: Implement login, registration, and password reset

That’s a MONSTER commit. It does three distinct things. If a bug pops up later, trying to figure out which part of that behemoth caused it will be a nightmare. I should have made three separate commits. I need to delete local git commit and start over.

Time for a mixed reset:

# Go back one commit, and unstage everything
git reset --mixed HEAD~1

# Or the shorthand version
git reset HEAD~1

Now, git status will show me this:

On branch feature/user-auth
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        app/Http/Controllers/Auth/LoginController.php
        app/Http/Controllers/Auth/RegisterController.php
        app/Http/Controllers/Auth/PasswordResetController.php
        ...etc.

All my hard work is still there, safe and sound in my working directory. But now, it’s unstaged. I have a clean slate. I can be the meticulous developer I always pretend to be.

# First, commit just the login logic
git add app/Http/Controllers/Auth/LoginController.php resources/views/auth/login.blade.php
git commit -m "feat: Implement user login functionality"

# Next, the registration
git add app/Http/Controllers/Auth/RegisterController.php resources/views/auth/register.blade.php
git commit -m "feat: Implement user registration"

# And finally, the password reset
git add app/Http/Controllers/Auth/PasswordResetController.php ...
git commit -m "feat: Implement password reset flow"

My history is now a clean, logical story:

* 7d8e9f0 (HEAD -> feature/user-auth) feat: Implement password reset flow
* 6c7d8e9 feat: Implement user registration
* 5b6c7d8 feat: Implement user login functionality

Ah, so much better. This is a history I can be proud of. It’s like turning a rambling, incoherent monologue into a series of crisp, well-articulated points.

The Hard Reset: The “Nuke It From Orbit” Option (git reset --hard)

Okay. Deep breaths. We need to talk about git reset --hard.

This command is the emergency eject button. It’s the “break glass in case of fire.” It’s the Thor’s Hammer of Git commands: incredibly powerful, deeply satisfying to use, and capable of causing immense destruction if you’re not worthy.

What it does: It moves the HEAD pointer back, unstages your files, and overwrites your working directory to match the commit you’re resetting to. Any changes made since that commit? GONE. Poof. Vanished into the digital ether.

The Suitcase Analogy: You don’t just unpack the suitcase. You throw the suitcase, its contents, and the closet it was in into a volcano. 🔥

When to use it:

A cautionary tale: One time, as a junior dev, I had been working on a complex feature for a day. I got hopelessly tangled. Nothing worked. In a fit of frustration, I decided to use git reset --hard to go back to my commit from the previous morning. I typed git reset --hard HEAD~5… but I miscounted. I actually went back HEAD~6, wiping out a crucial, working commit from the day before. I had no backup. I spent the next four hours re-writing code I had already written. The shame was palpable.

So, here’s how you use it, carefully:

# First, double-check your log. Be absolutely sure where you want to go back to.
git log --oneline

# Okay, I'm sure. I want to obliterate the last two commits and all my current changes.
# Let's say the last good commit is `a1b2c3d`
git reset --hard a1b2c3d

When you run this, there’s no “are you sure?” prompt. Git assumes you’re a responsible adult who knows what they’re doing (a bold assumption, I know). Your project directory is instantly reverted. It’s a clean slate. It’s terrifying and liberating all at once.

The reflog Safety Net: What if you mess up, like I did? What if you reset --hard too far? Is all hope lost?

NO! Git has a secret guardian angel called the reflog (reference log). The reflog keeps a record of everywhere HEAD has been. It’s your personal Git diary.

git reflog

You’ll see a list of actions: commits, resets, amends, everything. You can find the hash of the commit you accidentally nuked, and you can git reset --hard <that-lost-commit-hash> to bring it back from the dead. The reflog has saved my bacon more times than I can count. It’s the ultimate undo git commit tool.

The Stowaway: How to Remove a File from a Git Commit

Let’s return to my Friday afternoon horror story. I’ve got a commit that contains my .env file and a giant cat video. A soft or mixed reset could work, but what if I’ve already made a few more good commits on top of the bad one? Resetting would mean re-doing all that work.

I don’t want to undo the whole commit; I just want to perform a surgical strike. I need to remove a file from git commit without disturbing anything else.

The Quick Fix: Amending the Most Recent Commit (git commit --amend)

If the bad commit is the very last one you made, you’re in luck. This is an easy fix. The git commit --amend command is your best friend. It lets you tweak the previous commit, whether that’s changing the message or adding/removing files.

The Envelope Analogy: You’ve just sealed a letter, but you realize you forgot to include a photo. git commit --amend is like carefully steaming the envelope open, slipping the photo inside, and sealing it back up. No one will ever know.

Here’s how I’d solve my .env file problem if it were the last commit:

Step 1: Unstage the offending file. I need to tell Git, “Hey, I don’t want this .env file to be part of the commit anymore.” But I don’t want to delete the file from my computer, just from the repository’s tracking. The key is the --cached flag.

# This removes the file from the staging area, but leaves it on my disk
git rm --cached .env
git rm --cached funny_cat_video.mp4

If I actually wanted to delete the file from my project entirely, I’d leave off the --cached.

Step 2: Amend the commit. Now that the bad files are unstaged, I can tell Git to update the previous commit with the new state of the staging area.

git commit --amend --no-edit

The --amend flag tells Git to smoosh these changes into the previous commit. The --no-edit flag is a handy shortcut that says, “Don’t even bother opening the editor for the commit message; the old one is fine.”

And just like that, the commit is healed. The .env file and the cat video are gone from the commit history as if they were never there. They still exist on my local filesystem (because I used --cached), so I can add .env to my .gitignore file where it belonged all along. This is the cleanest way to handle git commit remove for the most recent commit.

The Deep Surgery: Removing a File from Older Commits

This is where things get serious. What if you didn’t notice the committed secret key until 10 commits later? The .env file is now buried deep within your branch’s history. Amending won’t work. Resetting would wipe out all the good work you did since then.

You need to perform historical surgery. You need to tell Git, “Go back in time, find every instance of this file, and erase it from existence.”

For years, the standard tool for this was git filter-branch. If you look up old Stack Overflow answers, you’ll see it everywhere. But filter-branch is notoriously slow, clumsy, and can be dangerous. The official Git documentation itself now recommends against it.

So, what’s the modern, safer, and infinitely faster alternative?

Meet git-filter-repo. You can find it and install it from here: https://github.com/newren/git-filter-repo.

git-filter-repo is a third-party script, but it’s the new gold standard for rewriting history. It’s like a high-precision laser scalpel compared to filter-branch’s rusty hacksaw.

A Real-World Scenario: A new developer on my team once committed the company’s Stripe API keys. Worse, he pushed them to a public fork on his GitHub account. This was a five-alarm fire. 🚨

Step one, of course, was to immediately log into Stripe and rotate the keys. The old keys were now compromised. But that wasn’t enough. We also had to remove the file from the git commit history, so no one could stumble upon the old, dead keys in the future.

Here’s how git-filter-repo saved the day.

Step 1: Install git-filter-repo. Follow the installation instructions on their GitHub page. It’s usually a simple matter of making sure you have Python 3 and then putting the script in your PATH.

Step 2: Make a fresh, bare clone of the repository. Rewriting history is a major operation. You NEVER want to do it on your main working copy. The official recommendation is to start fresh.

# Clone the repo but as a "bare" repo, which is just the .git data
git clone --bare https://github.com/my-company/my-project.git

cd my-project.git

Step 3: Run the filter. The command is surprisingly simple. We want to remove a file by its path.

# Tell filter-repo to find and obliterate 'config/secrets.yml' from all of history
git-filter-repo --path config/secrets.yml --invert-paths

git-filter-repo will whiz through your entire history, commit by commit, and build a new history that is identical to the old one, just without that pesky secrets.yml file. It’s incredibly fast.

Step 4: Push the new history back up. This is the DANGEROUS part. You are about to rewrite the history on the remote server. This will cause problems for any teammates who have the old history. You MUST coordinate with your team before you do this.

# Push the new, cleaned history to all branches on the remote
git push origin --force --all
git push origin --force --tags

The --force is necessary because you’re replacing the remote history with your new, altered version. It’s a destructive act, but in the case of leaked secrets, it’s a necessary one. After this, you should tell all your teammates to delete their local clones and re-clone from the server to ensure they have the new, clean history.

This procedure is a big deal, but it’s the only way to truly remove files from git commit history permanently.

The Art of Storytelling: A Masterclass in git commit squash

Alright, let’s shift gears from panic-driven fixes to proactive craftsmanship. A good developer doesn’t just write good code; they write a good story with their code. Your Git history is that story.

When you’re working on a feature, your local commit history can get… messy. Mine often looks like this:

* f1a2b3c - fix(ci): try to fix test again
* e4d5f6g - chore: more debug statements
* c7h8i9j - wip
* b1k2l3m - feat: Add user avatar upload, kind of works
* a4n5o6p - refactor: Tweak profile layout

This is not a story. This is the frantic, sleep-deprived diary of a madman. Submitting a Pull Request with this history is like handing in a term paper with all your rough drafts, coffee stains, and tear-soaked napkins attached. It’s unprofessional. It makes the code review process a nightmare for your colleagues. They have to wade through your dead ends and typos to understand the final feature.

This is where the git commit squash comes in. Squashing is the process of taking a series of small, messy commits and melting them down into a single, beautiful, cohesive commit.

It’s about turning “wip” and “fix typo” into “feat: Implement User Avatar Upload with Image Cropping and S3 Storage.”

Why Should I Bother Tidying Up?

“But Rama,” you might say, “the code works! Who cares about the commit history?”

I care. Your future self cares. Your team cares. Here’s why:

Your Weapon of Choice: Interactive Rebase (git rebase -i)

The tool we use for squashing is our old friend, git rebase, but in its powerful interactive mode (-i).

Interactive rebase is like opening up a text editor that contains a list of your recent commits. In this editor, you can play God. You can reorder commits, delete them, edit them, and—most importantly for us—squash them together.

Let’s squash that messy history from before.

My branch has 5 messy commits that I want to turn into one.

Step 1: Start the interactive rebase. I need to tell Git how far back I want to go. I can do this by counting (HEAD~5) or by providing the hash of the commit before the ones I want to edit.

# Start an interactive rebase for the last 5 commits
git rebase -i HEAD~5

This will open your default text editor (like Vim or Nano) with something like this:

pick a4n5o6p refactor: Tweak profile layout
pick b1k2l3m feat: Add user avatar upload, kind of works
pick c7h8i9j wip
pick e4d5f6g chore: more debug statements
pick f1a2b3c fix(ci): try to fix test again

# Rebase 1d2e3f4..f1a2b3c onto 1d2e3f4 (5 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
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label this HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#

Don’t be intimidated! Those comments at the bottom are your instruction manual. The important part is the list of commits at the top.

Step 2: Tell Git what to do. I want to keep the first commit as my base and then squash all the others into it. So, I’ll change the word pick on the subsequent lines to squash (or s for short).

I’ll edit the file to look like this:

pick a4n5o6p refactor: Tweak profile layout
squash b1k2l3m feat: Add user avatar upload, kind of works
squash c7h8i9j wip
squash e4d5f6g chore: more debug statements
squash f1a2b3c fix(ci): try to fix test again

A pro-tip: if the commit message is just junk like “wip”, you can use fixup (or f) instead of squash. A fixup does the same thing but automatically discards the squashed commit’s message, saving you a step.

Let’s use fixup for the really useless messages:

pick a4n5o6p refactor: Tweak profile layout
squash b1k2l3m feat: Add user avatar upload, kind of works
fixup c7h8i9j wip
fixup e4d5f6g chore: more debug statements
fixup f1a2b3c fix(ci): try to fix test again

Step 3: Save and close the file. When you save and exit the editor, Git will start the process. Since we used squash, it will then open a new editor window. This window contains all the commit messages from the commits you’re squashing together.

# This is a combination of 5 commits.
# The first commit's message is:
refactor: Tweak profile layout

# This is the 2nd commit's message:
feat: Add user avatar upload, kind of works

# This is the 3rd commit's message:
wip

# ...and so on

(Note: If we had only used fixup, this step would be skipped for those commits.)

Step 4: Craft the perfect commit message. This is your chance to shine. Delete all the old, messy messages and write one, beautiful, descriptive git commit with comment that perfectly summarizes the entire body of work.

I’ll delete everything and write:

feat(profile): Implement user avatar uploads

- Allows users to select a JPG or PNG image from their device.
- Implements a front-end cropping tool using React-Image-Crop.
- On the Laravel back-end, the image is validated, resized, and stored on a configured S3 bucket.
- Updates the user's `avatar_url` in the database.
- Closes #123

Now that is a commit message. It explains the “what” and the “why.” It’s a gift to my future self and my teammates.

Step 5: Save and close the file. Git will churn for a moment and then… success! If you check your git log now, you’ll see that your five messy commits have been replaced by one glorious, pristine commit.

* 9z8y7x6 (HEAD -> feature/user-avatar) feat(profile): Implement user avatar uploads

Now you’re ready to submit your Pull Request. You’ll look like the most organized, professional developer on the team. This process of tidying up git commits squash is a true superpower.

A Quick Warning: Just like git reset, git rebase rewrites history. Therefore, you should NEVER, EVER rebase a branch that other people are using, like main or develop. Only rebase your own local feature branches before they are merged.

The Point of No Return? How to revert git commit After Pushing

So far, we’ve been operating in the “Safe Zone” of our local machine. But what happens when the mistake gets out? What if you push that commit with the bug, or the wrong feature, to the main branch?

The whole team pulls your change. The continuous integration server deploys it to staging. The damage is done.

Your first instinct might be to use git reset --hard on the main branch and then git push --force.

STOP. RIGHT. THERE.

Force pushing to a shared branch is the cardinal sin of Git. It’s like secretly swapping out a chapter in a library book that everyone is reading. Suddenly, everyone’s page numbers are wrong, their understanding of the story is broken, and they will hunt you down with pitchforks. It creates a divergent history and will cause massive headaches and merge conflicts for every single person on your team.

So, how do we fix a mistake that’s already live and public? We need a way to rollback git commit actions safely.

The answer is git revert.

The Safe Undo: Your Public Retraction (git revert)

git revert is the civilized way to undo changes on a shared branch.

It does not rewrite or delete history. Instead, git revert creates a brand new commit that does the exact opposite of the bad commit. If the bad commit added three lines of code, the revert commit will remove those three lines.

The Accounting Analogy: git reset is like shredding a financial record. git revert is like making a new entry in the ledger that properly balances the books. It’s transparent, it’s safe, and it preserves the historical record.

Let’s walk through it.

Oh no! I just realized that commit b4c5d6e, which I pushed an hour ago, introduced a major bug that’s crashing the production server. I need to undo it, fast.

Step 1: Find the hash of the bad commit. A quick git log will give me what I need.

git log
...
* b4c5d6e (origin/main, main) feat: Optimize user query (and introduce horrible bug)
* a3b4c5d refactor: Clean up old code
...

The culprit is b4c5d6e.

Step 2: Revert the commit. The command is as simple as it sounds.

git revert b4c5d6e

This will do two things:

  1. It will calculate the inverse of the changes in b4c5d6e.

  2. It will open your text editor to create a commit message for this new revert commit. By default, it will be something like:

    Revert "feat: Optimize user query (and introduce horrible bug)"
    
    This reverts commit b4c5d6e1f2g3h4j5k6l7m8n9o0p1q2r3s4t5u6v.

    This is usually a good message, as it clearly explains what’s happening. You can just save and close the file.

Step 3: Push the revert commit. Now, if you check your git status, you’ll see you have one new commit. You can push this to the remote repository just like any other commit. No force push needed!

# git push git commit (as some might search for it)
git push origin main

The history now looks like this:

* f9e8d7c (HEAD -> main, origin/main) Revert "feat: Optimize user query (and introduce horrible bug)"
* b4c5d6e feat: Optimize user query (and introduce horrible bug)
* a3b4c5d refactor: Clean up old code

The bad commit is still there in the history, but its effects have been perfectly cancelled out by the new revert commit. The bug is fixed on production, and you didn’t destroy the timeline for your teammates. You can now safely work on a proper fix for the optimization feature in a new branch.

The git commit revert command is your best friend for dealing with post-push problems. It’s the responsible, team-friendly way to say, “My bad.”

Conclusion: From Git User to Git Guru

Phew. We’ve been through a lot. From the heart-pounding panic of committing a .env file to the zen-like satisfaction of a perfectly squashed feature branch.

Git can feel like a complex, arcane beast. It has more commands and flags than you can shake a stick at. But at its core, it’s a tool designed to empower you, not intimidate you. The key is understanding the difference between your private, local history and the sacred, shared history on the remote.

Let’s recap our new superpowers:

Mastering these commands will change your relationship with Git. You’ll move from a place of fear—the fear of making a mistake—to a place of confidence. You’ll know that no matter how badly you mess up, you have the tools and the knowledge to fix it. You can experiment, you can be messy (on your own branch!), and you can clean it all up before anyone else sees it.

So go forth and commit! And when you inevitably mess up (because we all do), come back to this guide, take a deep breath, and remember: you’ve got this.

Now it’s your turn. What’s the worst thing you’ve ever accidentally committed? What’s your favorite Git “save the day” story? Spill the beans in the comments below! 👇


Share this post on:
LLM-friendly version:
Open in ChatGPT Open in Claude

Related Posts


Previous Post
Unlocking Performance: A Pragmatic Guide to Laravel Database Indexing
Next Post
My Epic Quest to Master PSQL: From 'Show Tables' to Dropping Everything (Accidentally, Of Course)