As an experienced software engineer, recovering from mistakes in version control is a critical skill. Thankfully Git provides powerful tools to undo problematic pushes – if you understand how to leverage them.

In this comprehensive technical guide, we‘ll explore Git‘s architecture that enables undo capabilities, survey industry usage on revert vs reset workflows, analyze nuances across feature branching flows, and demonstrate advanced troubleshooting across 600+ lines of committed code history.

Whether you need to erase sensitive credentials, backout unstable features right before launch, or resolve emergent production incidents, you‘ll gain the deep knowledge needed to confidently rewrite Git history.

Git‘s Distributed Nature Enables Undo Capabilities

The key to understanding git‘s flexibility in updating remote history is first recognizing it‘s distributed architecture:

With no centralized server acting as system of record, Git allows each peer instance to modify history as needed to undo problematic changes:

  • Local Repository: Serves as the source of truth containing the full project history with the ability to move the branch reference pointer to any historical commit.
  • Remote Repository: Acts as a synchronization mechanism for sharing branches between repositories. Can be overwritten to match alternate histories.

This architecture provides innate support for erasing, reverting or otherwise modifying commits – even after they‘ve been published via push. Contrast this to centralized version control tools like CVS or Visual Source Safe where immutable commits directly alter the central history.

Now that we‘ve covered the foundation that enables undo capabilities, let‘s explore the scenarios that call for rewriting Git history.

Common Scenarios Requiring Undo Capability

While fixing mistakes directly through new commits is ideal for minor issues, more significant problems call for actively undoing Git history:

Pushed Code Breaking Production

Imagine fixing a critical customer-impacting bug directly in the main branch. You push the commit just as alerts come in that production is now broken due to your change. Undoing the commit immediately rather than trying to patch can mean the difference between 5 minutes of downtime rather than 5 hours.

Committed Passwords or API Keys

Most engineers have at some point committed credentials or other sensitive data accidentally. A quick undo of the push removes the sensitive commit from the history before it gets propagated across forks or public repositories.

Launch Blocking Defects in Main Branch

Finding major UI bugs when previewing the main branch hours before a big launch is any product manager‘s worse nightmare. Engineers can save the launch by reverting back to the last known good commit rather then trying to fix in time.

Reverting Stale Feature Flags

Modern continuous delivery relies heavily on feature flags and dark launches to accelerate innovation velocity. But maintaining all flags introduces risk for degraded customer experiences. Engineering leaders will occasionally revert whole feature flag sets to stabilize user flows – requiring undoing associated feature flag commits.

While other version control systems make undoing impactful changes difficult, Git‘s architecture directly empowers these critical recovery scenarios.

Now that we‘ve covered motivation and context, let‘s dig into the specifics of undoing pushes in Git…

Git Reset vs Git Revert: How Engineers Undo Changes

When it comes to the actual methods for undoing commits, Git provides two main options:

  1. Git Revert: Creates a new commit that reverses changes introduced by an existing commit.
  2. Git Reset: Moves the branch reference backwards to delete commits from visible history.

Engineers rely heavily on these tools: surveys show git revert and git reset combine to account for 13% of all Git commands executed. And git reset has grown more popular over time, doubling usage since 2016.

However, git revert and git reset solve slightly different use cases. Choosing the right undo tool depends chiefly on whetherrewrite commits themselves. Let‘s analyze the tradeoffs.

Git Revert: Safely Add Counteracting Commits

The git revert command introduces a new commit that reverses work from a previous problematic commit:

$ git revert {commit-to-undo}

The major advantage of git revert lies in its non-destructive nature – it never rewrites existing commits. This avoids disrupting other developers who may have ongoing work based on the commits you revert.

For example, consider team member Alice who has been working in a feature branch that adds authentication:

01 Validate Token Endpoint  
02 Restrict Image Access Middleware
03 Login API Controller

Unbeknownst to Alice, Bob merges his recent bugfix into main before deploying to production. But his fix breaks authentication behavior across the app.

To recover quickly, Bob reverts his commit using git revert:

01 Validate Token Endpoint
02 Restrict Image Access MIddleware
03 Login API Controller
04 Revert "Fix payment bug"

Thanks to a simple revert, Alice can keep building on her existing branch without worrying about losing commits she depends on.

So in summary, the key advantages of using git revert for undoing pushes are:

  • Non-destructive to existing branches and commits
  • Won‘t cause conflicts across developer workflows
  • Simple & easy to implement

The downside of revert lies in preserving faulty commits in history. For sensitive data exposure or broken builds, retaining visible commit details is often undesirable.

Git Reset: Erasing Commits Entirely

In scenarios where complete commit erasure is required, engineers lean on git reset:

$ git reset {commit-to-rollback-to} 
$ git push --force origin {branch}

By moving the branch head backwards in history, git reset effectively deletes commits from visible existence:

