Undo a local commit while keeping your changes
To undo the last unpublished commit while keeping its changes, use a soft reset when the changes should stay staged, or a mixed reset when they should return to unstaged edits. Both move the current branch backward; neither is an appropriate default for history other people already use. Inspect the staged and working changes before choosing, because keeping a file does not mean keeping its staging arrangement.
Decide what needs to change
Start with the reason for undoing the commit. If the file content is right but the commit needs another file or a different message, replacing that local commit may be enough. If the content needs to be divided differently, move back and prepare a new set of staged changes. If another person already depends on the commit, stop and choose a shared-history approach with them rather than rewriting first and asking afterward.
The reset manual separates three locations: the current commit, the index used for the next commit, and the working files. A soft reset moves the first; a mixed reset also replaces the index with the selected commit’s version1. The word “undo” therefore leaves an important question unanswered: where should the retained changes be ready for the next step?
Treat an unexpected status as a reason to pause. A file can have staged changes and newer unstaged edits at the same time. An editor may also hold unsaved text that Git has never seen. Save the work you intend to keep, inspect the repository’s state, and decide whether the last commit is actually the unit you want to change.
Run the complete example in an empty directory
This example makes three separate repositories. Each begins with the same two small commits, so the soft-reset, mixed-reset and amend results do not depend on one another. The fixed author information and dates make the printed commit identifiers reproducible. Use an empty directory and Git 2.56.0; the script refuses a different version and will not reuse its existing example directories.
Download the complete example. It changes only the new example directories it creates.
#!/bin/sh
# SPDX-License-Identifier: MIT
set -eu
export TZ=UTC LC_ALL=C
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
export GIT_AUTHOR_NAME='Example Author'
export GIT_AUTHOR_EMAIL='author@example.invalid'
export GIT_COMMITTER_NAME='Example Author'
export GIT_COMMITTER_EMAIL='author@example.invalid'
export GIT_AUTHOR_DATE='2026-10-25T12:00:00+00:00'
export GIT_COMMITTER_DATE='2026-10-25T12:00:00+00:00'
test "$(git version)" = 'git version 2.56.0'
start_case() {
mkdir "$1"
cd "$1"
git init -q --template= --initial-branch=main --object-format=sha1
git config commit.gpgsign false
git config core.autocrlf false
printf 'base\n' > note.txt
git add note.txt
git commit -qm 'Base note'
printf 'base\nsecond line\n' > note.txt
git commit -qam 'Add second line'
}
start_case soft-example
before=$(git rev-parse HEAD)
git reset --soft HEAD~1
test "$(git rev-list --count HEAD)" = 1
test "$(git status --porcelain=v1)" = 'M note.txt'
test "$(git show :note.txt)" = "$(cat note.txt)"
printf 'Soft reset: one commit; second line remains staged\n'
git status --short
git log -1 --format='%h %s'
git reset --soft "$before"
test -z "$(git status --porcelain=v1)"
printf 'Saved commit restored before any further edits\n'
cd ..
start_case mixed-example
git reset --mixed -q HEAD~1
test "$(git rev-list --count HEAD)" = 1
test "$(git status --porcelain=v1)" = ' M note.txt'
test "$(git show :note.txt)" = base
test "$(cat note.txt)" = "$(printf 'base\nsecond line')"
printf 'Mixed reset: one commit; second line remains unstaged\n'
git status --short
cd ..
start_case amend-example
old=$(git rev-parse HEAD)
printf 'extra note\n' > extra.txt
git add extra.txt
git commit --amend -qm 'Add second line and extra note'
test "$(git rev-list --count HEAD)" = 2
test "$(git rev-parse HEAD)" != "$old"
test "$(git show HEAD:extra.txt)" = 'extra note'
test -z "$(git status --porcelain=v1)"
printf 'Amend: replacement commit includes the staged extra file\n'
git log -1 --format='%h %s'
Case one: keep the change staged
In the first repository, the second commit adds a line to note.txt. The script remembers that commit, moves the branch to its parent, and checks that only one commit remains on the current branch. The file still contains both lines, and the staged copy matches it. Status shows M in the first column, followed by a blank second column.
That is the useful soft-reset result: the change is ready to be committed again. The example does not run another add command, because staging has already retained the second line. Before using this result in a real project, read the complete staged difference. Do not assume that every staged change came from the commit you just moved past.
The script then restores the remembered commit with another soft reset before making further edits. Status becomes clean because the remembered commit, staged content and file content agree again. This is a deliberately narrow reversal: it uses a commit identifier saved immediately beforehand and leaves the files alone. It is not a general recovery procedure after unrelated edits, expired history or missing objects.
Case two: leave the change unstaged
The second repository starts from the same two commits. Its mixed reset also leaves the branch with one commit, but its index contains only the original line. The working file still contains the second line. Status therefore places M in the second column, with the first column blank.
Choose this arrangement when the next commit needs a different selection of edits. Read the unstaged difference, decide what belongs together, and stage that content deliberately. The example’s second line is a convenient small unit; a real change may cross several files or depend on another change that should remain in the same commit.
Unstaged does not mean untracked. note.txt was already recorded in the first commit, so Git still recognizes it. A new file introduced only in the removed commit needs separate attention after a mixed reset. This example does not turn that different situation into a promise about every added, renamed or deleted path.
Case three: replace the local commit instead
The third repository keeps its second commit in place, adds a new file, stages it and amends. The result still has two commits along the current branch, but the last commit identifier changes and its snapshot now contains extra.txt. The script checks both facts and confirms a clean working state.
Amend creates a replacement commit; it does not edit the existing commit object in place2. Here, it is useful because the intention was to include one more file in the last local change. If the real aim is to build several smaller commits, amending once does not perform that separation. Choose the operation that matches the intended history rather than the command with the fewest characters.
Check before making the next commit
Use the current branch, its recent history and both differences as a single review. The staged difference answers what the next ordinary commit would include; the unstaged difference answers what additional file edits remain outside it3. Those views can legitimately differ. A clean status after the example’s amend is one outcome, not a requirement to hide or discard unrelated work.
The staged and working differences guide explains those two comparisons. If only one path needs to leave the staged set, use the unstaging guide instead of moving the branch. If the original problem was starting work on the wrong branch, the branch-switching guide addresses that task directly.
What changes in Git 2.56?
The new resolved-path staging option addresses conflict resolution, and the merged-branch deletion option addresses branch cleanup4. Neither changes the choice between a soft reset, a mixed reset and amending a local commit. This page keeps those separate tasks out of its example so that the observed result stays easy to inspect.
Not covered on this page
- Rewriting a branch already shared with other people.
- Removing secrets or other content from repository history.
- Root commits, merge commits, submodules or sparse checkouts.
- Recovery after object deletion, expired logs or later unrelated changes.
Sources
How we test: methodology. Found a mistake? Corrections.