Git Reset vs. Git Checkout vs. Git Revert: A Complete Technical Guide
Understanding the difference between git reset, git checkout, and git revert is essential for any developer working with version control. In short: git reset rewrites history by moving the HEAD pointer; git checkout navigates between branches, commits, or files without altering history; and git revert undoes a commit by creating a new inverse commit, leaving history intact. Choosing the wrong command — especially on a shared branch — can corrupt your team's commit history or cause irreversible data loss.
This guide goes beyond the surface-level syntax to explain the internal Git mechanics, the exact state of your working tree and index after each operation, and the real-world scenarios where each command is the correct (or catastrophically wrong) choice.
How Git Manages State: The Three Trees
Before comparing commands, you need a firm mental model of how Git tracks state. Git operates across three distinct layers:
- Working directory — the files you see and edit on disk.
- Staging area (index) — a snapshot of what will go into the next commit.
- Commit history (HEAD) — the chain of immutable commit objects stored in
.git/.
Every undo command in Git targets one or more of these layers. The confusion between reset, checkout, and revert almost always stems from not knowing which layers a given command touches.
git reset: Rewriting Local History
git reset moves the current branch's HEAD pointer to a specified commit. Depending on the mode flag you pass, it can also update the index and the working directory. It is a history-rewriting operation and should be treated as destructive when used with --hard.
Reset Modes Explained
git reset --soft <commit> # Move HEAD only; index and working tree unchanged
git reset --mixed <commit> # Move HEAD + reset index; working tree unchanged (default)
git reset --hard <commit> # Move HEAD + reset index + reset working tree| Mode | HEAD Moves | Index Reset | Working Tree Reset | Changes Preserved |
|---|---|---|---|---|
--soft | Yes | No | No | Staged and unstaged |
--mixed | Yes | Yes | No | Unstaged only |
--hard | Yes | Yes | Yes | None — permanently lost |
Practical Use Cases
Squashing commits before a push. If you have three messy work-in-progress commits that have not yet been pushed, git reset --soft HEAD~3 collapses them back into the index so you can recommit as a single clean commit.
Unstaging a file accidentally added. Running git reset HEAD <file> (or git reset without a commit reference) removes a file from the index without touching the working directory — identical in effect to git restore --staged <file> in Git 2.23+.
Recovering from a botched local merge. git reset --hard ORIG_HEAD restores the state that existed immediately before a merge, because Git writes the pre-merge HEAD to ORIG_HEAD automatically.
Critical Pitfall: Pushing After a Reset
If you reset a branch that has already been pushed to a remote and then force-push, every collaborator whose local branch tracks that remote will have a diverged history. This is one of the most common causes of lost commits in team environments. Never run git reset followed by git push --force on a shared branch without explicit team coordination.
# Dangerous on shared branches — use only on private/local branches
git reset --hard HEAD~2
git push --force origin feature/my-branchgit checkout: Navigation Without History Modification
git checkout is a multi-purpose command that switches the working tree to match a branch, a specific commit, or a single file. It does not modify commit history. In Git 2.23 and later, its responsibilities were split into git switch (for branches) and git restore (for files), but git checkout remains fully functional and is still dominant in production environments.
Syntax Reference
git checkout <branch_name> # Switch to an existing branch
git checkout -b <new_branch> # Create and switch to a new branch
git checkout <commit_hash> # Enter detached HEAD state at a specific commit
git checkout -- <file_name> # Discard working directory changes to a file
git checkout <commit_hash> -- <file> # Restore a single file from a specific commitDetached HEAD State: What It Actually Means
When you run git checkout <commit_hash>, Git moves HEAD to point directly at a commit object rather than at a branch ref. This is called detached HEAD state. Any commits you make in this state are not reachable from any branch — they are orphaned and will eventually be garbage-collected by Git unless you create a branch to capture them.
git checkout 4f7a2c1 # HEAD now points directly to commit 4f7a2c1
git checkout -b hotfix/patch # Rescue those commits by creating a branchA common real-world scenario: a developer checks out an old commit to test a regression, accidentally makes a fix, commits it, and then switches back to main — losing the fix entirely because it was never attached to a branch.
Restoring a Single File from History
One of the most underused forms of git checkout is targeted file restoration:
git checkout HEAD~3 -- src/config/database.phpThis pulls database.php as it existed three commits ago directly into your index and working directory, without touching any other file or moving HEAD. It is the surgical equivalent of a full branch switch.
git revert: Safe Undo for Shared History
git revert creates a new commit that applies the inverse diff of a specified commit. The original commit remains in history untouched. This is the only safe undo mechanism for commits that have already been pushed to a shared remote branch, because it does not rewrite history — it extends it.
Syntax Reference
git revert HEAD # Revert the most recent commit
git revert <commit_hash> # Revert a specific commit by hash
git revert HEAD~3..HEAD # Revert a range of commits (creates multiple revert commits)
git revert -n <commit_hash> # Stage the revert without committing (--no-commit)The –no-commit Flag: Batching Reverts
When reverting multiple commits, creating one revert commit per original commit can pollute the log. The -n (or --no-commit) flag stages all the reversals without committing, letting you bundle them into a single revert commit:
git revert -n HEAD~4..HEAD
git commit -m "Revert: roll back broken authentication refactor"Merge Commit Reverts Require Extra Care
Reverting a merge commit requires specifying the mainline parent with the -m flag, because Git needs to know which side of the merge to treat as the "correct" history:
git revert -m 1 <merge_commit_hash>Here, -m 1 designates the first parent (typically the branch you merged into) as the mainline. Omitting this flag on a merge commit will cause Git to throw an error. This is a common stumbling block when reverting a bad release merge in CI/CD pipelines.
Side-by-Side Comparison: Reset vs. Checkout vs. Revert
| Criterion | `git reset` | `git checkout` | `git revert` |
|---|---|---|---|
| Modifies commit history | Yes | No | No (adds to it) |
| Affects working directory | With --hard or --mixed | Yes | Yes (via new commit) |
| Affects staging area (index) | Yes (except --soft) | Only with file form | No |
| Safe on shared/remote branches | No | Yes (read-only) | Yes |
| Creates a new commit | No | No | Yes |
| Reversible | Partially (via ORIG_HEAD) | Yes | Yes |
| Handles merge commits | No | Yes (navigation) | Yes (with -m) |
| Git 2.23+ modern equivalent | Same | git switch / git restore | Same |
When to Use Each Command: Decision Matrix
Use git reset when:
- You are working on a local, unpushed branch and want to clean up, squash, or discard commits.
- You need to unstage files before a commit.
- You want to undo a local merge that has not been shared.
- You are the sole developer on a feature branch and need to rewrite its history before opening a pull request.
Use git checkout when:
- You need to switch between branches during active development.
- You want to inspect the state of the repository at a historical commit without changing anything.
- You need to restore a single file to its state at a specific commit.
- You are creating a new branch from a specific point in history.
Use git revert when:
- A commit has already been pushed to a remote or shared branch.
- You are working in a team and need to maintain a transparent, auditable history.
- You need to undo a specific commit that is not the most recent one (non-linear revert).
- Your project requires compliance or audit trails where history deletion is prohibited.
Advanced Scenario: Recovering from git reset –hard
If you accidentally ran git reset --hard and lost commits, they are not immediately gone. Git's reflog records every position HEAD has pointed to, even after a hard reset:
git reflog
# Output example:
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3
# 7e8f9a0 HEAD@{1}: commit: Add payment gateway integration
# ...
git reset --hard HEAD@{1} # Restore to the commit before the accidental resetThe reflog entries expire after 90 days by default (gc.reflogExpire), so this recovery window is not infinite. On a production server or a VPS Hosting environment running a self-hosted Git service like Gitea or GitLab, you should ensure your .git directory is included in regular backup routines precisely because of this expiry.
Hosting Your Git Infrastructure
Running a self-hosted Git server — whether GitLab CE, Gitea, or Gogs — demands reliable storage I/O and consistent uptime. A single corrupted pack file or an interrupted push during a git gc cycle can damage repository integrity. For teams managing multiple repositories, a Dedicated Server provides isolated resources, full root access for fine-tuning Git's core.packedGitWindowSize and pack.threads settings, and the NVMe throughput needed for large monorepos.
For smaller teams or individual developers who need a clean Linux environment to run Git hooks, CI scripts, and deployment pipelines, a VPS with cPanel offers a managed control plane alongside full SSH access — removing the overhead of manual server administration while retaining the flexibility to configure Git server-side hooks and access controls.
If your workflow involves automated deployments triggered by git push, securing your server with a valid SSL Certificate is non-negotiable — both for encrypting webhook payloads and for authenticating HTTPS-based Git remotes without disabling certificate verification.
Key Technical Takeaways
git reset --hardis the only command among the three that can cause permanent, unrecoverable data loss if the reflog has expired.git revertis the only command that is safe to use after agit pushwithout requiring a force-push.- Detached
HEADstate fromgit checkout <hash>does not delete commits — but any new commits made in that state will be orphaned unless you immediately rungit checkout -b <new_branch>. - The
-nflag ongit revertis critical for keeping a clean log when rolling back multiple commits at once. - Git 2.23+ splits
git checkoutintogit switchandgit restorefor clarity — understanding the original command makes both modern alternatives immediately intuitive. - Always verify your current branch with
git statusandgit log --oneline -5before running any undo operation. - On shared infrastructure, enforce branch protection rules (GitHub, GitLab, Gitea all support this) to block
git push --forceonmainandreleasebranches at the server level.
Frequently Asked Questions
What is the difference between git reset --soft and git reset --mixed?
Both move HEAD to the specified commit, but --soft leaves your changes staged in the index, while --mixed (the default) also clears the index, leaving changes only in the working directory. Neither touches files on disk.
Can I recover commits after git reset --hard?
Yes, within the reflog expiry window (90 days by default). Run git reflog to find the commit hash of the lost state, then run git reset --hard <hash> or git checkout -b recovery <hash> to restore it.
Why does git revert on a merge commit require the -m flag?
A merge commit has two parents. Git cannot determine which parent's diff to invert without you specifying the mainline. -m 1 tells Git to treat the first parent as the trunk, reverting the changes introduced by the merged branch.
Is git checkout -- <file> the same as git restore <file>?
Functionally yes — both discard unstaged working directory changes to a file by restoring it from the index. git restore was introduced in Git 2.23 as a less ambiguous replacement, but git checkout -- <file> produces identical results on all Git versions.
When should I absolutely never use git reset on a branch?
Never use git reset (especially with --hard or --mixed) on any branch that other developers have already cloned or pulled from. Doing so diverges their local history from the remote, requiring every collaborator to perform a forced reset or re-clone — and risks silently discarding commits that exist only on their machines.
