提交完成后才发现:这次提交里混进了一个不该提交的文件,或者漏掉了某个文件,又或者提交信息写错了。只要这个提交还没有推送到远端,或者推送后可以安全地强制更新,就可以用几条命令把它“重做”一遍。
本文用一个真实场景,讲清楚下面这三条命令各自做了什么:
git reset --soft HEAD~1
git restore --staged cmd/ofd-wasm/web/index.html
git commit -m "fix(wasm): 移动端缩略图间距从 0 改为 8px" -- cmd/ofd-wasm/web/viewer.js场景:一次提交里混进了多余的文件 #
假设最近一次提交是这样的:
commit a1b2c3d
fix(wasm): 移动端缩略图间距从 0 改为 8px
cmd/ofd-wasm/web/index.html | 12 ++++++------
cmd/ofd-wasm/web/viewer.js | 4 ++--
2 files changed, 10 insertions(+), 6 deletions(-)其中 viewer.js 才是本次修复的内容,index.html 只是本地调试时顺手改的,不应该出现在这个提交里。
目标很明确:
- 保留
index.html的修改,但把它从这次提交中拿出来; - 让最近一次提交只包含
viewer.js; - 不产生新的“撤销提交”,保持提交历史干净。
三条命令 #
# 1. 软回退最近一次提交,改动全部保留在暂存区
git reset --soft HEAD~1
# 2. 把 index.html 从暂存区移出,修改仍留在工作区
git restore --staged cmd/ofd-wasm/web/index.html
# 3. 只提交 viewer.js
git commit -m "fix(wasm): 移动端缩略图间距从 0 改为 8px" -- cmd/ofd-wasm/web/viewer.js执行完后再看历史,最近一次提交就只剩下 viewer.js,而 index.html 仍然是工作区里未提交的修改。
每一步到底做了什么 #
理解这三条命令,关键是把 Git 的几个区域分清楚:
工作区(Working Tree) --git add--> 暂存区(Index) --git commit--> 提交(HEAD)git reset 改变的是分支指针和暂存区,git restore 改变的是暂存区或工作区,而 git commit 把暂存区(或指定路径)写成新的提交。
1. git reset --soft HEAD~1:撤销提交,保留改动
#
HEAD~1 表示当前提交的上一个提交。--soft 的意思是:只把当前分支的指针退回到上一个提交,暂存区和工作区都不动。
回退前:
HEAD -> a1b2c3d (包含 index.html 和 viewer.js)回退后:
HEAD -> 上一个提交
暂存区:仍然保留 a1b2c3d 带来的全部改动
工作区:完全没有变化所以执行完这一步,index.html 和 viewer.js 的修改都还在,只是从“已经提交”变成了“已暂存、待提交”。
HEAD~1和HEAD^在只有一个父提交时效果相同。如果要回退多个提交,可以写HEAD~2、HEAD~3。
2. git restore --staged <file>:把文件移出暂存区
#
git restore --staged 只修改暂存区,把指定文件的暂存内容恢复成 HEAD 中的版本,不会动工作区里的文件。
执行:
git restore --staged cmd/ofd-wasm/web/index.html之后:
index.html从“已暂存”变成“已修改、未暂存”,文件内容一点没丢;viewer.js仍然留在暂存区。
如果想把所有文件都移出暂存区,可以用:
git restore --staged .3. git commit -- <path>:只提交指定路径
#
普通的 git commit 会提交整个暂存区。加上 -- <path> 后,Git 只提交指定路径的内容,其他已暂存的改动不会进入这次提交。
git commit -m "fix(wasm): 移动端缩略图间距从 0 改为 8px" -- cmd/ofd-wasm/web/viewer.js这里的 -- 用来分隔“提交参数”和“路径”,避免文件名与分支名、选项名产生歧义。
这条命令会基于当前 HEAD 创建一个新提交,只把 viewer.js 的当前内容写进去,并沿用原来的提交信息。
验证结果 #
提交完成后,用下面几条命令确认:
# 查看最近两条提交
git log --oneline -2
# 查看最近一次提交涉及哪些文件
git show --stat HEAD
# 查看当前工作区状态
git status预期结果:
git show --stat HEAD中只出现cmd/ofd-wasm/web/viewer.js;git status中index.html显示为未暂存的修改;- 提交历史中没有多出“revert”或“撤销上一次提交”之类的提交。
关于第三条命令的一个细节 #
其实 git commit -- <path> 本身就会忽略其他已暂存的改动,所以第 2 步不是绝对必需的。即使 index.html 还在暂存区,直接执行:
git commit -m "..." -- cmd/ofd-wasm/web/viewer.js也只会提交 viewer.js。
那为什么还要先执行 git restore --staged?因为这样暂存区的状态更干净、意图更明确,后续执行 git status 或普通 git commit 时不容易误提交。对于初学者来说,显式地把不需要的文件移出暂存区,是更稳妥的做法。
常见变体 #
只是提交信息写错了 #
不需要回退提交,直接修改提交信息:
git commit --amend -m "新的提交信息"漏了一个文件 #
先补进暂存区,再合并到最近一次提交:
git add 漏掉的文件
git commit --amend --no-edit--no-edit 表示沿用原来的提交信息。
想沿用原提交信息重做提交 #
回退并调整好暂存区后,可以用 -C 直接复用原提交的信息,避免重新输入:
git reset --soft HEAD~1
git restore --staged 不需要的文件
git commit -C ORIG_HEAD -- 需要提交的文件ORIG_HEAD 是 reset 之前的位置,-C 表示复制提交信息但不打开编辑器。
想连作者、时间一起改 #
git commit --amend --author="名字 <邮箱>" --no-edit
git commit --amend --date="2026-11-04T10:00:00" --no-editreset 的三种模式
#
git reset 常用的三个参数区别如下:
| 模式 | 分支指针(HEAD) | 暂存区 | 工作区 | 典型用途 |
|---|---|---|---|---|
--soft |
移动 | 不变 | 不变 | 重做提交,改动全部保留在暂存区 |
--mixed(默认) |
移动 | 重置 | 不变 | 取消提交和暂存,改动留在工作区 |
--hard |
移动 | 重置 | 重置 | 彻底丢弃提交和改动,慎用 |
本文使用的是 --soft,因为它不会碰工作区,最安全。
风险和注意事项 #
只重写还没有共享出去的提交 #
重写提交会改变提交的哈希值。如果这个提交已经推送到远端,再次推送时需要强制更新:
git push --force-with-lease优先使用 --force-with-lease 而不是 --force,它会在远端出现别人新提交时拒绝覆盖,降低误伤风险。
如果分支是多人共用的,重写历史前一定要先和协作者沟通,或者改用普通的“新提交”来修正,例如:
git revert <commit>确认回退的是正确的提交 #
HEAD~1 只适用于最近一次提交,并且会沿着第一父提交回退。合并提交、变基后的历史可能比想象中复杂,操作前先看一眼:
git log --oneline --graph -5不要随手使用 --hard
#
git reset --hard HEAD~1 会连同工作区的修改一起丢掉。本文的场景只需要 --soft,没有理由使用 --hard。
万一操作错了,可以用 reflog 找回 #
git reflog 记录了 HEAD 的移动历史。只要提交对象还没被 Git 回收,通常都能找回来:
git reflog
git reset --hard HEAD@{1}一句话总结 #
git reset --soft HEAD~1 把最近一次提交“拆开”,git restore --staged <file> 把不想要的文件放回工作区,git commit -- <path> 只提交真正需要的文件。这套组合的本质就是重写最近一次提交,适用于提交错文件、漏文件、提交信息写错等常见场景。