Git and GitHubgit 2.54 · GitHub · macOS, Linux or Windows
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
26 small wins to finish your pathNext lesson →

Amending, reverting and resetting a commit

Amend rewrites the last commit, revert adds a new commit that cancels an old one, and reset moves the branch back to an earlier commit. All three undo a commit, and they leave very different histories behind.

Last updated: 12 Oct, 2026 · git 2.54

Undoing changes before a commit dealt with work that was not committed yet, where there was no history to protect. Once a commit exists, undoing it means changing history or adding to it, and the choice between those two is the whole of this lesson.

Three ways to undo a commit, starting from a history of A, B and C with main pointing at C. git commit --amend replaces C with a different commit C2 that has a different hash, and C is crossed out. git revert C leaves A, B and C in place and adds a new commit D holding the opposite of C's changes, so nothing is lost and the history says what happened. git reset --hard B moves main back to B and leaves C with no branch pointing at it, although C is still in the repository for a while and git reflog can find it. Amend and reset rewrite history; on a branch other people have pulled, revert is the safe one.

Syntax: the three commands, and the one that finds a commit you lost.

bash
git commit --amend -m "A better message"   # replace the last commit
git commit --amend --no-edit               # replace it, keep the message
git revert <commit> --no-edit              # add a commit that undoes that one
git reset --soft HEAD~1                    # drop the commit, keep its changes staged
git reset --hard HEAD~1                    # drop the commit and its changes
git reflog                                 # every position the branch has been in

Starting from a repository with one commit

The examples below all work in one throwaway repository: a folder with a single file, committed once. The two export lines stand in for the name and email you set once with git config --global, and fix the date so the commit hashes are the same on every run. Watching the hashes is the point of this lesson: when a hash changes, history was rewritten.

ExampleOne commit, and the hash to watch
export GIT_AUTHOR_NAME=Reader GIT_AUTHOR_EMAIL=reader@example.com GIT_AUTHOR_DATE=2026-10-12T10:00:00
export GIT_COMMITTER_NAME=Reader GIT_COMMITTER_EMAIL=reader@example.com GIT_COMMITTER_DATE=2026-10-12T10:00:00
mkdir ask-docs && cd ask-docs && git init -q -b main
printf 'chunk_size = 400\n' > settings.py
git add . && git commit -q -m "Add the settings"
git log --oneline

Fixing the last commit with amend

git commit --amend does not edit the last commit. It builds a new commit from the same parent, with whatever is staged now, and moves the branch to it. The old commit is left with nothing pointing at it.

ExampleA second commit, with a message nobody can use
printf 'Answer only from the notes.\n' > prompt.txt
git add . && git commit -q -m "add stuff"
git log --oneline

The commit is fine and the message says nothing about it. Amend replaces that commit with one that has the message you meant to write.

ExampleThe message changed, and so did the hash
git commit --amend -m "Add the prompt the model is given"
git log --oneline
  • The top hash is different. A commit is its contents, its parent, its author, its date and its message. Change any of those and it is a different commit with a different hash.
  • The bottom hash is unchanged. Only the last commit was touched.
  • A Date: line appears in the output. Git prints it when the author date is not the current time, which here is the fixed date in the export lines above.

Adding a file you forgot

--amend takes whatever is staged, so staging the forgotten file first folds it into the same commit. --no-edit keeps the message you already wrote.

ExampleA commit that names two files and holds one
git add . && git commit -q -m "Add the prompt and the loader"
printf 'def load(path):\n    return open(path).read()\n' > loader.py
git show --stat --oneline HEAD

The message promises the prompt and the loader. git show --stat lists one file, because loader.py was written after the commit was made.

ExampleOne commit that now holds both files
git add loader.py
git commit --amend --no-edit
git show --stat --oneline HEAD

The second git show --stat lists two files and the message is the same. There is no extra commit saying forgot a file, which is the point: the history reads as if the mistake never happened.

Undoing a commit by adding one, with revert

git revert works out what a commit changed, turns that upside down, and commits the result. The commit being undone stays exactly where it is. Nothing is rewritten, so no hash that anyone else has changes.

ExampleA commit that added a file that must never have been there
printf 'chunk_size = 50\n' > settings.py
printf 'OPENAI_API_KEY=real-key-here\n' > .env
git add . && git commit -q -m "Tune the chunk size"
ls -A

The commit tuned a number and swept up .env with the real key in it. Reverting it undoes both.

ExampleA new commit on top, and the old one still there
git revert HEAD --no-edit
git log --oneline

Two commits are in the log, not none. The reverted commit is where it was, and the new one above it carries the message git wrote: Revert "Tune the chunk size".

ExampleThe files as they were before
ls -A
cat settings.py
  • The files are back. .env is gone from disk and settings.py holds the old value, because the revert applied the opposite of everything that commit did.
  • --no-edit accepted the message git wrote. Without it, git opens an editor so you can explain why.
  • The key is still in the history. The commit that added .env was not removed, so anyone who clones can read it. A revert undoes the files, never the record, and a leaked key has to be revoked wherever it came from.

Moving the branch back with reset

git reset moves the branch to another commit. The three options differ only in what they do with the changes from the commits you moved past. All three below start from the same two commits, with the second one being the mistake.

reset --soft, which keeps the change staged

