本节目标
这一节学习 Git 中非常重要、也非常容易误用的内容:撤销。
你要理解这些问题:
工作区修改怎么撤销?
暂存区修改怎么取消?
提交错了怎么办?
reset 是什么?
revert 是什么?
reset --soft、--mixed、--hard 有什么区别?
为什么团队协作中更推荐 revert?
Gerrit 工作流中应该如何撤销错误提交?
本节重点命令:
git status
git restore
git restore --staged
git reset --soft
git reset --mixed
git reset --hard
git revert
git log --oneline
git reflog
1. 先分清三种撤销场景
学习撤销前,先判断你要撤销的是哪一层。
Git 常见撤销场景有三种:
1. 文件改错了,还没有 git add
2. 文件已经 git add,但还没有 commit
3. 已经 commit 了,想撤销提交
对应命令大概是:
撤销工作区修改 git restore 文件名
取消暂存 git restore --staged 文件名
撤销已经提交的 commit reset 或 revert
这一课重点讲第三种:
已经 commit 之后,应该怎么撤销。
2. 复习:工作区和暂存区撤销
如果只是工作区改错,还没有 git add:
git restore readme.md
作用是:
丢弃 readme.md 在工作区里的未暂存修改。
如果已经 git add,但还没有 commit:
git restore --staged readme.md
作用是:
把 readme.md 从暂存区拿出来,但保留文件内容修改。
这两个命令在第二课已经学过。
这一课继续深入:
如果已经 commit 了怎么办?
3. reset 是什么
reset 的核心作用是:
移动当前分支指针,并根据参数决定是否影响暂存区和工作区。
假设当前提交历史是:
A --- B --- C main
当前分支 main 指向 C。
如果执行:
git reset B
那么 main 会从 C 移回 B:
A --- B main
\
C 被移出当前分支历史
所以你可以先简单理解:
reset 是让当前分支回到某个提交。
但关键是:
reset 有三种常见模式:--soft、--mixed、--hard。
它们对工作区和暂存区的影响不同。
4. HEAD 是什么
学习 reset 前要理解 HEAD。
HEAD 可以理解为:
当前所在位置。
通常情况下,HEAD 指向当前分支的最新提交。
例如:
A --- B --- C main
^
HEAD
常见写法:
HEAD 当前提交
HEAD~1 当前提交的上一个提交
HEAD~2 当前提交往前两个提交
HEAD~3 当前提交往前三个提交
如果当前历史是:
A --- B --- C --- D
那么:
HEAD = D
HEAD~1 = C
HEAD~2 = B
HEAD~3 = A
5. reset --soft
命令:
git reset --soft HEAD~1
作用是:
撤销最近一次 commit,但保留修改在暂存区。
假设历史是:
A --- B --- C
执行:
git reset --soft HEAD~1
结果:
分支回到 B。
C 里的修改还在暂存区。
适合场景:
刚提交完,发现 commit message 写错了。
刚提交完,发现还想把这个提交和下一次修改合在一起。
想撤销提交,但保留所有修改,并且保持已暂存状态。
查看状态时,你可能看到:
Changes to be committed
说明修改还在暂存区。
6. reset --mixed
命令:
git reset --mixed HEAD~1
或者简写:
git reset HEAD~1
因为 --mixed 是默认模式。
作用是:
撤销最近一次 commit,保留修改在工作区,但取消暂存。
假设历史是:
A --- B --- C
执行:
git reset HEAD~1
结果:
分支回到 B。
C 里的修改还在文件里。
但这些修改没有进入暂存区。
查看状态时,你可能看到:
Changes not staged for commit
适合场景:
提交错了,想重新选择哪些文件提交。
想拆分一个提交。
想重新检查 diff 后再提交。
7. reset --hard
命令:
git reset --hard HEAD~1
作用是:
撤销最近一次 commit,并丢弃对应修改。
它会同时影响:
分支指针
暂存区
工作区
也就是说:
提交没了,文件修改也没了。
这是一个危险命令。
例如:
git reset --hard HEAD~1
表示:
回到上一个提交,并把当前工作区也恢复到那个状态。
适合场景很少:
你非常确定最近的提交和修改都不要了。
你在本地做实验,想彻底回到之前状态。
新手要特别记住:
不要随便使用 git reset --hard。
8. 三种 reset 对比
| 命令 | 撤销 commit | 保留暂存区 | 保留工作区修改 | 风险 |
|---|---|---|---|---|
git reset --soft HEAD~1 |
是 | 是 | 是 | 较低 |
git reset --mixed HEAD~1 |
是 | 否 | 是 | 中等 |
git reset --hard HEAD~1 |
是 | 否 | 否 | 高 |
简单记忆:
soft:提交撤了,修改还在暂存区。
mixed:提交撤了,修改还在工作区。
hard:提交撤了,修改也丢了。
9. reset 的重要风险
reset 会改变当前分支历史。
如果提交还只在你本地,没有推送给别人,使用 reset 通常问题不大。
但如果提交已经推送到远程,尤其是别人已经基于它开发:
不要随便 reset 后强推。
因为这会重写公共历史。
团队协作中,重写公共历史会导致:
-
别人的分支对不上
-
远程提交消失
-
合并关系混乱
-
需要别人额外处理冲突
-
Gerrit 或 CI 状态混乱
所以简单原则是:
本地未推送提交,可以考虑 reset。
已经推送或共享的提交,优先考虑 revert。
10. revert 是什么
revert 的作用是:
创建一个新的提交,用来抵消某个旧提交的修改。
假设历史是:
A --- B --- C main
现在发现 C 有问题。
执行:
git revert C
结果不是删除 C,而是新增一个提交 D:
A --- B --- C --- D main
其中 D 的作用是:
撤销 C 带来的代码变化。
所以:
reset 是回到过去。
revert 是新增一个反向提交。
11. revert 为什么适合团队协作
revert 不会重写历史。
原来的提交还在:
A --- B --- C --- D
大家的历史仍然一致。
这对团队协作很重要。
优点:
-
不破坏远程历史
-
别人的分支不会突然失去提交
-
可以清楚看到哪个提交被撤销
-
适合已经合入主分支的代码
-
适合 Gerrit 或 CI 记录追踪
所以真实团队中经常说:
已经共享出去的提交,不要 reset,应该 revert。
12. revert 基本用法
查看历史:
git log --oneline
假设看到:
def5678 添加错误配置
abc1234 添加基础文件
现在想撤销 def5678:
git revert def5678
Git 会生成一个新的提交,提交信息通常类似:
Revert "添加错误配置"
查看历史:
git log --oneline
可能变成:
9999999 Revert "添加错误配置"
def5678 添加错误配置
abc1234 添加基础文件
13. revert 可能也会冲突
revert 是反向应用某个提交。
如果后来的代码已经改过同一块内容,revert 也可能冲突。
这时流程类似冲突解决:
git status
# 手动解决冲突
git add 冲突文件
git revert --continue
如果不想继续 revert:
git revert --abort
注意:
revert 冲突解决后,用 git revert --continue。
不是 git commit。
14. reset 和 revert 的核心区别
| 对比项 | reset | revert |
|---|---|---|
| 是否重写历史 | 会 | 不会 |
| 是否新增提交 | 通常不会 | 会 |
| 适合本地未推送提交 | 适合 | 也可以 |
| 适合已共享提交 | 不推荐 | 推荐 |
| 是否容易影响别人 | 可能会 | 较安全 |
| 常见用途 | 整理本地提交 | 撤销已发布或已合入修改 |
一句话区别:
reset 是把历史指针移回去。
revert 是创建一个新提交来抵消旧提交。
15. Gerrit 中如何选择 reset 和 revert
15.1 Change 还没有合入
如果你的 Gerrit Change 还在评审中,没有 Submit。
你发现提交内容有问题,通常使用:
git add .
git commit --amend --no-edit
git push origin HEAD:refs/for/master
如果想重新整理本地提交,也可以在本地使用:
git reset --soft HEAD~1
或者:
git reset HEAD~1
然后重新提交。
因为此时还没合入目标分支,影响范围比较小。
15.2 Change 已经合入
如果 Change 已经 Submit 到 master/main/develop。
后来发现它有问题。
这时更推荐:
git revert 提交ID
然后把 revert 提交推到 Gerrit 审查:
git push origin HEAD:refs/for/master
原因是:
已经合入的提交属于共享历史,不应该随便 reset。
16. 撤销最近一次本地提交但保留修改
场景:
你刚 commit,发现提交信息不对,或者文件选错了。
还没有 push。
如果想保留修改在暂存区:
git reset --soft HEAD~1
如果想保留修改在工作区,重新选择文件:
git reset HEAD~1
然后重新检查:
git status
git diff
重新提交:
git add .
git commit -m "新的提交信息"
17. 彻底丢弃最近一次本地提交
场景:
你做了一个实验提交,现在完全不要了。
还没有 push。
可以:
git reset --hard HEAD~1
但执行前必须确认:
git log --oneline
git status
再强调一次:
git reset --hard 会丢弃修改。
新手不要把它当成普通撤销命令。
18. 撤销某个已经合入的提交
场景:
某个提交已经进了主分支,现在发现有问题。
先找到提交 ID:
git log --oneline
执行:
git revert 提交ID
然后推送到 Gerrit:
git push origin HEAD:refs/for/master
这会创建一个新的 Change:
Revert "原提交标题"
评审通过后合入,主分支就撤销了原提交的效果。
19. git reflog:后悔药
如果你误用了 reset,先不要慌。
Git 有一个很重要的命令:
git reflog
它会记录 HEAD 移动历史。
例如:
abc1234 HEAD@{0}: reset: moving to HEAD~1
def5678 HEAD@{1}: commit: 添加错误配置
如果你发现刚才 reset 错了,可以尝试回到之前的提交:
git reset --hard def5678
注意:
reflog 是本地记录。
不是远程共享历史。
虽然 reflog 很有用,但不要依赖它来随便执行危险命令。
20. 实战练习一:reset --soft
创建练习仓库:
mkdir git-reset-demo
cd git-reset-demo
git init
创建初始提交:
echo "base" > readme.txt
git add readme.txt
git commit -m "添加基础文件"
创建第二次提交:
echo "second line" >> readme.txt
git add readme.txt
git commit -m "添加第二行"
查看历史:
git log --oneline
撤销最近一次提交,保留暂存:
git reset --soft HEAD~1
查看状态:
git status
你应该看到修改还在:
Changes to be committed
21. 实战练习二:reset --mixed
在上一个练习基础上,如果你想取消暂存:
git reset
或者从一个新提交开始:
git reset HEAD~1
查看状态:
git status
你会看到:
Changes not staged for commit
说明:
提交撤销了,修改还在工作区,但没有暂存。
22. 实战练习三:reset --hard
先重新提交:
git add readme.txt
git commit -m "添加第二行"
确认历史:
git log --oneline
彻底丢弃最近一次提交:
git reset --hard HEAD~1
查看文件:
type readme.txt
你会发现第二行也消失了。
这说明:
reset --hard 不仅撤销提交,也丢弃文件修改。
23. 实战练习四:revert
创建一个错误提交:
echo "bad config" > config.txt
git add config.txt
git commit -m "添加错误配置"
查看历史:
git log --oneline
找到最新提交 ID,然后执行:
git revert HEAD
查看历史:
git log --oneline
你会看到一个新的提交:
Revert "添加错误配置"
这说明:
revert 没有删除原提交,而是新增一个反向提交。
24. 常见错误
错误一:把 reset --hard 当普通撤销
不要随便执行:
git reset --hard
它会丢弃工作区修改。
错误二:对公共分支 reset 后强推
不要随便对 main/master/develop 做:
git reset --hard 某个提交
git push --force
这会破坏团队历史。
错误三:已经合入的提交还用 reset 撤销
已经合入共享分支的提交,优先:
git revert 提交ID
错误四:revert 冲突后执行 git commit
revert 冲突解决后应该:
git revert --continue
不是直接 git commit。
错误五:不知道 HEAD~1 是谁
执行 reset 前先看:
git log --oneline
确认你要回到哪里。
25. 本节必须记住的命令
git reset --soft HEAD~1
git reset HEAD~1
git reset --hard HEAD~1
git revert 提交ID
git revert --continue
git revert --abort
git reflog
对应含义:
git reset --soft HEAD~1 撤销最近提交,修改保留在暂存区
git reset HEAD~1 撤销最近提交,修改保留在工作区
git reset --hard HEAD~1 撤销最近提交,并丢弃修改
git revert 提交ID 新建一个提交,抵消指定提交
git revert --continue 解决 revert 冲突后继续
git revert --abort 放弃当前 revert
git reflog 查看 HEAD 移动历史
26. 本节总结
这一节你学习了 Git 撤销提交:
-
reset会移动分支指针,可能重写历史 -
reset --soft保留修改在暂存区 -
reset --mixed保留修改在工作区 -
reset --hard会丢弃修改,风险最高 -
revert会新增一个反向提交,不重写历史 -
本地未推送提交可以考虑
reset -
已经共享或合入的提交更推荐
revert -
Gerrit 中已合入的问题通常通过
revert新建 Change 来撤销
最重要的一句话:
本地整理用 reset,团队协作用 revert。
下一节建议学习:
Git cherry-pick:如何把某个提交复制到另一个分支,以及 Gerrit 中如何挑选修复提交。