3、回滚的底层原理:Git对象模型(Blob、Tree、Commit)、指针与引用(HEAD、分支指针)、回滚的本质是移动指针。
说实话,很多人用了好几年 Git,回滚操作全靠肌肉记忆——git reset --hard 敲完就跑。但真要问一句「回滚到底干了什么」,能说清楚的人不多。
我当年刚带团队的时候,就吃过这个亏。有个同事回滚完发现代码丢了,急得满头大汗。我过去一看,他以为回滚是「撤销操作」,其实 Git 压根没有「撤销」这个概念。
Git 的核心,说白了就三样东西:对象模型、指针、引用。回滚的本质,就是移动指针。你把这个搞懂了,以后任何回滚场景都不会慌。
3.1 Git 对象模型:Blob、Tree、Commit
先聊聊 Git 怎么存数据的。很多人以为 Git 存的是「文件差异」,其实不是。Git 存的是快照。
每次你执行 git commit,Git 会拍一张当前项目的「照片」。这张照片由三种对象组成:
| 对象类型 | 英文名 | 存什么 | 类比 |
|---|---|---|---|
| 数据对象 | Blob | 文件内容(二进制) | 文件本身 |
| 树对象 | Tree | 目录结构 + 文件名 + 权限 | 文件夹清单 |
| 提交对象 | Commit | 作者、时间、消息、父提交、根 Tree | 快照的「封皮」 |
举个例子。假设你有一个项目,里面就一个文件 hello.txt,内容是 "Hello Git"。当你执行 git add 时,Git 会:
- 把 "Hello Git" 这个内容存成一个 Blob 对象,得到一个哈希值(比如
a1b2c3d)。 - 创建一个 Tree 对象,里面记录着:文件名
hello.txt,权限100644,指向 Bloba1b2c3d。 - 当你
git commit时,Git 创建一个 Commit 对象,指向这个 Tree,并记录父提交(第一次提交没有父提交)。
嗯,这里要注意:Blob 只存内容,不存文件名。文件名是 Tree 的事。这意味着,如果你改了文件名但内容没变,Blob 不会重复存储。Git 的存储效率就是这么来的。
核心结论:Git 的每一次提交,都是一个完整的项目快照。不是差异补丁,是快照。这一点和 SVN 完全不同。
3.2 指针与引用:HEAD、分支指针
好,对象模型搞清楚了。那 Git 怎么知道「当前在哪个版本」?靠指针。
Git 里有两种指针:
- 分支指针:比如
main、develop,它们指向某个 Commit 对象。 - HEAD 指针:指向当前检出的分支或提交。
我画个图你就明白了:
你看,main 指向 C4,HEAD 也指向 main。所以当前工作区就是 C4 的快照。
个人习惯:我每次看 Git 状态,第一反应就是看 HEAD 指向哪。很多冲突和误操作,都是因为 HEAD 指向了意料之外的地方。
3.3 回滚的本质:移动指针
现在重点来了。回滚到底在干什么?
说白了,就是把分支指针或 HEAD 指针,从当前提交移到目标提交。
Git 不会删除任何提交对象。它只是让指针换个地方指。你想想看,这就像你手里拿着一张地图,标记着「当前位置」。回滚就是把图钉拔起来,插到另一个位置。地图本身没变,只是你的视角变了。
三种常见的回滚方式,本质都是指针操作:
| 命令 | 移动了什么 | 工作区变化 | 我常用的场景 |
|---|---|---|---|
git reset --soft |
仅移动分支指针 | 工作区不变,差异留在暂存区 | 合并多个小提交 |
git reset --mixed |
移动分支指针 + 重置暂存区 | 工作区不变,差异留在工作区 | 撤销 add 操作 |
git reset --hard |
移动分支指针 + 重置暂存区 + 重置工作区 | 工作区完全变成目标提交的状态 | 彻底放弃当前修改 |
举个例子。假设当前 main 指向 C4,你想回退到 C2:
# 当前状态:main -> C4,HEAD -> main
git reset --hard <C2的哈希>
# 执行后:main -> C2,HEAD -> main
# C3 和 C4 还在对象库里,只是没人指向它们了
我曾经踩过的坑:有一次我用了 git reset --hard 回退,然后发现 C3 和 C4 的代码找不到了。其实它们还在,只是没有分支指向。如果你还没运行 git gc(垃圾回收),可以用 git reflog 找回。记住:--hard 不是删除,是「遗忘」。
那 git revert 呢?它和 reset 不一样。revert 不是移动指针,而是创建一个新的提交,这个新提交的内容和之前的某个提交相反。说白了,revert 是「用新的提交来抵消旧的提交」。指针还是往前走,只是走出来的路把之前的路给覆盖了。
我个人建议:
- 如果你在本地分支,还没推送,用
reset最干净。 - 如果你已经推送了,或者和别人协作,用
revert更安全。因为 reset 会改写历史,别人 pull 的时候会出问题。
3.4 一张图看懂回滚本质
最后,我画个流程图,把整个逻辑串起来:
你看,整个回滚的底层逻辑,其实就是指针的移动。你理解了这一点,以后不管遇到多复杂的回滚场景,都能自己分析出该用什么命令。
避坑指南:我曾经在线上环境用 git reset --hard 回退已推送的提交,结果同事 pull 的时候直接报错。后来我养成了一个习惯:推送过的提交,只用 revert,不用 reset。这个习惯救了我好几次。
好了,这一章的核心就这些。记住三句话:
- Git 存的是快照,不是差异。
- 分支和 HEAD 都是指针。
- 回滚就是移动指针,不是删除数据。
下一章我们会聊聊实际工作中最常用的回滚场景,以及那些让你头疼的「冲突」到底怎么处理。