--soft moves the branch and leaves everything else alone, so the changes from the commit you dropped are still in the staging area.

ExampleThe commit gone from the log, the change still staged
printf 'chunk_size = 200\n' > settings.py
git add . && git commit -q -m "Try 200 character chunks"
git reset --soft HEAD~1
git log --oneline
git status --short

The commit is no longer in the log and the mark is in the left column, M . Everything the commit held is ready to be committed again, which is how you redo a commit from scratch.

reset --mixed, which is what plain git reset means

--mixed is the default. It moves the branch and empties the staging area, leaving the change on disk only.

ExampleThe same commit dropped, the change left unstaged
git reset HEAD~1
git status --short
cat settings.py

The mark moved to the right column and the file still says 200. Git prints a line about unstaged changes to say what it did, and because this is the default, a plain git reset HEAD~1 means this one.

reset --hard, which throws the change away

--hard moves the branch, empties the staging area and overwrites the files on disk.

ExampleThe commit and its change both gone
git reset --hard HEAD~1
git status --short
echo "(git status printed nothing)"
cat settings.py
  • Status printed nothing and the file is back to 400. Nothing on disk survived.
  • HEAD~1 means one commit before where you are. HEAD~2 is two, and a commit hash works in the same place.
Watch out. git reset --hard is the one command in this lesson that can lose work with no way back. Commits it moves past can be found again with git reflog, but uncommitted edits in the files it overwrites cannot: they were never in the repository. Run git status before typing it.

What happens when the commit was already pushed

Amend and reset both end with the branch pointing at a commit that is not the one the remote has, and the remote's commit is not an ancestor of yours. That is the same situation as a push that arrives out of date, and git refuses it for the same reason.

ExampleTwo commits, both on the remote as well
printf 'chunk_size = 50\n' > settings.py
git add . && git commit -q -m "Tune the chunk size"
git init --bare -q ../ask-docs.git            # the stand-in for the repository on GitHub
git remote add origin ../ask-docs.git && git push -q -u origin main
git log --oneline

The bare repository stands in for the one on GitHub, and the push put both commits there. Now the second commit turns out to be a mistake.

ExampleA reset on a branch that is already pushed
git reset --hard HEAD~1
git log --oneline
git push origin main

The push is rejected. The only way to make it go through is git push --force, which deletes the commit from the remote for everyone, including anyone who has already pulled it and built on top. That is why the rule is short: rewrite history that is still only on your machine, and revert history that has left it.

ExampleThe same mistake, undone in a way the remote accepts
git revert HEAD --no-edit
git push origin main
git log --oneline
cat settings.py

The push is an ordinary one, because the branch moved forward rather than backward. Everyone else picks the fix up with a normal pull, and the file holds the old value again.

Finding a commit you reset away

git reflog lists every commit HEAD has pointed at in this repository, including the ones no branch reaches any more. A commit is not deleted when you reset past it; it is left without a name, and the reflog still has the name.

ExampleA commit made and then reset away
printf 'chunk_size = 200\n' > settings.py
git add . && git commit -q -m "Try 200 character chunks"
git reset --hard HEAD~1
git log --oneline

The log says the commit never happened. It is still in the repository, with nothing pointing at it.

ExampleEvery position HEAD has been in
git reflog

Three lines, newest first: the reset, the commit that was dropped, and the first commit. The dropped commit's hash is on the line marked HEAD@{1}.

ExampleA commit recovered from the reflog
git reset --hard HEAD@{1}
git log --oneline
cat settings.py
  • HEAD@{1} is where HEAD was one move ago. The numbers count backwards through the list, and a hash from the same list works too.
  • The commit came back whole. Same hash, same message, same file contents.
  • The reflog is local and temporary. It is not pushed, a fresh clone has none, and entries are cleared after a while. It saves you from your own reset, not from a lost laptop.

Restore, reset, revert and amend side by side

git restoregit resetgit revertgit commit --amend
What it works ona file you have not committedwhere the branch pointsone commit, by its hashthe last commit
Does the history change?no, nothing is committed yetyes, commits are left behindno, a commit is addedyes, the last commit is replaced
Does a hash change?nothe branch moves to an older hashno, one is addedyes, the last one is new
Safe after pushing?yes, it never touched a commitnoyesno
Can you get the work back?no, it was never in a commityes, with git reflogyes, revert the revertyes, with git reflog
Reach for it whenthe edit is wrong and not committedthe last few commits should never have existed, and are not pusheda pushed commit has to be undonethe last commit is nearly right

Where each one belongs

  • A typo in the message you wrote ten seconds ago. git commit --amend -m, before anyone pulls it.
  • Three experimental commits that went nowhere, never pushed. git reset --hard HEAD~3, or --soft if you want the changes back in the staging area to try again.
  • A change that broke the project and is already on GitHub. git revert, which everyone else picks up with an ordinary pull.
Try it yourself
  • In the amend example, run git log --oneline before and after and write the two top hashes down. They are the proof that amend does not edit a commit.
  • Revert the revert: run git revert HEAD --no-edit twice and read the log. The file comes back and the history records both decisions.
  • Run git reset --hard HEAD~1 with an uncommitted edit in the file, then look for the edit in git reflog. It is not there, because it was never in a commit.

Every expert started right here.