
Ever had that “oh no” moment when you realize you’ve just committed a file with sensitive data (like passwords or secret keys) to your Git repo? Yeah, been there, done that. So how do you walk yourself out of this pickle, whether you’ve already pushed your commits or not. .
Sections ahead
- git reset HEAD - Undoing a Commit That’s Not Pushed Yet
- git filter-branch - Yep ive already sent it to space, how to fix…?
- git cherry-pick - Rewriting History to Start from a new Beginning
- git stash - Something came up, Stash and Save my changes for later
- git filter-branch - Into The details of filter-branch
- git rebase main - My feature-branch needs an update from main - with a rebase
- git gloal ignore main - Update your global git so you dont commit foolishly again!
Undoing a Commit That’s Not Pushed Yet
Caught a mistake before pushing? Phew! Use:
# This command is your "undo" button. It un-commits your last commit but keeps all your changes handy for a redo.
git reset HEAD~1
This is the main command used for undoing changes. It has several modes, but without any specific flag, it defaults to –mixed.
- Can the Number Be Something Else?
Yes, you can change the number to match how many commits back you want to go:
git reset HEAD~2 would take you back two commits before HEAD. git reset HEAD~3 would take you back three commits, and so on. - How to Know the How many commits I want to go back? use the
git logOther modes flags:# View Your Commit History git log --oneline- Keep changes and recommit: Use
--soft. - Rework changes before committing again: Use
--mixed(Default). - Discard changes entirely: Use
--hard( Remember that with this you’re making irreversible changes, this will remove/edit the files on disk ).
- Keep changes and recommit: Use
Scenario 1: The Oopsie’s Already Online
Nuke That File From Your Commits…
We’ll use git filter-branch to remove the file from all commits, (filter-branch is a powerfull tool and can do a lot of stuff)
# Yes this is a large one-liner command
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch path/to/your/oopsie/file" \
--prune-empty --tag-name-filter cat -- --all
This command is like a time machine. It rewrites history as if your sensitive file never existed. Its job is to go back through your commit history and remove any trace of that file you accidentally committed.
Here’s what each part of the command does:
git filter-branch: This is the time machine. It lets you rewrite your Git history.--force: This is like saying, “Yes, I’m sure. Just do it!” It overrides any safety checks.--index-filter: This part gets the cleanup ready. It tells Git you’re going to make changes to the index (where Git tracks what’s in your current commit)."git rm --cached --ignore-unmatch path/to/your/oopsie/file": Here’s the actual cleanup. This command says, “Hey Git, please remove theoopsie/filefrom every commit, but if you don’t find it in some commits, just chill and move on (that’s the--ignore-unmatchpart). Oh, and don’t touch the file in my current working directory (--cached).”--prune-empty: After the cleanup, some commits might end up empty (because maybe that file was the only change in those commits). This part tells Git to toss out those now-empty commits.--tag-name-filter cat: Tags are like bookmarks in your Git history. This part ensures that your tags also get updated in this whole history rewrite. Thecathere about a command that says, “Keep the tag names as they are.”-- --all: This is like saying, “Apply this cleanup to all branches and tags in the repository. Leave no stone unturned!”
You can then updated your .gitignore and add that file to it and run
git add .gitignore
git commit -m "Update .gitignore and remove oopsie/file"
git push origin --force
Nuke That File (shorter) but different.
git filter-branch --tree-filter 'rm -f path/to/your/oopsie/file' -- --all
The two git filter-branch commands are similar in their goal. They both aim to remove a file from your Git history but they differ in how they achieve this and their impact on the repository.
Key Differences:
- Performance:
--index-filteris faster and more efficient for operations that don’t require working tree modifications, It directly manipulates the index of each commit without checking out the commit into the working directory .--tree-filteris more flexible but slower, especially on large repositories. It checks out each commit into the working directory, performs the specified operation (rm -f path/to/your/oopsie/file), and then recomputes the commit. This involves more I/O operations. - Operation Scope:
--index-filteroperates only on the index, while--tree-filtermodifies the working tree. - Use Cases:
--index-filteris best for simple tasks like removing a file from tracking, ideal for repositories with a large number of commits or when the file is not present in every commit. It’s efficient for simply removing a file from tracking without modifying the working tree..--tree-filteris suitable for more complex transformations that require actual file content changes and inspecting or modifying the working tree of each commit. It’s not limited to just removing a file; you can run any shell command to modify the contents of the working tree.
Now, push these changes to your remote repo. Heads up, this is irreversible:
git push origin --force --all
git push origin --force --tags
--force: This tells Git, “I know what I’m doing. Make it happen.”--alland--tags: These ensure all your branches and tags get the memo and update accordingly.
Scenario 2: Rewriting History to Start Fresh
Want to make your latest commit look like the first? Here’s how:
Step 1: Create a New Branch
Make a new branch from your current state:
git checkout -b new-beginnings
Step 2: Cherry-Pick and Push
Find the hash of your initial and latest commits. Reset to the initial commit and cherry-pick the latest one:
git reset --hard <initial-commit-hash>
git cherry-pick <latest-commit-hash>
Then, rename and push your branches:
git branch -m main old-main
git branch -m new-beginnings main
git push -f origin main
Renaming branches here is like witness protection for your commits. You’re giving them a new identity before pushing them to the remote repository.
Stash Like a Pro: Saving Changes for Later
Why and When to Stash
Imagine you’re in the middle of coding a feature, and an urgent bug fix comes up. You’re not ready to commit, but you need a clean working area. Enter git stash:
# Stashing takes your modified files and saves them away neatly, leaving you with a clean working directory.
git stash
Naming Your Stash (It’s Like Labeling Your Lunch)
Need to keep track of multiple stashes? Give them names!
git stash save "Feature XYZ Work In Progress"
This names your stash, making it easier to identify when you’re ready to come back to it.
Popping the Stash
Fixed the bug and ready to return to your feature? Use:
git stash pop
This command brings back your stashed changes and applies them to your current working directory.
Managing Your Stashes
Got a stash collection? Here’s how to handle them:
List all stashes:
git stash listApply a specific named stash:
git stash apply stash@{index}Remove a specific stash:
git stash drop stash@{index}Clear all stashes:
git stash clear
Into The details of filter-branch
git filter-branch is a powerful tool in Git, capable of rewriting large parts of your repository history. It’s commonly used for tasks like editing old commit messages, changing author details, or deleting files from history, as we’ve seen. However, its capabilities extend much further. Here are some of the other actions you can perform with git filter-branch:
Changing Commit Messages: You can modify commit messages in the entire history or a range of commits. For example, to change a specific word in all commit messages:
git filter-branch --msg-filter 'sed "s/oldword/newword/"' -- --allChanging Author/Committer Information: Useful for correcting commits attributed to the wrong author/committer. To change the author for all commits: (yes the –env-filter is plain bash code)
git filter-branch --env-filter ' OLD_EMAIL="[email protected]" CORRECT_NAME="Your Correct Name" CORRECT_EMAIL="[email protected]" if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ] then export GIT_COMMITTER_NAME="$CORRECT_NAME" export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL" fi if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ] then export GIT_AUTHOR_NAME="$CORRECT_NAME" export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL" fi ' -- --allRemoving Files from Specific Commits Only: While the earlier example removes a file from all commits, you can also target specific commits. For instance, to remove a file only from commits within a certain date range:
# From remove file from a specific date git filter-branch --tree-filter ' if [ $(git show -s --format=%ci $GIT_COMMIT) > "yyyy-mm-dd" ] then rm -f path/to/file fi ' HEADgit show -s --format=%ci $GIT_COMMITretrieves the commit date of the current commit being rewritten. Theifstatement checks if this date is after a specified date (yyyy-mm-dd). If the condition is true, it removes the specified file.
The Rebase
Git’s most powerful (and often misunderstood) tools: rebase. Ever felt like your Git history looks like a plate of spaghetti? Well, rebase is kind of like a magic wand that can straighten those noodles out.
What’s Rebase, Anyway?
Imagine you’re working on your project, and your commits are like snapshots of your progress. Now, let’s say you’ve been working on a feature branch while others have updated the main branch. You want to bring those updates into your branch, but without the messy “merge commit” that usually comes with a git merge.
Enter git rebase. What it does is kind of like picking up your work (commits) and reapplying them on top of the latest version of the main branch. It’s like saying, “Hey, I want my work to look like it started from the latest stuff on the main branch, not from where I actually started.”
Rebase in Action
Let’s put this into action. You’ve got a branch called feature-branch, and you want to include the latest updates from main.
# First, switch to your feature branch
git checkout feature-branch
# Now, let's rebase onto main
git rebase main
This will start the rebase process. Git will take each of your commits from feature-branch, temporarily “lift” them, apply the new commits from main, and then reapply your commits on top.
Rebasing can do more cool stuff:
Interactive Rebase (
git rebase -i): This is like having a time machine for your commits. You can squash commits (combine them), fix up commit messages, or even drop commits entirely. It’s great for cleaning up your work before sharing it with the world.Solving Conflicts: Sometimes, Git can’t reapply a commit cleanly because the same lines were changed in
main. No worries! Git will pause and let you sort out the conflicts. Once you fix them, just continue the rebase withgit rebase --continue.
Why Use Rebase?
- Clean History: It makes your project history linear and easier to understand.
- Avoiding Extra Merge Commits: Keeps your history from getting cluttered with those “Merge branch ‘main’” commits.
- Rewriting History: Perfect for cleaning up your work before a pull request.
No more foolish Commits
So in order to not push .env and node_modules or any other foolish commits you can add those into your global .gitignore which you can have in your home directory
git config --global core.excludesfile ~/.gitignore
Buy Me a Coffee