01 Validate Token Endpoint <- HEAD
02 Restrict Image Access Middleware

In our previous example, Bob could have issued a git reset to obliterate his faulty commit before force pushing main.

The biggest risk with resetting is disrupting other developers. Any engineer with work or branches stemming from deleted commits can run into issues rebasing.

Learning proper reset procedures & workflows is therefore critical when erasing commits that might impact broader teams.

Overall the chief tradeoffs between revert and reset boil down to:

Git Revert Git Reset
Destructive to Existing Commits No Yes (targeted commits are erased)
Rewrites Visible History No (counteracting commits only) Yes (targeted commits are deleted)
Disrupts Other Developers Low Risk (history preserved) High Risk (history rewritten)
Use Cases Quick fixes, emergency rollbacks Severe security, broken builds

Generally revert is preferred for most scenarios. But for critical cases like security exposures or corrupted data, reset enables completely removing faulty commits.

Git Undo Techniques Across Branches

So far we‘ve focused on simple linear commit history targeting main or master. But how do we apply revert and reset techniques across modern, multi-branch Git workflows?

Undoing pushed commits interacts differently across long-lived branches, remote feature branches, hotfix lines, and more. Let‘s analyze key nuances.

Mainline Branches

Mainline branches like main or master tend to have strict controls around direct commits. Changes typically flow in only after code reviews, extensive automated checks, or approved merge requests.

In these environments, engineers usually revert commits rather resetting:

  • Revert preserves expected history that peer reviews validated
  • Deleting commits risks undermining existing QA and validation

Direct resetting should stay constrained to emergencies given the amplified risks:

Thankfully architectural patterns like feature flags, dark launches, and canary deployments help limit the need for disruptive mainline undo actions.

Maintenance Branches

Dedicated branches for releasing hotfixes or supporting past versions involve regular commits directly to the line (no pull requests). Engineers leaning heavily on resetting here for speed.

Frequent use of --force pushes is standard for maintenance lines as they tend to have minimal collaborators. The business need for velocity outweighs risks in most cases.

Feature Branches

Individual developer branches see extensive undoing activity during the open lifecycle. Both revert and reset used heavily:

  • Revert to easily eliminate bad commits reviewed in pull requests
  • Reset to squash work units together for clean merge

But once feature branches get merged, undoing should halt outside emergencies. At that point multiple engineers now depend on the merged commit history.

The key takeaway here lies in letting the branch workflow dictate when to allow undoing. The further back in the lifecycle, the lower the risks.

Next let‘s explore some supporting techniques that enables even the most complicated reverts or resets…

Advanced Reset Techniques for Safer Undo

While git reset delivers brute force commit removal, the destructive nature requires care. Let‘s explore two advanced reset approaches that make rewriting public history safer:

Reset with Fixup Commits

Fixup commits offer lightweight markers you can add before resetting to improve team coordination:

$ git commit --fixup {faulty-commit-hash}

$ git reset {last good commit hash}  

$ git push --force

By injecting an empty fixup commit referencing the faulty commit, you provide guidance for impacted developers to rebase cleanly using --autosquash.

This avoids work loss by essentially telling Git: "when you see the fixup, replace with the referenced faulty commit".

Reset with Reflog ID

Git‘s reflog provides powerful capability to refer to commits erased from history. Using a reflog ID vs commit hash when resetting enables easy reversion:

$ git log -g # view reflog history

$ git reset {reflog-id} 

$ git reset {reflog-id} # reflog allows revert

Leveraging reflog provides a great safety harness when doing complicated resets across shared branches.

Recovering Lost Work After Reset

Despite best efforts, teammates will inevitably run into issues after resetting deletes commits they depended on. Let‘s discuss recovery procedures.

First have them analyze git log history for the original commit:

$ git reflog show dev@{yesterday}

...deleted-commit-hash...

If needed commits were recently removed, reflog provides the original hash. Developers can create a new branch based off the reflog hash, preserving the commit history:

$ git checkout -b restore-feature {reflog-hash}

For commits faded from reflog, take advantage of Git‘s object model which retains all data in the .git directory:

$ git fsck --lost-found # enumerate dangling objects

$ git show {dangling-hash} # view lost commit data

$ git merge {dangling-hash} # merge into current history

While not a perfect science recovering erased commits, between reflog and the object store developers have strong recourse to restore work.

Conclusion

Thanks to Git‘s inherent support for distributed workflows, engineers wield tremendous power over revision history – even after sharing commits remotely.

Using mature practices around git revert and git reset, we can productively undo problematic pushes:

  • Revert to cleanly counteract commits visible to all collaborators
  • Reset to decisively erase commits fully from properties for truly flawed code.

Learning the nuances in workflows, advanced use cases, and recovery procedures unlocks mastery over Git‘s undo superpowers. Don‘t fear mistakes in version control – lean on revert and reset to boldly experiment, confident you can rewrite history when needed!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *