Why this matters
Sometimes a whole commit is wrong, not one line in it. A commit may add a secret, break the build, or belong on the wrong branch. The previous lesson fixed only the newest commit, and it changed that commit's ID. This lesson covers two commands that undo commits in a bigger way: git reset and git revert.
They look alike, but they treat history in opposite ways. git reset moves a branch back, so the later commits drop off the branch. git revert adds a new commit that undoes the old one, so every commit stays in the record. Picking the wrong command can delete work, or it can break the copies that teammates made. This lesson shows what each command keeps and what it erases, with one small example that is used throughout.
Terms used in this lesson
A commit is a saved snapshot with a message, an author, and an ID. Each commit also stores its parent, which is the commit that came before it. A chain of commits is the history of a project.
A branch is a name that points at one commit. When you make a new commit on a branch, the name moves forward to the new commit. The name itself holds no files. It is only a pointer, which is why moving it is cheap.
HEAD is Git's name for where you are now. Usually HEAD points at a branch, and that branch points at the newest commit. HEAD~1 means the parent of HEAD, and HEAD~2 means the grandparent.
The staging area holds the changes for the next commit. The working directory is the folder of files on disk that you edit.
The reflog is Git's private record of every place HEAD has pointed to. It is a safety net. A commit that a reset removed from a branch often still appears there for a while.
The example history
Suppose a small project has three commits on the branch main. The file notes.txt is the only file that changes.
A <- B <- C mainHere is what each commit did:
- Commit A created
notes.txtwith one line, "Git stores snapshots." - Commit B added a second line, "Each commit keeps its parent."
- Commit C added a third line, "SECRET: the admin password is ...". This line should not be there.
The ID of each commit is different, but the letters A, B and C are enough for this lesson. In the examples, HEAD points at C, and main points at C too. The working directory is clean, so git status reports nothing to save.
Three kinds of reset
git reset takes a target, usually a commit, and moves the current branch to that target. The mode decides what happens to the staging area and the files on disk. There are three modes, and each one does more than the one before it.
Reset with --soft
Run this to undo commit C but keep its changes ready to commit again:
git reset --soft HEAD~1The branch now points at B. Commit C still exists in the repository, but no branch leads to it. The line "SECRET" is still in the file, and it is still on the staging area, because --soft leaves both the staging area and the files untouched. Nothing is erased.
This is useful when the commit was made with the wrong message or a few small mistakes. You can run git commit again and make one better commit in its place.
Reset with --mixed
--mixed is the default. If you type git reset HEAD~1 without a mode, you get this mode.
git reset HEAD~1The branch moves to B, and the staging area is also reset to match B. The line "SECRET" is still in notes.txt on disk, but it is no longer staged. Git shows it as a change that you have not added yet. Nothing is erased from the files.
Use this when you want to review the changes before you commit them again. It is what people usually mean when they say "undo my last commit but keep my work".
Reset with --hard
--hard does the most. It moves the branch, resets the staging area, and rewrites the files on disk to match the target.
git reset --hard HEAD~1After this command, notes.txt has only the first two lines. The "SECRET" line is gone from the file. Any edit you had made to a tracked file since commit C is gone too, and Git does not store those edits anywhere.
Two details matter here. First, commit C is still in the reflog, so you can get it back for a while. Second, --hard does not touch files that Git does not track. A new file that you never added stays where it is. Git does not track such files, so the reflog cannot bring them back if they are deleted, for example by git clean.
The three modes side by side
| Mode | Branch moves | Staging area | Files on disk |
|---|---|---|---|
--soft | Yes | Unchanged | Unchanged |
--mixed (default) | Yes | Reset to match the target | Unchanged |
--hard | Yes | Reset to match the target | Rewritten to match the target |
Read the table from left to right. Each mode keeps less than the one above it. The branch moves in all three modes, so every mode drops the commits from the branch. Only --hard erases work from the files.
Before any reset, look first
Run git status and git log --oneline before a reset. The status shows whether any edits would be lost. The log shows the commit IDs, so you can write a target that you are sure about. If you are unsure, first create a branch that names the current commit:
git branch keep-this-workThe new branch is a second name for C. A reset of main no longer loses C, because keep-this-work still points at it. You can delete the extra name later with git branch -D keep-this-work. The lower-case -d is refused here, because main no longer contains C.
Finding a commit after a hard reset
Suppose you ran git reset --hard HEAD~1 and then realised that C held work you need. Run:
git reflogThe output lists each place HEAD has been, newest first. A line for the reset looks similar to this, with a commit ID at the start:
a1b2c3d HEAD@{0}: reset: moving to HEAD~1
f5e6d7c HEAD@{1}: commit: Add the third line
9e8d7c6 HEAD@{2}: commit: Add second line
3c4d5e6 HEAD@{3}: commit (initial): Create notesThe ID on the second line is C. Create a branch at that commit so that the work is safe again:
git branch recovered f5e6d7cUse the ID that your own reflog shows. The IDs in this example are made up for the explanation. The branch can then be merged, or its files copied out. Do this soon. The reflog keeps entries for weeks, but it is not a permanent archive, and a cleanup of the repository can remove old entries.
Reverting a commit
git revert takes a commit and creates a new commit that does the opposite of it. History does not shrink. It grows by one commit.
git revert HEADGit opens a text editor with a default message, usually Revert "Add the third line". Save and close it. Now the history reads:
A <- B <- C <- D mainCommit D removes the "SECRET" line. Commit C still exists, and anybody who looks at the history can see that the line was added and then removed. This is the main reason to use revert on shared work. It does not break any copy of the branch, because it only adds a commit on top of what everybody already has.
To skip the editor, use --no-edit:
git revert --no-edit HEADTo revert a commit that is not the newest, give its ID:
git revert f5e6d7cGit creates a new commit that undoes that change, even if other commits came after it. If the later commits touched the same lines, Git can stop with a conflict. Git then lists the files, and you fix the lines by hand, run git add on the fixed files, and finish with git revert --continue.
Reverting several commits in one step
Sometimes two or more commits need to go together. You can ask Git to undo a range without committing each undo on its own:
git revert --no-commit HEAD~2..HEAD
git commit -m "Undo the last two commits"The first command applies the undo of commits B and C to the files and the staging area, but it does not commit. The second command makes one new commit. The result is one revert commit that covers both. This is the same idea as before, but with one message for the group.
Reverting a merge commit
A merge commit joins two lines of work. It has two parents, not one. Git needs to know which parent is the main line to keep. If you try to revert a merge commit without that information, Git stops:
error: commit 9d1e2f3a7c5b8e04d61f2a9c3b7e5d08f4a1c6e2 is a merge but no -m option was given.
fatal: revert failedAdd -m 1 to say that the first parent is the main line. The first parent is the branch you were on when you ran the merge:
git revert -m 1 9d1e2f3Use the ID from your own history. Reverting a merge removes the changes that came in with that merge. Commits made on the other branch are not removed.
Choosing between reset and revert
The choice depends on whether anyone else may have copied the commits you want to remove.
- If the commits exist only on your computer,
resetis fine. Nobody else is affected, and the history stays clean. - If the commits have been pushed to a branch that others use,
revertis the choice. A reset would move the branch backwards, and the remote would refuse your next push.
The reason is the same as in the previous lesson. A push works only when the remote branch is somewhere in the history of your newest commit. After a reset, your branch is behind the copy the others have, so the push is refused:
To ...
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to '...'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.The hint suggests git pull, which would bring the removed commit back onto your branch.
A force push would overwrite the remote. The teammates who pulled C would then have a branch that no longer matches the remote. They would see confusing errors on their next push or pull. A revert avoids the problem completely, because the remote only moves forward.
When history must stay
Some histories are records that other people rely on. The rule is simple: if a commit is on a shared branch, leave it in place and add a revert. This covers the main branch, release branches, and any branch that someone else has pulled.
Here is a case that comes up often. Anisha committed a secret key and pushed it. Ravi pulled the branch the same afternoon. Anisha reset the branch and force pushed, so the key is gone from the newest history. Ravi's copy still has the key, and his next push is rejected or it re-adds the old commit. The key is also still on the remote for anybody who fetches old objects, and on GitHub it may still be reachable through the commit page. Revert would have been the better choice, and it still would not have removed the key from history.
A secret that was pushed must be treated as leaked. Change it at its source, such as the password store or the service's settings page. A revert or a reset does not make a secret safe again. Removing a secret from every old commit needs a separate history rewrite, and that has its own cost for everyone who shares the repository.
Common mistakes
Running git reset --hard without checking the status. The command gives no warning and does not ask for confirmation. If the working directory had edits, they are gone. Run git status first. If the output lists files, decide what to do with them before the reset.
Reverting a merge commit without -m. The error message shown earlier appears when the commit has two parents. Add -m 1, or choose another commit if you meant a different one.
Resetting a pushed branch and then pushing normally. The push is rejected with the message shown earlier. Do not force push as the next step. Work out whether a teammate has the old commits. If they do, revert instead.
Expecting git revert to remove a file. Revert undoes the changes that the commit made, and that includes files the commit created. A file the commit only edited goes back to its older content. A file the commit did not touch is left alone.
Forgetting the reflog exists. After a reset, people often assume the commit is gone and start to rebuild the work by hand. Run git reflog first. The old commit is often still there.
Reverting a commit that depends on a later commit. If commit C changes code that commit D later builds on, the revert of C can fail to apply. Git stops with a conflict, and the fix is not simple. Read the conflict before you run git revert --abort, which cancels the revert and returns the branch to where it was.
Practice
Exercise 1. You committed a file to main on your own laptop. You have not pushed. You want to undo the commit, keep the file's changes, and commit them again later with a better message. Which command do you run?
Show the answer
Run git reset --soft HEAD~1. The branch moves back to the parent, and the changes stay staged, so you can run git commit again. If you want the changes unstaged so you can review them first, use git reset HEAD~1, which is the same as --mixed.
Exercise 2. The same commit was pushed to the shared main branch yesterday. Your teammate pulled it. The commit adds a line you want removed. Which command is right, and why?
Show the answer
Use git revert. The branch is shared, and a reset would move it backwards and force a push. Revert adds a new commit that removes the line, so the remote only moves forward and the teammate's copy still matches.
Exercise 3. You ran git reset --hard HEAD~2 by mistake. You want the two commits back. Write the commands to find them and make a branch named recovered at the newest of the two.
Show the answer
Run git reflog and find the line that reads commit: for the newest commit that you lost. Copy its ID, then run git branch recovered ID, using the ID you copied. The branch now holds the commit, and you can merge or check out the files from it.
Exercise 4. The last three commits on your local branch are experiments. Nobody else has them. You want them gone from the branch, and you do not care about the files. Which command do you run, and what do you check first?
Show the answer
Run git reset --hard HEAD~3 after checking git status and git log --oneline. Since you do not need the files, --hard is the right mode. Check the status first, because --hard also removes any uncommitted edits to tracked files. If you may need the commits later, first run git branch experiments-backup to keep a name on them.
Exercise 5. You want to revert the last two commits as one new commit with the message Undo the password change. Write the commands.
Show the answer
Run git revert --no-commit HEAD~2..HEAD, then run git commit -m "Undo the password change". The first command applies both undos to the files without committing. The second command makes one commit that holds the combined undo.
Summary
git resetmoves a branch back. Its modes decide whether the staging area and the files are also reset.--softkeeps the changes staged,--mixedkeeps them as unstaged edits, and--hardremoves them from the files.- Commits removed by a reset stay in the reflog for a while, so
git reflogcan find them and a branch can save them. git revertadds a new commit that undoes an old one, and it keeps every earlier commit in the history.- On a branch that others have pulled, use revert, because a reset followed by a force push breaks their copies.
- A secret that was pushed is leaked, and reverting it does not make it safe. Change the secret.