git的reset和revert原理?
好的,我们来深入浅出地讲解 Git 中 reset 和 revert 的原理。这两个命令都用于“撤销”更改,但它们的底层机制和适用场景有本质区别。
理解它们的关键在于理解 Git 的三个核心区域和三个重要指针。
核心概念预备知识
三个区域:
- 工作目录 (Working Directory): 你当前在编辑器里看到和修改的文件。
- 暂存区 (Staging Area / Index): 使用
git add后,更改存放的地方,准备下一次提交。 - 版本库 (Repository): 使用
git commit后,更改被永久存储的地方,包含所有的提交历史。
一个关键指针:HEAD:
- HEAD 是一个指针,它通常指向当前所在的分支指针(比如
main或develop)。 - 分支指针(如
main)则指向某个特定的提交。 - 简单理解:HEAD 指向当前的最后一次提交。
- HEAD 是一个指针,它通常指向当前所在的分支指针(比如
一、git reset(重置)
核心原理:移动 HEAD 指针(以及可选的分支指针)到指定的提交,从而“丢弃”掉一些提交。
你可以把 git reset 想象成一条时间线,HEAD 是当前你所在的时间点。reset 命令就是让你在时间线上“后退”到某个历史节点。
它有三种模式,区别在于“后退”时,如何处理工作目录和暂存区的内容。
三种模式详解
假设我们有如下提交历史:A <- B <- C (HEAD, main)。当前在提交 C。
| 模式 | 命令 | HEAD 的位置 | 暂存区 (Index) | 工作目录 (Working Directory) | 效果说明 |
|---|---|---|---|---|---|
--soft |
git reset --soft B |
移动到 B |
不改变(C的更改仍在暂存区) | 不改变 | 仅移动了HEAD指针。提交 C 中的更改被“取消提交”,但依然处于 git add 后的状态。适合重新提交。 |
--mixed (默认) |
git reset --mixed B |
移动到 B |
更新为提交B的状态(C的更改被移出暂存区) | 不改变 | 最常用模式。移动HEAD指针,并且将暂存区恢复为提交B的状态。提交C的更改变成了未暂存的状态。适合重新修改和提交。 |
--hard |
git reset --hard B |
移动到 B |
更新为提交B的状态 | 更新为提交B的状态 | 危险模式。彻底回溯到提交B的那个时间点。提交C的更改以及所有未提交的工作目录更改都会被丢弃。 |
重置示意图 (git reset --hard B):
重置前: A <- B <- C (HEAD -> main)
重置后: A <- B (HEAD -> main)
(提交C在Git数据库中暂时存在,但会被垃圾回收机制最终删除)
使用场景与警告
--soft: 想撤销上次提交,但保留更改以便立即重新提交(比如修改提交信息)。--mixed(默认): 想撤销上次提交,并且想重新检查代码,分多次提交。这是最安全的“撤销提交”方式。--hard: 彻底丢弃最近的所有更改,让代码库回到某个干净的历史状态。警告:此操作可能造成工作丢失,需极其谨慎。
关键点:git reset 会重写历史。 如果被重置的提交 C 已经被推送到远程仓库,其他人可能已经基于 C 进行了工作,那么你再强制推送 (git push --force) 会导致严重的协作问题。
二、git revert(反转/还原)
核心原理:创建一个新的提交,这个新提交的内容恰好是“撤销”指定提交的更改。
你可以把 revert 想象成一次“外科手术”。它不删除或移动任何历史节点,而是在历史的前端新增一个“负负得正”的提交,来抵消掉之前某个提交的效果。
工作原理详解
同样以历史 A <- B <- C (HEAD, main) 为例。
如果我们想撤销提交 B 引入的更改,但不删除提交 B 本身,我们执行:
git revert B
Git 会:
- 计算出提交
B与其父提交A之间的差异(即B改了哪些内容)。 - 尝试应用这个差异的逆操作。比如,如果
B新增了一行代码,revert就会删除那行代码;如果B删除了一行,revert就会把它加回来。 - 创建一个新的提交
R,其中包含了上述逆操作的结果。Git 会让你为这个新提交输入信息(如 “Revert ‘Commit B’…”)。
反转后的历史:
A <- B <- C <- R (HEAD -> main)
提交 R 的内容,等同于直接来自提交 A 的内容(就撤销 B 的更改而言)。历史是线性的、完整的。
使用场景与优点
- 安全撤销公共历史:当你要撤销的提交已经推送到公共仓库(如 GitHub)时,这是唯一安全的方法。因为它不会重写历史,不会影响其他协作者。
- 记录撤销操作:撤销行为本身也被记录为一个新的提交,便于审计和追踪为什么某个更改被移除了。
三、核心区别总结
| 特性 | git reset |
git revert |
|---|---|---|
| 操作对象 | HEAD 指针(和分支指针) | 具体某个提交的内容 |
| 历史记录 | 重写历史,丢弃旧的提交 | 添加新历史,保留所有旧提交 |
| 适用分支 | 主要用于本地分支,未推送的更改 | 主要用于公共分支,已推送的更改 |
| 安全性 | 风险较高(特别是 --hard),可能丢失工作 |
非常安全,不会丢失已提交的工作 |
| 结果 | 将分支的顶端移动到另一个提交 | 创建一个新的提交来抵消之前提交的更改 |
如何选择?
场景一:我刚做了一个本地提交,但发现有问题,想撤销它。
- 选择:
git reset HEAD~1(或git reset --mixed HEAD~1)。这是最直接的方法,让提交记录保持干净。
- 选择:
场景二:我已经把一个有问题的提交推送到了团队的公共仓库(如
main分支)。- 选择:
git revert <坏提交的哈希>。这是唯一正确的选择,可以避免破坏团队其他成员的历史记录。
- 选择:
场景三:我
git add了一些文件,但现在我不想提交它们了。- 选择:
git reset(默认--mixed)。这将取消暂存这些文件,但保留你在工作目录的修改。
- 选择:
场景四:我想彻底丢弃所有未提交的更改(包括工作目录的修改)。
- 选择:
git reset --hard HEAD。这将让工作目录和暂存区完全回退到最近一次提交的状态。请确保你真的想丢弃这些更改!
- 选择: