Fixing Git History with git replace: How to Save Your Repo Without Changing Commit Hashes

Git tutorial - IT technology blog
Git tutorial - IT technology blog

The Problem: When Git History Becomes a Liability

Have you ever accidentally committed a 2GB log file or a .env file containing all your database passwords to a repo? Even worse, the mistake happened dozens of commits ago and has already been merged into the main branch. Once discovered, handling 10 Git disasters like this becomes a major headache.

Usually, we think of git rebase -i or using the BFG Repo-Cleaner. However, these commands change the entire Commit Hash from the point of modification onwards. If your team has 15 people working, a force push after modifying the history will create a nightmare for the other 14. Conflicts will pop up everywhere when they pull the new code.

I once saw a project come to a standstill for an entire afternoon just to handle errors after a repo “cleanup” using rebase. Never touch pushed history unless you truly understand the consequences. But what if you absolutely must fix it? git replace is the “silent” solution you need.

Why Are Commit Hashes So Sensitive?

In Git, a commit’s Hash isn’t just based on its content. It’s a combination of the timestamp, author information, and most importantly, the parent commit’s Hash. You can dissect Git from the inside to see how these identifiers are generated. Even if you just change a comma in a commit from last month, its Hash changes. Consequently, all descendant commits change their Hashes in a domino effect.

git replace solves this with a clever mechanism. It doesn’t actually modify the old commit. Instead, it tells Git: “Hey, whenever you see Object A, display the content of Object B instead.” This configuration keeps the Git graph looking intact while the content you see is updated.

Comparing Common Solutions

  • Git Amend: Quick and easy, but only usable for the most recent commit.
  • Git Rebase -i: Suitable for personal (local) branches. Absolutely avoid using it on shared branches as it causes mass Hash changes.
  • Git Filter-repo: Powerful for large-scale cleanup (like removing a 500MB file from history). However, it still changes Hashes and requires everyone to re-clone the repo.

The Solution: Using the git replace Command

This command allows you to replace any object (commit, tree, blob) without distorting the Git graph structure. Old commit hashes remain the same, ensuring consistency for the whole team.

Step 1: Identify the Faulty Commit

Suppose commit a1b2c3d contains a JSON configuration file with a syntax error that crashes a legacy system. You need to fix that file’s content right within that old commit.

If you need to validate the JSON format, I often use the JSON Formatter & Validator on ToolCraft. This tool runs 100% in the browser, so there’s no risk of leaking sensitive info to a server. Just paste it in, fix the syntax, and get ready to put it back into Git.

Step 2: Create a Replacement Commit

Create a temporary commit containing the corrected content:

# After fixing the file, commit normally
git add config.json
git commit -m "Temporary content fix"

Suppose this new commit has the hash e5f6g7h.

Step 3: Execute the replace Command

This is where the magic happens. We will link the faulty commit with the correct one:

git replace a1b2c3d e5f6g7h

Now, when you run git show a1b2c3d, Git will display the content of commit e5f6g7h. However, the identifier displayed in the system remains the old Hash a1b2c3d.

Step 4: Share Changes with the Team

By default, replace commands only take effect on your machine. To let colleagues see the fixed content, you must push these special references to the server:

git push origin 'refs/replace/*'

Others will also need to fetch them to sync when they pull:

git fetch origin 'refs/replace/*:refs/replace/*'

Use Case: Removing Large Files from History

Another common case is removing a large binary file that was accidentally committed, which is a key step in rescuing bloated and sluggish Git repositories. You can use the command git replace --edit <commit-hash>. This opens an editor allowing you to directly delete the line containing the large file in that commit. Git will automatically create a clean replacement object.

To ensure the file isn’t corrupted after replacement, I often use the Hash Generator. Comparing the SHA-256 hash of the local file and the original file helps ensure data integrity.

Important Practical Notes

  1. Don’t overdo it: Think of git replace as a coat of paint. If used too much, the Git history will become confusing for newcomers to the project.
  2. Check CI/CD: Some automated build systems do not fetch refs/replace/ by default. Double-check your script configuration if the build fails due to content issues.
  3. Choose the right time: If the error just happened and hasn’t been pushed, use commit --amend. Only use replace when the error is buried deep in the shared history.

Git is a flexible tool, and a great developer knows how to choose the right solution for each situation. Hopefully, this little tip helps you handle those “impossible” cases and automatically clean up commit history without fearing a force push.

If you frequently process text or format code, check out ToolCraft. This toolkit is completely free and very safe for tech professionals.

Share: