Chapter 10: Undoing Changes in Git
Chapter Objectives
By the end of this chapter, you will be able to:
- Unstage files that were accidentally added to the staging area using
git restore --staged
orgit reset HEAD
. - Discard uncommitted changes in your working directory using
git restore
orgit checkout --
. - Modify the most recent commit using
git commit --amend
to fix its message or add/remove changes. - Safely undo a previous commit by creating a new “revert” commit using
git revert
. - Understand and use
git reset
with its--soft
,--mixed
, and--hard
options to move the current branch pointer and optionally alter the staging area and working directory. - Recognize the critical difference between
git revert
(safe for shared history) andgit reset
(can rewrite shared history, use with caution). - Appreciate the implications of rewriting history, especially for commits already pushed to a remote repository.
Introduction
Mistakes happen. It’s a natural part of any creative or development process. You might accidentally stage the wrong file, make a commit with a typo in the message, realize a recent commit introduced a bug, or simply want to undo some local changes that aren’t working out. Git, being a robust version control system, provides several powerful tools to help you manage and undo changes at various stages of your workflow.
In this chapter, we’ll explore how to correct mistakes, from unstaging files to discarding local modifications, amending the last commit, and even “undoing” previous commits. We’ll delve into two key commands for dealing with committed history: git revert
, which safely undoes changes by creating new commits, and git reset
, which can move your branch pointer back in time, effectively rewriting history. Understanding when and how to use these commands—and crucially, understanding their impact on shared history—is essential for maintaining a clean, reliable, and collaborative project.
Theory
Levels of “Undoing”
When we talk about “undoing” in Git, it’s important to consider what you want to undo and where that change currently lives:
- Changes in the Working Directory (Not Staged): Modifications you’ve made to tracked files but haven’t yet told Git to stage for commit.
- Changes in the Staging Area (Staged, Not Committed): Changes you’ve added using
git add
but haven’t yet committed. - The Last Commit: The most recent commit you made, which might need a quick fix (message or content).
- Older Commits in Your Local History: Commits made some time ago that you now realize were problematic.
- Commits Already Shared with Others (Pushed to a Remote): This is the most sensitive scenario, as rewriting shared history can cause significant problems for collaborators.
Git provides different tools for each of these situations.
Unstaging Files: git restore --staged
and git reset HEAD
If you’ve used git add
to move changes from your working directory to the staging area, but then decide you don’t want those changes included in the next commit (or want to unstage a specific file), you can “unstage” them.
git restore --staged <file>
: This is the modern, recommended command (introduced in Git 2.23). It removes the specified file from the staging area. The changes to the file itself remain in your working directory.git reset HEAD <file>
: This is an older command that achieves the same result for unstaging.HEAD
here refers to the last commit. This command tells Git to reset the staging area for<file>
to match what’s inHEAD
, effectively unstaging it. The changes in your working directory are preserved.
Discarding Working Directory Changes: git restore
and git checkout --
If you’ve made changes to a file in your working directory that you haven’t staged or committed, and you want to discard those changes completely and revert the file back to its last committed state (or its state in the staging area if it’s staged), you can use:
git restore <file>
: This is the modern command. If the file is staged,git restore <file>
will revert it to its staged state. If it’s not staged, it will revert it to its state in the last commit (HEAD
).git checkout -- <file>
: This is the older command. The--
is important to separate the file name from branch names (asgit checkout
is also used for switching branches). This command discards changes in the working directory for<file>
, reverting it to the version inHEAD
(or the staging area if the file is staged there).
Warning: Both
git restore <file>
(without--staged
) andgit checkout -- <file>
discard uncommitted changes in your working directory. These changes are not stored by Git anywhere, so if you discard them this way, they are generally gone for good (unless your OS or editor has a separate recovery mechanism). Use with caution.
Amending the Last Commit: git commit --amend
Sometimes you make a commit and then immediately realize you made a mistake:
- A typo in the commit message.
- You forgot to stage a file that should have been part of that commit.
- You staged a file that shouldn’t have been part of that commit.
The git commit --amend
command lets you fix the most recent commit. It doesn’t create a new commit; instead, it replaces the previous commit with a new, “amended” commit.
When you run git commit --amend
:
- If you have changes currently staged, Git will include them in the amended commit.
- If you don’t have anything staged, Git will use the content of the previous commit.
- Git will then open your configured text editor with the previous commit message, allowing you to edit it.
Warning: Because git commit --amend
replaces the last commit (creating a new commit object with a different SHA-1 hash), it rewrites history. This is generally fine if you haven’t pushed the original commit to a remote repository yet. However, if you have already pushed the commit you are amending, you should avoid amending it. Amending a pushed commit and then trying to push again will require a force push, which can cause serious problems for collaborators who have based their work on the original commit. For pushed commits, git revert
is usually a better option.
Reverting Commits: git revert
What if you need to undo a commit that’s older than the very last one, or one that has already been pushed to a remote repository? git revert <commit-hash>
is the command for this.
git revert
does not delete or alter existing history. Instead, it figures out how to undo the changes introduced by the specified commit and creates a new commit that applies these “inverse” changes. This new commit is added to the end of your branch history.
%%{ init: { "theme": "base", "themeVariables": { "primaryColor": "#EDE9FE", "primaryTextColor": "#5B21B6", "primaryBorderColor": "#5B21B6", "lineColor": "#5B21B6", "textColor": "#1F2937", "fontSize": "13px" } } }%% gitGraph BT: commit id: "C1" tag: "Initial good commit" commit id: "C2" tag: "Problematic commit (Bad C2)" type: HIGHLIGHT commit id: "C3" tag: "Later commit" commit id: "C2p" tag: "Revert 'Problematic commit'" type: REVERSE %% Diagram Explanation: %% 1. History: C1 <- C2 (bad commit) <- C3 %% 2. 'git revert C2' is run. %% 3. A new commit C2' is created. %% - C2' contains changes that are the inverse of the changes in C2. %% - The original commit C2 remains in the history, untouched. %% - The branch 'main' (and HEAD) moves forward to C2'. %% 4. History becomes: C1 <- C2 <- C3 <- C2' %% This is a safe way to undo changes in shared history.
Because git revert
adds new history rather than rewriting existing history, it is safe to use on commits that have been shared (pushed) with others. Your collaborators can simply pull the new revert commit.
You can revert any commit by its hash. git revert HEAD
will revert the changes made by the most recent commit.
Resetting Commits: git reset
The git reset <commit-hash>
command is a powerful and potentially dangerous tool that moves the current branch pointer (and HEAD
) to a specified earlier commit. It can also optionally modify the staging area and the working directory. git reset
is primarily used to undo commits in your local history that you haven’t shared yet.
git reset
has three main modes:
git reset --soft <commit-hash>
:- Moves the
HEAD
and current branch pointer to<commit-hash>
. - The staging area and working directory are NOT touched.
- All the changes from the commits that came after
<commit-hash>
(up to the originalHEAD
) will now appear as if they are staged changes. This is useful if you want to squash several local commits into one, or re-commit them differently.
- Moves the
git reset --mixed <commit-hash>
(This is the default mode if no option is specified):- Moves the
HEAD
and current branch pointer to<commit-hash>
. - The staging area IS reset to match the state of
<commit-hash>
. - The working directory is NOT touched.
- All the changes from the commits that came after
<commit-hash>
will now appear as unstaged modifications in your working directory. This is useful if you want to re-evaluate what to include in new commits.
- Moves the
git reset --hard <commit-hash>
:- Moves the
HEAD
and current branch pointer to<commit-hash>
. - The staging area IS reset to match the state of
<commit-hash>
. - The working directory IS ALSO reset to match the state of
<commit-hash>
. - Warning: This is a destructive operation. Any uncommitted changes in your staging area and working directory, as well as any commits made after
<commit-hash>
on the current branch, will be permanently lost from your working directory. Usegit reset --hard
with extreme caution. It’s often used to completely throw away local work and get back to a known good state.
- Moves the
%%{ init: { "theme": "base", "themeVariables": { "primaryColor": "#EDE9FE", /* Default commit node color */ "primaryTextColor": "#5B21B6", "primaryBorderColor": "#5B21B6", "lineColor": "#5B21B6", /* Default branch line color */ "textColor": "#1F2937", "fontSize": "12px", /* Smaller for more text */ "fontFamily": "Open Sans", "commitLabelColor": "#FFFFFF", "commitLabelBackground": "#5B21B6", "gitBranchLabel0": "#5B21B6", /* main branch color */ "areaBoxFill": "#DBEAFE", /* Light Blue for area boxes */ "areaBoxStroke": "#2563EB", "areaBoxColor": "#1E40AF", "highlightFill": "#FEF3C7", /* Amber for highlighting states */ "highlightStroke": "#D97706" }, "flowchart": { "htmlLabels": true, "nodeSpacing": 30, /* Compact */ "rankSpacing": 40 /* Compact */ } } }%% graph LR; subgraph InitialState ["<b>Initial State</b>: main (HEAD) at C3"] direction LR C1_initial["C1"] --> C2_initial["C2"] --> C3_initial["C3 (main, HEAD)"]; style C1_initial fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6; style C2_initial fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6; style C3_initial fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6; end subgraph ResetSoft ["<b>git reset --soft C1</b>"] direction TB Soft_Log["<u>Log:</u><br>C1 (main, HEAD)"] Soft_Staging["<u>Staging Area:</u><br>Changes from C2 & C3<br><i>(Ready to commit)</i>"] Soft_WorkDir["<u>Working Directory:</u><br>Contains changes from C2 & C3<br><i>(Files look like C3)</i>"] style Soft_Log fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6; style Soft_Staging fill:#FEF3C7,stroke:#D97706,color:#92400E; style Soft_WorkDir fill:#DBEAFE,stroke:#2563EB,color:#1E40AF; end subgraph ResetMixed ["<b>git reset --mixed C1</b> (Default)"] direction TB Mixed_Log["<u>Log:</u><br>C1 (main, HEAD)"] Mixed_Staging["<u>Staging Area:</u><br>Matches C1<br><i>(Changes from C2 & C3 unstaged)</i>"] Mixed_WorkDir["<u>Working Directory:</u><br>Contains changes from C2 & C3<br><i>(Files look like C3, but changes are unstaged)</i>"] style Mixed_Log fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6; style Mixed_Staging fill:#DBEAFE,stroke:#2563EB,color:#1E40AF; style Mixed_WorkDir fill:#DBEAFE,stroke:#2563EB,color:#1E40AF; end subgraph ResetHard ["<b>git reset --hard C1</b> (Caution!)"] direction TB Hard_Log["<u>Log:</u><br>C1 (main, HEAD)"] Hard_Staging["<u>Staging Area:</u><br>Matches C1<br><i>(Changes from C2 & C3 gone)</i>"] Hard_WorkDir["<u>Working Directory:</u><br>Matches C1<br><i>(Changes from C2 & C3 GONE from files)</i>"] style Hard_Log fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6; style Hard_Staging fill:#DBEAFE,stroke:#2563EB,color:#1E40AF; style Hard_WorkDir fill:#FEE2E2,stroke:#DC2626,color:#991B1B; end InitialState -->|"Resets to C1"| ResetSoft; InitialState -->|"Resets to C1"| ResetMixed; InitialState -->|"Resets to C1"| ResetHard; classDef default fill:#transparent,stroke:#1F2937,stroke-width:1px,color:#1F2937,font-family:'Open Sans';
This diagram shows the three modes of git reset:
Initial state: C1 <- C2 <- C3 (main, HEAD)
git reset --soft C1
:main
andHEAD
point to C1. Staging area contains changes from C2 & C3. Working dir has changes from C2 & C3.git reset --mixed C1
:main
andHEAD
point to C1. Staging area matches C1. Working dir has changes from C2 & C3 as unstaged modifications.- git reset –hard C1: main and HEAD point to C1. Staging area matches C1. Working dir matches C1. Changes from C2 & C3 are gone from working dir.
git reset and Shared History:
Like git commit –amend, git reset rewrites history because it moves the branch pointer, effectively making some commits no longer part of that branch’s history. Therefore, you should avoid using git reset on commits that have already been pushed to a shared remote repository. If you do, and then try to push, you’ll need to force push, which can cause significant problems for collaborators. For shared history, git revert is the safer alternative.
Commands for Undoing Changes
Command | Purpose | Effect on Working Dir | Effect on Staging Area | Effect on History | Safety for Shared History |
---|---|---|---|---|---|
git restore –staged <file> | Unstages a file. | No change. | Removes file from staging. | No change. | Safe |
git reset HEAD <file> | Unstages a file (older command). | No change. | Removes file from staging. | No change. | Safe |
git restore <file> | Discards changes in working directory. Reverts file to staged state (if staged) or HEAD. | Discards uncommitted changes. | No change (if file was only in WD), or matches staging (if file was staged then restored). | No change. | Caution: Loses local WD changes. |
git checkout — <file> | Discards changes in working directory (older command). | Discards uncommitted changes. | No change. | No change. | Caution: Loses local WD changes. |
git commit –amend | Modifies the most recent commit (message and/or content). | Reflects new commit. | Reflects new commit. | Rewrites last commit. | Unsafe if original commit was pushed. |
git revert <commit> | Creates a new commit that undoes the changes of a specified commit. | Applies inverse changes. | Applies inverse changes (staged for new commit). | Adds a new “revert” commit. | Safe |
git reset –soft <commit> | Moves HEAD to <commit>. “Undone” commits’ changes become staged. | No change (files still reflect pre-reset state). | Updated with changes from “undone” commits. | Rewrites history (moves branch pointer). | Unsafe if original commits were pushed. |
git reset [–mixed] <commit> | Moves HEAD to <commit>. Resets staging area. “Undone” commits’ changes are in working dir as unstaged. | No change (files still reflect pre-reset state, but changes are unstaged). | Reset to match <commit>. | Rewrites history. | Unsafe if original commits were pushed. |
git reset –hard <commit> | Moves HEAD to <commit>. Resets staging area AND working directory. | Matches <commit>. Uncommitted changes & “undone” commits’ changes are GONE. | Matches <commit>. | Rewrites history. Destructive. | Unsafe if original commits were pushed. Extreme caution! |
Practical Examples
Setup:
Create a new repository for these examples.
mkdir undoing-practice
cd undoing-practice
git init
# Configure user for this repo (optional if globally set)
# git config user.name "Your Name"
# git config user.email "youremail@example.com"
echo "File 1: Initial content." > file1.txt
git add file1.txt
git commit -m "C1: Add file1.txt"
echo "File 2: Version A." > file2.txt
git add file2.txt
git commit -m "C2: Add file2.txt version A"
echo "File 1: Updated content for C3." > file1.txt # Modify file1
echo "File 3: New file." > file3.txt
git add file1.txt file3.txt
git commit -m "C3: Update file1 and add file3"
1. Unstaging a File
Suppose you want to make another commit, and you accidentally stage a file you didn’t mean to.
Modify file2.txt
and create a new file temp.txt
:
echo "File 2: Version B (work in progress)." > file2.txt
echo "Temporary data" > temp.txt
Stage both files:
git add file2.txt temp.txt
git status
Expected Output (status):
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file2.txt
new file: temp.txt
Realize temp.txt
shouldn’t be in this commit. Unstage it using git restore --staged
:
git restore --staged temp.txt
git status
Expected Output (status):
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: file2.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
temp.txt
nothing added to commit but untracked files present (use "git add" to track)
```temp.txt` is no longer staged (it's now untracked again). `file2.txt` is still staged. The changes in `temp.txt` are still in your working directory.
**Alternative using `git reset HEAD`:**
If `temp.txt` was staged, `git reset HEAD temp.txt` would also unstage it.
2. Discarding Working Directory Changes
Now, let’s say you want to completely discard the changes made to file2.txt
in your working directory (the “Version B” change).
Currently, file2.txt
is staged with “Version B”. First, let’s unstage it to simplify the example for discarding working directory changes against HEAD
.
git restore --staged file2.txt
git status
Expected Output (status will show file2.txt
as modified but not staged):
On branch main
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: file2.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
temp.txt
no changes added to commit (use "git add" and/or "git commit -a")
Discard the changes in file2.txt
using git restore
:
git restore file2.txt
cat file2.txt # Check its content
Expected Output (cat file2.txt
):
File 2: Version A.
The content of file2.txt has reverted to “Version A” (from commit C2). The “Version B” change is gone.git status will now show a clean working directory regarding file2.txt. temp.txt will still be untracked.
Alternative using git checkout –:
git checkout — file2.txt would have done the same.
3. Amending the Last Commit
Let’s say commit C3 (“C3: Update file1 and add file3”) had a typo in its message, and we also forgot to add file2.txt with its “Version A” content (which it should have had at that point).
Currently, HEAD is at C3.
First, let’s ensure file2.txt
has “Version A” (it should from the previous step). If not, set it:
# echo "File 2: Version A." > file2.txt # If needed
Stage file2.txt
to add it to the upcoming amended commit:
git add file2.txt
Amend the last commit (C3):
git commit --amend
Your editor will open with the message “C3: Update file1 and add file3”.Change the message to something like: “C3 (amended): Update file1, add file3 and correct file2”.Save and close the editor.
Expected Output (after closing editor):
[main <new_hash_C3_amended>] C3 (amended): Update file1, add file3 and correct file2
Date: <original date of C3>
3 files changed, X insertions(+), Y deletions(-) # Numbers will vary
create mode 100644 file3.txt
The original C3 is replaced by a new commit (C3_amended
) which now includes file2.txt
and has the new message. git log --oneline
will show this new commit at the tip.
4. Reverting a Commit
Suppose commit C2 (“C2: Add file2.txt version A”) introduced a problem, and we want to undo it. HEAD
is currently at C3_amended
.
Identify the hash of C2:
git log --oneline
# Let's say C2's hash is <hash_C2>
Revert commit C2:
git revert <hash_C2>
Your editor will open with a default commit message like “Revert “C2: Add file2.txt version A””. You can keep it or modify it. Save and close.
Expected Output (after closing editor):
[main <hash_C4_revert>] Revert "C2: Add file2.txt version A"
1 file changed, 1 deletion(-)
delete mode 100644 file2.txt
A new commit (C4_revert
) has been created. This commit undoes the changes made in C2. In this case, it means file2.txt
(which was added in C2) is now deleted. The original C2 is still in the history.
5. Resetting Commits
Let’s say we want to completely undo the last two commits (C4_revert
and C3_amended
) from our local history and go back to the state of C1, but keep the changes as unstaged modifications in our working directory.
Identify the hash of C1:
git log --oneline
# Let's say C1's hash is <hash_C1>
Perform a mixed reset (default mode) back to C1:
git reset <hash_C1>
# or git reset --mixed <hash_C1>
Expected Output:
Unstaged changes after reset:
M file1.txt
A file3.txt
D file2.txt # Or however the revert affected it
(The exact files and states (M/A/D) will depend on the cumulative changes in C3_amended
and C4_revert
relative to C1).
Check the status and log:
git status
git log --oneline
```git log --oneline` will show that `HEAD` and `main` are now at C1.
`git status` will show that `file1.txt`, `file2.txt`, and `file3.txt` have changes in the working directory that reflect the cumulative effect of the "undone" commits, but they are not staged.
Now, let’s try a –soft reset. Imagine we just made C3_amended and C4_revert and want to combine them into a single new commit.
Assume HEAD is at C4_revert. We want to go back to C1 but keep all changes from C3_amended and C4_revert staged.
# First, get HEAD back to C4_revert (if you followed the mixed reset example)
# You might need to recommit the changes or use git reflog to find C4_revert's hash and reset to it.
# For simplicity, let's assume we are at C4_revert.
# git log --oneline # to find <hash_C1>
git reset --soft <hash_C1>
git status
git log --oneline
will show HEAD
and main
at C1.
git status
will show all changes from C3_amended
and C4_revert
as “Changes to be committed” (staged). You could then make a single new commit.
Finally, a –hard reset (USE WITH EXTREME CAUTION).
Let’s say we want to completely discard C3_amended and C4_revert and make our local repository identical to the state of C1, losing all subsequent changes.
# Ensure you know what you're doing! This is destructive for unpushed work.
# git log --oneline # to find <hash_C1>
git reset --hard <hash_C1>
git status
git log --oneline
git log --oneline
shows HEAD
and main
at C1.
git status
shows a clean working directory (no changes).
The content of file1.txt
, file2.txt
, file3.txt
will be exactly as they were in commit C1. All changes from C3_amended
and C4_revert
are gone from your working directory and staging area.
OS-Specific Notes
- File Recovery After
git reset --hard
orgit restore
:- Git itself does not keep a separate trash can for changes discarded from the working directory by these commands. Once they are gone from your working directory (and weren’t committed or stashed), they are generally unrecoverable through Git.
- Windows: Files might go to the Recycle Bin if deleted by some editors before a hard reset, but
git reset --hard
itself doesn’t use the Recycle Bin. - macOS: Similar to Windows, no automatic “Trash” for
git reset --hard
changes. - Linux: No system-wide trash for command-line operations by default.
- Your best bet for recovery in such cases might be filesystem-level recovery tools or editor-specific local history/backup features, but these are outside of Git. This underscores the importance of committing frequently and using
--hard
resets with extreme care.
- Editor for
git commit --amend
andgit revert
:- As with regular commits, Git will use the editor configured via
core.editor
(or your system default) when these commands require you to edit or confirm a commit message. This editor behavior is consistent across OSs, though the specific editor path might vary.
- As with regular commits, Git will use the editor configured via
Common Mistakes & Troubleshooting Tips
Git Issue / Error | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Lost work after git reset –hard | Uncommitted local changes disappear from working directory. | Solution: Extreme caution with –hard. Commit/stash important changes first. Recovery outside Git (editor history, backups) might be possible. git reflog shows previous HEAD states but doesn’t recover uncommitted WD changes. |
Amended a pushed commit | git push rejected (non-fast-forward) after git commit –amend. | Solution: Avoid amending pushed commits. Use git revert for pushed changes. If amend is necessary and pushed, coordinate git push –force-with-lease with team. |
git reset on shared history | Local history diverges; force push needed, disrupting collaborators. | Solution: For shared/pushed commits, use git revert <commit>. It creates a new commit, preserving history. |
Confusing restore –staged vs. restore | Working directory changes discarded when only intending to unstage. | Solution: git restore –staged <file> = unstage. git restore <file> = discard WD changes. |
Unsure what a git reset option will do | Hesitation before running git reset due to potential data loss. | Solution: Review differences: –soft (HEAD moves, changes staged), –mixed (HEAD moves, staging reset, changes in WD), –hard (HEAD moves, staging & WD reset). Always check git status. Start with –soft or –mixed if unsure. |
Reverted the wrong commit | A git revert introduced an undesired state. | Solution: A revert is just a commit. You can revert the revert commit: git revert HEAD (if the problematic revert was the last commit). Or, identify the revert commit’s hash and git revert <revert_commit_hash>. |
Exercises
Use the undoing-practice
repository. Reset it to a known state if needed for each exercise (e.g., back to C3 by finding its hash in git reflog
and doing git reset --hard <C3_hash>
).
- Staging, Unstaging, and Discarding:
- Modify
file1.txt
andfile2.txt
. - Create a new file
extra.txt
. - Stage
file1.txt
andextra.txt
. - Check
git status
. - Unstage
extra.txt
. Checkgit status
again. - Discard the changes made to
file1.txt
in your working directory. Check its content andgit status
. - What happens to
file2.txt
‘s modifications andextra.txt
?
- Modify
- Amending and Reverting:
- Ensure your repository has at least two commits.
- Make a new commit with the message “Featur X addedd” (notice the typo) and add a new file
feature-x.txt
. - Use
git commit --amend
to fix the typo in the commit message of the last commit without changing any files. - Now, amend the last commit again, this time also adding a modification to
feature-x.txt
. - Suppose the commit before your amended one introduced a bug. Find its hash.
- Use
git revert
to undo the changes from that problematic commit. Inspect the new revert commit message and the state of your files.
- Exploring
git reset
Modes (Local History Only):- Make three new commits on top of your current
main
branch (C4, C5, C6). - Use
git log --oneline
to note their hashes and messages. - Perform
git reset --soft HEAD~2
. What doesgit status
show? What doesgit log --oneline
show? How do the files in your working directory look? - Now, from this state, perform
git reset --mixed HEAD~1
(this will take you back one more commit from the currentHEAD
, which is now effectively C4). What doesgit status
show now? What aboutgit log --oneline
? - (Carefully!) Perform git reset –hard <hash_of_original_C4>. What is the state of your repository now (log, status, file contents)?Important: This exercise is for understanding reset on local, unshared commits.
- Make three new commits on top of your current
Summary
Git provides a versatile toolkit for undoing changes at different stages:
- Unstaging:
git restore --staged <file>
: Removes file from staging; changes remain in working dir.git reset HEAD <file>
: Older equivalent to unstage.
- Discarding Working Directory Changes:
git restore <file>
: Reverts file in working dir to its state in staging (if staged) orHEAD
.git checkout -- <file>
: Older equivalent to discard working dir changes.- Caution: These discard uncommitted work permanently from the working directory.
- Amending the Last Commit:
git commit --amend
: Modifies the most recent commit (message and/or content).- Rewrites history; avoid on pushed commits.
- Reverting Commits:
git revert <commit-hash>
: Creates a new commit that undoes the changes of a specified commit.- Safe for shared history as it doesn’t alter existing commits.
- Resetting Commits:
git reset [--soft | --mixed | --hard] <commit-hash>
: Moves the current branch pointer.--soft
: MovesHEAD
; changes from “undone” commits become staged.--mixed
(default): MovesHEAD
, resets staging area; changes become unstaged in working dir.--hard
: MovesHEAD
, resets staging area and working directory; changes are lost.
- Rewrites history; dangerous for pushed commits. Use primarily for local cleanup.
Understanding the difference between history-preserving operations (git revert
) and history-rewriting operations (git commit --amend
, git reset
) is crucial for effective and safe version control, especially in collaborative environments.
Further Reading
- Pro Git Book:
- Chapter 7.6 Git Tools – Rewriting History (covers
amend
andreset
in detail, with necessary warnings): https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History - Chapter 2.4 Git Basics – Undoing Things: https://git-scm.com/book/en/v2/Git-Basics-Undoing-Things
- Chapter 7.6 Git Tools – Rewriting History (covers
- Official Git Documentation:
git-restore(1)
: https://git-scm.com/docs/git-restoregit-reset(1)
: https://git-scm.com/docs/git-resetgit-commit(1)
(see--amend
option): https://git-scm.com/docs/git-commitgit-revert(1)
: https://git-scm.com/docs/git-revert
- Atlassian Git Tutorials – Undoing Changes: https://www.atlassian.com/git/tutorials/undoing-changes
