Ops Did I just Commit That?

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

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 log
    # View Your Commit History
    git log --oneline
    
    Other modes flags:
    • 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 ).

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 the oopsie/file from every commit, but if you don’t find it in some commits, just chill and move on (that’s the --ignore-unmatch part). 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. The cat here 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-filter is 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-filter is 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-filter operates only on the index, while --tree-filter modifies the working tree.
  • Use Cases: --index-filter is 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-filter is 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.”
  • --all and --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 list
    
  • Apply 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:

  1. 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/"' -- --all
    
  2. Changing 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
    ' -- --all
    
  3. Removing 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
    ' HEAD
    

    git show -s --format=%ci $GIT_COMMIT retrieves the commit date of the current commit being rewritten. The if statement 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 with git 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