Clarify the differences between git reset --[options]
<commit-hash or HEAD~n>
Using practical examples with simple code changes can definitely help clarify the differences between
git reset --soft
<commit-hash or HEAD~n>
git reset --mixed
<commit-hash or HEAD~n>
git reset --hard
<commit-hash or HEAD~n>
Let’s simulate a scenario where we modify a file called example.txt
in different ways across several commits, and then see how each type of reset affects the repository.
Initial Setup
Suppose example.txt
originally contains:
Line 1: Initial conten
Commit 1 (A):
- Modify
example.txt
to:Line 1: Initial content
Line 2: Added by commit A - Commit the change:
git add example.txt
git commit -m "Commit A: Add second line"
Commit 2 (B):
- Modify
example.txt
to
Line 1: Initial content
Line 2: Added by commit A
Line 3: Added by commit B
- Commit the change:
git add example.txt
git commit -m "Commit B: Add third line"
Commit 3 (C):
- Modify
example.txt
to
Line 1: Initial content
Line 2: Added by commit A
Line 3: Added by commit B
Line 4: Added by commit C
- Commit the change:
git add example.txt
git commit -m "Commit B: Add third line"
Now, let's see how different resets work:
git reset --soft HEAD~2
- Goal: Undo the last two commits (B and C), but keep their changes staged.
- Command:
git reset --soft HEAD~2
- Result:
- The repository HEAD is back to commit A.
- Changes from commits B and C remain staged.
- Running
git status
will show lines from commits B and C staged for recommitting. - Commit History After Reset: Only commit A remains in the
main
branch's history. Commits B and C are no longer part of the commit history, but their changes are ready to be committed anew. - A [main]
Changes from B and C are staged.
git reset --mixed HEAD~2
(default behavior ofgit reset
)- Goal: Undo the last two commits (B and C), keep their changes in the working directory but not staged.
- Command:
git reset --mixed HEAD~2
- Result:
- HEAD is back to commit A.
- Changes from commits B and C are in the working directory but not staged.
- Running
git status
will show lines from commits B and C as modified but not staged. - Commit History After Reset: The commit history is similar to the
--soft
reset: only commit A is visible inmain
. The work from commits B and C is not staged but remains in the working directory, allowing you to stage and commit them again as desired. - A [main]
Changes from B and C are modified but not staged.
git reset --hard HEAD~2
- Goal: Completely undo the last two commits (B and C) and remove all associated changes.
- Command:
git reset --hard HEAD~2
- Result:
- HEAD is back to commit A.
- All changes from commits B and C are discarded.
example.txt
is back to its state at commit A, with no trace of changes from B and C in the working directory.- A [main]
- Soft Reset (
--soft
): Moves HEAD but leaves your working directory and staging area untouched as they were at the last commit before reset. Great for modifying commit history while keeping changes ready to recommit. - Mixed Reset (
--mixed
): Moves HEAD back and unstages changes but leaves them in your working directory. Useful for redoing commits if you want to modify how changes are staged. - Hard Reset (
--hard
): Moves HEAD back and discards all changes completely, both staged and unstaged. Use with caution as it can lead to data loss.
Steps to Safely Revert Changes and branch history backup
To preserve both the changes and the commit history while still reverting to a previous state in your original branch, the best practice is to create a new branch before performing any type of reset. This allows you to retain all commit history and changes in one branch while experimenting or reverting changes in another.
Suppose you are on the main
branch and have made several commits that you wish to review or revert, but you want to ensure that none of your work is lost irreversibly:
Creating a Backup:
git branch backup-branch # Creates a backup git checkout backup-branch # Switches to backup
Resetting the Main Branch:
git checkout main # Return to main git reset --hard HEAD~3 # Reverts the last three commits
Result:
main
branch is now reverted to an earlier state, with the last three commits removed.backup-branch
contains all the commits, including those removed frommain
.
Before creating backup-branch
and resetting:
A -- B -- C -- D -- E [main]
After creating backup-branch
and performing git reset --hard HEAD~3
on main
:
A -- B [main]
\
C -- D -- E [backup-branch]
This approach gives you flexibility. You can move forward on the main
branch from the older state, potentially taking a different direction, while still having the option to refer back to or reuse work done in those "removed" commits through the backup-branch
.
Note: When using git reset
, you can only directly influence the state of the branch from which you're performing the reset, and it directly affects the HEAD
of that branch. Essentially, git reset
modifies the position of HEAD
and potentially changes the staging area and working directory depending on the options used (--soft
, --mixed
, or --hard
).
Scenario: Local Reset with an Aligned Remote Repo
Initial State: Assume your local and remote repositories are synchronized.
Local and Remote: A -- B -- C -- D [main]
Create a Backup Branch Locally:
- You create a backup branch from the current state of
main
:git branch backup-branch git checkout backup-branch
- At this point, both your local
main
andbackup-branch
are identical, but this new branch is only local.
- You create a backup branch from the current state of
Reset the Main Branch Locally:
- You reset the
main
branch to an earlier commit, for example,B
:git checkout main git reset --hard B
- Locally, your
main
branch now looks like:A -- B [main]
- You reset the
Updating the Remote Repository
- Pushing Changes: If you wish to update the remote repository to match your local reset state, you would need to use force push (if the history has diverged, which it typically does after a reset):
git push origin main --force
- Backup Branch: If you want to push the
backup-branch
to the remote, simply use:
This will create a new branch on the remote repository with all commits up togit push origin backup-branch
D
.
Scenario: Resetting and Pushing in the main
Branch
Initial State:
- Assume your local and remote
main
branches are synchronized with the following commits:A -- B -- C -- D [main]
Local Reset:
You decide to reset themain
branch back to commitB
:git reset --hard B
- Locally, your branch now looks like this:
A -- B [main]
Making a New Commit:
After the reset, you make a new commit E:git add . git commit -m "Commit E"
Locally, your branch now looks like this:A -- B -- E [main]
What Happens to the Remote Repository?
If you use
git push --force
, Git will update the remotemain
branch to exactly match your localmain
branch, which now looks like:A -- B -- E [main]
- When you attempt to push your local
main
to the remote without using force: git push origin main - This push will likely be rejected because it would remove commits
C
andD
from the remote, which Git interprets as a non-fast-forward update. The remote still has:A -- B -- C -- D [main]
When you try to push your local changes to the remote:
- Git compares the histories of the two branches.
- The push would require removing commits
C
andD
from the remote, which Git identifies as a non-fast-forward update because the commitE
does not directly follow commitD
. Instead, it appears to "replace" commitsC
andD
Why Doesn't Git Just Append E
?
Appending E
to make the history A -- B -- C -- D -- E
would be the straightforward solution if you hadn't reset and E
was simply a continuation of D
. However, because your local main
branch's history effectively rewrites the sequence from B
directly to E
, Git cannot append E
without potentially discarding C
and D
, unless it merges or rebases, which could change the contents and intention of E
.
Why Git Rejects Non-Fast-Forward by Default
- Data Loss Prevention: Allowing such a push would mean that commits
C
andD
are lost in the remote'smain
branch. Git rejects this by default to prevent data loss that could affect other collaborators who might be basing their work on those commits. - Repository Integrity: Maintaining a linear history on shared branches like
main
ensures that all collaborators have a consistent view of the project history. Disruptions in this history could lead to conflicts and confusion.
Creating a new branch to backup or preserve commits before making significant changes or resets to your main development branch is indeed a best practice in many development workflows. This strategy ensures you maintain the flexibility to revert or modify your approach without losing any historical data or previous work. Here’s why this is often the best approach:
Git Staging Area and Working Directory
Let's consider a practical scenario to better understand these relationships:
Suppose you modify
file.txt
in your working directory:// Initial content Line 1: Hello World
You then edit
file.txt
to add a second line:// Modified in working directory Line 1: Hello World Line 2: New line added
You stage this change:
git add file.txt
After staging, the changes (the addition of Line 2) are in both the staging area and the working directory.
If you now modify the same file again without staging the new change:
// Further modified in working directory Line 1: Hello World Line 2: New line added Line 3: Another new line added
The staging area still only has up to Line 2 staged. Line 3 is in the working directory but not staged.