As an experienced full-stack developer, I rely on Git daily to track changes, collaborate with others, and shape a project‘s history. While Git enables powerful version control workflows, mistakes inevitably happen in any development process.

Thankfully, Git offers the git reset command – a versatile tool for undoing changes by manipulating the commit history. Using reset properly is key to maintaining speed and sanity when developing applications.

In this comprehensive guide from a full-stack developer‘s perspective, you will learn:

  • What happens under the hood when resetting commits
  • How to leverage the mixed, soft, and hard reset modes
  • When and why to reach for each reset type in development
  • Best practices around resetting from remote branches

Grasping reset in depth transforms you from Git novice into a reset ninja able to rewrite history with precision and confidence.

Let‘s master the art of git reset!

Reset vs Related Git Commands

Before starting, it‘s useful to distinguish reset from related Git commands that modify history:

Command Purpose Preserves Work?
git reset Erases commits by moving branch backwards Optional
git revert Counter a commit with an inverse commit Yes
git checkout Switch between branches or restore files from commits Partial

Reset erases commits by literally rewinding the current branch, whereas revert creates inverse commits that cancel unwanted changes. Checkout can restore files from commits, behaving similarly to --hard resets.

Understanding the available history editing verbs enables reaching for the right tool for each job. Typically reach for revert over reset when wanting to nullify public changes without destroying history.

With that clarified – let‘s understand what happens when doing a reset under the hood.

Git Reset Explained

Internally within Git, three key components determine your development state:

  1. The commit history
  2. The staging index
  3. The working directory

The commit history represents all previous versions and work saved. Git references commits through branch pointers such as HEAD and master that mark your current position.

The staging index is a file manifest used for the next commit. Adding files/changes to the index stages them for including in the subsequent snapshot.

Finally, the working directory holds uncommitted local changes that live on your file system. This represents changes not yet staged/committed.

Git architecture diagram

Simplified Git architecture – Atlassian

When executing git reset:

  1. The current branch pointer is moved backwards to the specified commit
  2. The staging index gets updated to match that commit‘s state
  3. The working directory then syncs to either the index or target commit

The three reset modes of mixed, soft, and hard control that final working tree sync behavior – determining if uncommitted changes are preserved, staged, or deleted.

Let‘s now break down each mode and when to leverage them.

Git Reset Mixed

The default mixed reset mode unstages files while keeping working directory changes intact. This effectively backs out any adds/removals since your last commit.

For example:

# Edit existing filed.txt
vim file.txt

# Also stage a new file 
git add new.txt

# Oops, unstage that file add 
git reset --mixed

git status 

This would provide output showing new.txt was unstaged, but changes to file.txt preserved:

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
          modified:   file.txt

Untracked files:  
  (use "git add <file>..." to include in what will be committed)
          new.txt

Conceptually, mixed reset allows removing specific files/changes from the next commit while keeping other edits. This granularly controls what gets committed next without harming uncommitted work.

When To Use Mixed Reset

  • Unstage incorrect file adds/removals while preserving working changes
  • Split large unstable commits into smaller focused ones
  • Temporarily set aside working changes for later
  • Integrate changes from another branch without losing current progress

For any scenario requiring adding/removing files from the next commit while protecting local edits, reach for --mixed.

Git Reset Soft

The soft mode preserves both staged changes and working directory changes. This effectively undoes commits while keeping all progress ready for recommit.

For example, soft resetting multiple commits:

# Make some commits 
git commit -m "Commit 1"
git commit -m "Commit 2"

# Undo last 2 commits preserving changes
git reset --soft HEAD~2

git status

This would show all files from the undone commits as staged and ready to recommit:

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   file.txt
        new file:   new.txt 
        deleted:    removed.txt

By leaving the index and working tree untouched, it‘s like the commits never occurred!

When To Use Soft Reset

  • Quickly undo unstable commits without losing work
  • Edit commit messages or splits without redoing changes
  • Temporarily set aside commits while handling upstream changes
  • Rework commits touching multiple areas avoiding partial reverts

Soft reset lets modifying commit contents without forcing file-level merges or destabilizing branches.

Git Reset Hard

The harshest reset mode – hard resets all files back to the target commit state discarding any working directory and staging index changes. Meaning uncommitted progress not present in that commit gets wiped out entirely.

For example:

# Make some commits
git commit -m "Commit 1" 
git commit -m "Commit 2"

# Revert all changes and deletion 
git reset --hard HEAD~2

Post-reset, the staging index and working tree match files from two commits prior – all uncommitted work gets deleted:

git status

# On branch main  
nothing to commit, working tree clean

Just like that, local commits and edits lost as if never there!

When To Use Hard Reset

While incredibly destructive to uncommitted work, some valid --hard use cases exist:

  • Completely erasing broken commits as if never occurred
  • Forcibly removing unwanted untracked files/dirs (Safer: git clean)
  • Blowing away temporary work/experiments on local branches
  • Resetting local branch in sync with updated remote branch

As a basic rule however, strive to avoid --hard unless absolutely necessary. Losing uncommitted work from days of focused development is heartbreaking. Lean towards mixed and soft modes over nuking changes.

Comparing Reset Behavior

Let‘s summarize the key differences between reset modes:

Reset Type Uncommitted Staged Files Uncommitted Unstaged Files Common Use Case
–mixed Unstaged Preserved Unstage incorrect commits
–soft Preserved Preserved Rework/split commits
–hard Removed Removed Erase broken commits

Mixed resets Stage work. Soft preserves it entirely. Hard nukes all local changes.

Now that you grok the reset modes – let‘s walk through some best practices in leveraging reset safely and effectively.

Resetting From Remote Branches

One area of caution is using reset to modify the history of remote branches tracked locally. Since remote branches like origin/main get updated from outside peers, resetting can lead to the commits getting orphaned or duplicated on later fetches.

As a rule of thumb, only reset local unpublished work on topic branches. Manipulate history before pushing changes externally when possible.

For public branches like main, prefer using git revert to counter changes over resetting published history. Revert appends inverse commits keeping network history intact and avoiding synchronization conflicts.

Structuring Resets In Practice

Through years of resetting commits, I‘ve found some helpful patterns for keeping changes organized. Here are best practices that serve me well:

  • Prefer smaller focused commits decomposed by logic change
  • Leverage --mixed for temporarily shelving work between commits
  • Build up a logical changeset before committing with --soft
  • Tag temporary states before big refactors with reset in mind
  • Limit reset depth with HEAD~N over removing by date or hash
  • Always verify changes post-reset before force pushing

Keep commits organized, leverage tags as future reset points, and verify integrity before overwriting public branches.

Internal Git Reset Strategies

In addition to modifying the three-tree state of your repository, git reset has a couple useful behavioral flags that modify how it moves the branch pointer itself:

1. --merge

The --merge flag resets the index but performs a git merge style branch synchronization to preserve history continuity:

git reset --merge ORIG_HEAD

Merging avoids creating detached HEAD situations allowing easy reversing of resets.

2. --keep

The --keep option preserves branch state without moving HEAD and the current branch allowing resetting of just the staging index and working copy:

git reset --keep HEAD^

This lets quickly unstaging commits while staying put on the branch.

Understanding these specialty flags provides additional options for safely using reset in your workflows.

Conclusion

Git reset is a remarkably versatile tool once internalized – allowing erasing commits while precisely selecting what happens to uncommitted changes. Understanding the subtleties between soft, mixed, and hard reset modes enables surgically undoing unstable commits while avoiding unnecessary work loss.

Some key ideas for walking away with:

  • Know what gets modified in working vs staged state for each reset type
  • Prefer mixed and soft modes over nuking work with --hard
  • Structure commits that isolate logical changes for clean reset points
  • Be extremely careful about resetting published shared branch history

While simple on surface, intelligently wielding git reset accelerates development by enabling erasing mistakes and crafting commit history with intent. Master its usage and unlock your next level Git abilities today!

I‘m happy to clarify or expand on any areas – please let me know if you have any other questions!

Similar Posts