辨析git reset和git revert
git revert:撤销指定版本的修改内容
git revert
不是让的项目“回到”版本C的状态。
作用是:创建一个全新的提交,这个新提交的内容,刚好是版本C所做修改的“反向操作”。
实例介绍
假设项目是一个故事文件 story.txt
,历史如下:
Commit A (初始)
- 文件内容:
从前有座山。
- 文件内容:
Commit B (发展)
- 增加了新的一行。
- 文件内容:
从前有座山。 山里有座庙。
Commit C (错误的修改)
- 画蛇添足,加了一句不该加的话。
- 文件内容:
从前有座山。 山里有座庙。 【庙被拆了。】 <-- 这是Commit C引入的修改
Commit D (后续的工作)
- 没意识到C的错误,继续往下写。
- 文件内容:
从前有座山。 山里有座庙。 【庙被拆了。】 和尚下山了。 <-- 这是Commit D引入的修改
现在的项目历史是 A -> B -> C -> D
。
执行 git revert C
目标:希望撤销“庙被拆了”这句错误的话,但必须保留“和尚下山了”这句有用的后续工作。
分析Commit C:Git会查看 Commit C 到底做了什么修改。它发现 C 的修改是**“增加了‘庙被拆了。’这一行”**。
执行反向操作:
revert
的核心就是做反向操作。既然 C 的操作是“增加”,那么它的反向操作就是**“删除‘庙被拆了。’这一行”**。创建新Commit E:Git 会把这个“反向操作”作为一个全新的提交,我们叫它 Commit E。
- 这个 Commit E 的提交信息通常会自动生成为 “Revert ‘Commit C 的提交信息’”。
- 这个 Commit E 的修改内容,就是从当前最新版本(D的状态)中,删除 “庙被拆了。” 这一行。
revert
之后的结果:
- 新的项目历史:
A -> B -> C -> D -> E (Revert C)
- 最终的文件内容:
从前有座山。 山里有座庙。 和尚下山了。
revert总结
- 我们没有回到 Commit C 的状态(那会丢失“和尚下山了”)。
- 我们也没有回到 Commit B 的状态(那同样会丢失“和尚下山了”)。
- 我们只是在最新的状态(D)基础上,精确地“抵消”掉了 C 所带来的变更,同时完好无损地保留了 D 的变更。
git reset : 回退到指定版本
继续用这个生动的故事例子来解析 git reset C
会发生什么。这就像是坐上了时光机,但时光机有不同的模式,对应着 reset
的三个主要选项:--hard
, --mixed
(默认), 和 --soft
。
出发前的状态(和之前一样):
- 项目历史:
A -> B -> C -> D (HEAD指针在这里)
- 当前文件
story.txt
内容:从前有座山。 山里有座庙。 【庙被拆了。】 和尚下山了。
现在,我们执行 git reset C
。这个操作的核心是:把 HEAD
指针和当前分支(比如 main
)的指针,强行移动到 commit C
。这就像是宣布:“官方历史只记录到 C 为止!”
commit D
并没有被立即删除,它只是变成了“孤魂野鬼”,不再被任何分支引用。如果后续没有其他操作,它最终会被Git的垃圾回收机制清理掉。
那么,你的工作区(story.txt
文件)和暂存区会发生什么变化呢?这取决于你使用的模式。
模式一:git reset --hard C
(最暴力、最彻底的模式)
这就像是霸道总裁的时光机,不仅回到了过去,还把之后的一切痕迹都抹除了。
操作:
git reset --hard C
时光机的行为:
- 历史指针移动:
HEAD
指针从D
移动回C
。历史记录现在看起来是A -> B -> C
。 - 暂存区重置: 暂存区(Staging Area)的内容被完全重置,使其与
C
的内容一模一样。 - 工作区重置: 你的工作目录(硬盘上的
story.txt
文件)被强制更新,使其内容也与C
的内容一模一样。
- 历史指针移动:
结果:
- 最终历史:
A -> B -> C (HEAD)
- 最终文件
story.txt
内容:从前有座山。 山里有座庙。 【庙被拆了。】
- 损失: “和尚下山了”这句在
D
中的修改,彻底消失了,无论是在暂存区还是在你的文件里,都找不到了。这是个有损操作,一定要小心!
- 最终历史:
模式二:git reset --mixed C
(默认模式)
如果你只输入 git reset C
,Git默认就是使用 --mixed
模式。这像是时光机把你带回去了,但把你之前写的草稿(工作成果)留在了桌子上。
操作:
git reset C
或git reset --mixed C
时光机的行为:
- 历史指针移动:
HEAD
指针从D
移动回C
。历史记录现在是A -> B -> C
。 - 暂存区重置: 暂存区的内容被重置,与
C
的内容保持一致。 - 工作区不变: 关键区别! 你的工作目录(
story.txt
文件)保持不变,它仍然是D
操作之后的样子。
- 历史指针移动:
结果:
- 最终历史:
A -> B -> C (HEAD)
- 最终文件
story.txt
内容:从前有座山。 山里有座庙。 【庙被拆了。】 和尚下山了。
git status
的状态: Git会告诉你,你的工作目录和暂存区有差异。它会显示“和尚下山了”这句是未暂存的修改 (unstaged change)。- 意义: Git帮你撤销了
commit D
这个提交行为,但保留了D
的代码修改。你可以检查这些修改,决定是重新提交、修改后再提交,还是彻底丢弃 (git checkout -- .
)。
- 最终历史:
模式三:git reset --soft C
(最温柔的模式)
这就像是时光机仅仅移动了书签,而书的内容和你的草稿都原封不动。
操作:
git reset --soft C
时光机的行为:
- 历史指针移动: 这是它唯一做的事情!
HEAD
指针从D
移动回C
。历史记录现在是A -> B -> C
。 - 暂存区不变: 暂存区的内容保持不变,和
D
操作后一样。 - 工作区不变: 你的工作目录(
story.txt
文件)也保持不变。
- 历史指针移动: 这是它唯一做的事情!
结果:
- 最终历史:
A -> B -> C (HEAD)
- 最终文件
story.txt
内容:从前有座山。 山里有座庙。 【庙被拆了。】 和尚下山了。
git status
的状态: Git会告诉你,你有已暂存的修改 (staged changes),这个修改就是D
所做的 “和尚下山了”。- 意义: 这个模式非常适合用于“合并提交”。比如你发现
C
和D
两个提交其实应该合成一个,你就可以reset --soft B
,然后把C
和D
的所有修改一次性重新提交。
- 最终历史:
reset总结
操作 (git reset C ) |
历史指针 (HEAD) | 暂存区 (Staging Area) | 工作目录 (文件) | 主要用途 |
---|---|---|---|---|
--hard |
回到 C |
变成 C 的样子 |
变成 C 的样子 |
彻底丢弃 C 之后的所有提交和修改,慎用! |
--mixed (默认) |
回到 C |
变成 C 的样子 |
保持不变 | 撤销提交但保留代码修改,可以重新整理。 |
--soft |
回到 C |
保持不变 | 保持不变 | 合并多个提交,或只是想修改最后一次提交的信息。 |
所以,git reset C
确实是“回到过去”,但“回到过去”之后现场会变成什么样,取决于你选择的时光机模式。这与 git revert
那种“向前走,做反向操作来弥补”的思路,有着本质的区别。
总结
git revert
像是在故事的最新结尾(“和尚下山了”之后)补写了一个新章节,内容是“哦,之前说的庙被拆了是假的”,从而优雅地修正了错误,保留了所有历史。
git reset
则是直接乘坐时光机回到“庙被拆了”那一刻,然后粗暴地撕掉了后面“和尚下山了”这一页历史,让故事看起来就像从未发生过一样。