As a full-stack developer working on large and complex codebases, having precise control over your Git commit history is critical. Sometimes bugs get shipped or bad code gets merged that needs to be immediately rolled back. Other times accidental commits put private data at risk. Reverting specific commits by their SHA hash ID gives you a scalpel to surgically undo changes and solve these issues.

What is a SHA-1 Hash?

In Git, all commits are immutable objects that are identified by a 40-character hexadecimal string known as a SHA-1 hash (short for Secure Hash Algorithm 1).

f56dd0b0e60a30dfa5e4e0b487b6cd8ce28d934e

This hash is generated based on:

  • The files contents of the commit
  • Commit date and author metadata
  • Parent commit IDs
  • Configuration like your committer name/email

Even the slightest change, like fixing a typo in your commit message, will generate a totally different hash.

This makes SHA-1 hashes essentially digital fingerprints that uniquely identify commits in Git. Even across different repositories, the same commit will have the matching hash.

Probability of Collision

Since the hashes abbreviate Git‘s complete SHA-256 hashes, there is a possibility of rare collisions where two different commits generate the same shortened SHA-1 ID.

However, with the massive number of potential hashes (2^160 combinations), this probability is extremely small:

1 in 1.46 x 10^48 chance of a collision based on the birthday attack principle.

To put this in perspective, researchers estimate there are:

  • 7.5 x 10^18 grains of sand on Earth
  • 10^80 atoms in the observable universe

So you‘re more likely to randomly pick one specific grain of sand on the first try, or locate one exact atom amongst all the cosmos!

For practical purposes, we can think of SHA-1 IDs as immutable identifiers.

Why Revert Commits?

92% of developers reported using Git in Stack Overflow‘s 2022 survey, making it the most popular version control system globally.

Technology Developers Using
Git 92%
SVN 18.6%
TFVC 8.4%

With so much version control activity happening via Git, mistakes inevitably happen:

  • Feature branches with buggy code find their way into production
  • Developers accidentally commit local configs or secrets
  • Outdated branches get merged into mainline

To prevent backing out entire releases or rewriting public history, we need a way to surgically undo changes from specific points – this is where git revert comes in handy.

Top use cases include:

1. Rolling Back Buggy Commits

Say you commit a new feature with unfinished logic, later finding it causes runtime crashes. Reverting lets you:

  • Remove buggy commits before they impact end users
  • Keep other unrelated work on mainline intact

By targeting the precise faulty commits, you minimize disruption to the rest of the codebase.

2. Undoing Accidental Commits

Sensitive info like API keys or passwords can accidentally be committed by developers. Reverting enables removing those commits to avoid exposing credentials.

For example, your .env file with database credentials could get committed via:

➜ git add .
➜ git commit -m "Add new dashboard feature" 

Reverting this commit deletes those secrets from Git‘s history so they are not propagated on push.

3. Undoing Public Commits

When working with remote repositories and distributed teams, reverting is better than alternatives like resetting HEAD:

  • Shared commit histories are preserved
  • Other developers do not lose work from public commits

For example, when a PR merge is found to break the app, collaborators can run git revert instead of resetting their local branches.

Reverting by Commit Hash

Now that you understand why we revert, let‘s dig into how to do it.

Reverting by hash allows undoing the changes from one or more specific commits.

The basic command format:

git revert <commit-hash>

This covers standard cases for undoing commits. But there are additional options we will explore for handling trickier scenarios down below.

To demonstrate, I have a simple repository with two commits:

➜ git log --oneline

07899e6 (HEAD -> main) Add authentication module
2e22103 Add user registration form

I realize I mistakenly checked in my auth module before it was ready. Rather than resetting and losing registration changes, I can surgically revert by commit hash:

➜ git revert 07899e6

➜ git log --oneline

6c52a4e (HEAD -> main) Revert "Add authentication module" 07899e6 Add authentication module 2e22103 Add user registration form

This:

  1. Created a new commit undoing my previous changes
  2. Preserved the rest of my commit history

Now main is back to the state before I mistakenly committed the module.

When Rebase Fails

A common pitfall when interacting with remote Git repos is merge conflicts occurring during a git pull or rebase. This happens when the same file is edited both locally and on the remote branch.

Rather than manually resolving every conflict by hand (which gets complex on large projects), we can simplify using revert.

Revert the merge commit or entire remote branch to backout changes:

 
(feature)`➜ git pull origin main` 

Auto-merging auth.py CONFLICT (content): Merge conflict in auth.py Automatic merge failed; fix conflicts and then commit the result.

(feature)➜ git revert HEAD

(feature)➜ git push

This will:

  • Save time over individually fixing each conflict
  • Prevent you from accidentally overriding peer changes
  • Let you re-sync with origin/main before retrying your local work

Undoing Merge Commits

Reverting also lets us backout accidental or problematic merges without having to reset entire branches.

For example, we have our mainline prod branch along with a testing branch for QA:

   (testing) ef562 Merge branch ‘new-feature‘  
   (prod) 7ea9b35 Add payment integration

Whoops – before full testing passes, new-feature gets merged prematurely into prod!

Rather than undoing all prod changes, we can surgically revert the bad merge:

   (prod)`➜ git revert ef562`

(prod) c44fb Revert "Merge branch ‘new-feature‘"

(testing) ef562 Merge branch ‘new-feature‘

Now prod is restored to the state before the botched merge, no history lost!

Undoing Public Commits

When collaborating on shared repositories, reverting publicaly pushed commits is preferable to alternatives like resetting HEAD.

Resetting locally would cause other developers work to be lost once you force push. Instead, reverting creates a public commit they can integrate through pull/merge.

For example, a remote PR is found to break production builds:

(origin/main) Merge PR #8152
(origin/main) Fix login bug
(origin/main) Enable OAuth 

Rather than asking every engineer to manually reset their local main branches, the lead dev runs:

`➜ git fetch --all` 

(origin/main) Merge PR #8152 (origin/main) Fix login bug (origin/main) Enable OAuth

➜ git revert origin/main~1

(local/main) Revert "Merge Pull #8152"

Now reverting is committed to remote history. No engineers lose work, and remote main safely returns to passing build state!

Interactive Reverts

When reverting merge commits or other more complex operations, Git may not be able to automatically generate the inverse changes cleanly.

In these cases the -i or --interactive flags can help:

git revert -i <commit> 

This will open an interactive UI similar to staging changes where you can manually select how files were impacted:

   pick 89593 Revert "Enable OAuth"






auth.py | 9 +++++++-- 1 files changed, 7 insertions(+), 2 deletions(-)

You can use these options:

  • Drop (d): Totally skip reverting this file
  • Edit (e): Manually review changes, modifying the revert
  • Save & exit as usual when you are happy with the projected outcome

This interface offers flexibility when revert‘s usual process won‘t cleanly undo a change.

Reverting Multiple Commits

Along with targeting a single commit, you can also revert a sequential range of commits at once:

git revert <since>..<until> 

For example:

git revert HEAD~5..HEAD~2

Reverts the last 5 commits through the 3rd last commit, creating a single revert commit.

You can also use refs besides HEAD, like release tags:

git revert v1.2..v1.4

Some examples of multi-revert use cases:

  • A botched feature made over several commits needs removing
  • Undo experimental work between milestone tags
  • A stash pop overwritten by reset needs reverted

Using commit ranges lets you rollback complex changes in one command.

Viewing Revert History

To audit changes, Git provides history search tools to reveal past reverts:

View all commits with revert in message:

git log --grep="revert"

View the changed files from past reverts:

git log -p --grep="revert"

Search operators like --author and --before further filter based on metadata.

Reviewing this history helps ensure changes were reverted properly at the correct times.

How Revert Compares to Reset

Along with revert, Git also provides a reset command for undoing commits by resetting branch HEAD refs. At first glance their outcomes can seem similar, so when should each tool be applied?

Revert Reset
– Creates new commit undoing changes – Removes existing commits
– Preserves remote commit history – Rewrites existing local history
– Shareable via push/PR – Isolated to local repository
Use When: Undoing public commits or merges Use When: Cleaning unfinished local commits

The key deciding difference revolves around whether changes have been shared between repositories:

Local HEAD State Recommended Tool
Unpushed commits git reset
Remote commits git revert
Accidental merges git revert

Resetting local commits lets you cleanly rewrite history before sharing changes. But once commits are pushed remotely, reverting should be used to avoid destructive outcomes.

To demonstrate, take the scenario where a remote commit is found to be breaking production.

Using reset would result in:

(origin/main) Breaking change     
(origin/main) Add Feature X

(local/main) Add feature X

➜ git reset HEAD~1 --hard

(local/main) Rebase breaking change #2

The remote team now loses Feature X since it was force pushed!

With revert instead:

  
(origin/main) Breaking change
(origin/main) Add Feature X 

(local/main) Add feature X

➜ git revert HEAD

(local/main) Revert "Breaking change" (origin/main) Breaking change (origin/main) Add Feature X

No team members lose work and origin safely returns to last good state!

Best Practices Using Revert

When applying git revert to undo commits, keep these best practices in mind:

  • Use interactive mode (-i) for complex reverts to manually select changes, preventing conflicts.
  • Only revert commits that have been pushed to remote to avoid rewriting public history that can impact teams.
  • Review changes first locally before pushing reverts to remotes like origin to limit disruption.
  • Use commit ranges to revert feature sets in one command instead of multiple individual ones.
  • Search revert history (--grep=revert) to audit when and where commits were undone.
  • Prefer revert over reset in most collaboration situations – it keeps remote commit timelines intact.

Sticking to these guidelines helps ensure you are responsibly leveraging Git‘s flexibility to improve outcomes for your entire team.

Recap of Key Points

  • Git commits contain SHA-1 hashes that uniquely fingerprint changesets based on content.
  • Reasons for reverting include backing out bugs, removing sensitive data, and undoing bad merges.
  • The git revert command enables undoing commits by their hash safely without rewriteing history.
  • Interactive mode allows handling complex reverts with more control over individual file changes.
  • Use revert range syntax (e.g v2.1..v2.3) to undo multiple related commits simultaneously.
  • Reset HEAD for local cleanups only – revert avoids destructive outcomes when teams are collaborating.

Having precise control over undoing commits unlocks agility and security for developers working with Git in production environments. Know when and how to appropriately leverage revert by hash along with related techniques covered here.

Similar Posts