为什么 Git 如此快速,或者它是如何在不占用硬盘空间的情况下跟踪代码库中的每一个更改的?
Git 的强大之处在于其巧妙的数据结构和算法。它使用内容可寻址存储,将数据视为快照流,并采用聪明的压缩技术。这使得分支和合并等操作既快速又高效。
Git:你的代码时光机
在我们深入研究之前,先快速回顾一下 Git 是什么,以及为什么它受到全球开发者的喜爱:
- 分布式版本控制系统
- 由 Linus Torvalds 于 2005 年创建(是的,就是那个给我们带来 Linux 的人)
- 允许多个开发者在同一项目上协作而不互相干扰
- 跟踪每一个更改,让你可以在项目历史中“时光旅行”
现在,让我们来剖析一下这个美丽的工具,看看它是如何运作的!
Git 的核心:对象和哈希
从本质上讲,Git 是一个内容可寻址的文件系统。这是一个花哨的说法,意思是 Git 本质上是一个键值存储。“键”是内容的哈希值,“值”是内容本身。
Git 使用四种类型的对象:
- Blob:存储文件内容
- Tree:表示目录结构
- Commit:表示项目历史中的一个特定点
- Tag:为特定的提交分配一个人类可读的名称
每个对象都由一个 SHA-1 哈希标识。这个 40 字符的字符串对对象的内容是唯一的。即使只改变一个字节,你也会得到一个完全不同的哈希。
下面是 Git 如何计算 blob 对象哈希的一个简单示例:
$ echo 'Hello, Git!' | git hash-object --stdin
af5626b4a114abcb82d63db7c8082c3c4756e51b
这个哈希现在是从 Git 的对象数据库中检索内容 'Hello, Git!' 的钥匙。
快照,而非差异:Git 的时光旅行机器
与其他版本控制系统存储版本之间的差异不同,Git 在每次提交时存储整个项目的快照。这听起来可能效率不高,但实际上是一个天才的设计。
当你进行提交时,Git:
- 拍摄所有跟踪文件的快照
- 为更改的文件存储新的 blob
- 创建一个新的 tree 对象,表示目录的新状态
- 创建一个新的 commit 对象,指向这个 tree
这种方法使得切换分支或查看旧版本的操作非常快速。Git 不需要应用一系列差异;它只需检索该提交的快照。
暂存区:Git 的秘密武器
Git 的一个独特功能是暂存区(或索引)。它是工作目录和仓库之间的中间步骤。
当你运行 git add
时,你并没有将文件添加到仓库中。你是在更新索引,告诉 Git 你想在下次提交中包含哪些更改。
索引实际上是 .git 目录中的一个二进制文件。它包含一个排序的路径列表,每个路径都有权限和一个 blob 对象的 SHA-1。这就是 Git 知道在下次提交中包含哪个版本的文件的方式。
分支:指向提交的指针
这里有一个让人惊讶的事实:在 Git 中,分支只是一个可移动的指向提交的指针。仅此而已。没有文件复制,没有单独的目录。只是一个包含提交 SHA-1 的 41 字节文件。
当你创建一个新分支时,Git 只是创建一个新的指针。当你切换分支时,Git 更新 HEAD 以指向该分支,并更新你的工作目录以匹配该提交的快照。
这就是为什么在 Git 中分支如此快速和廉价。它只是更新几个指针!
打包对象:Git 的压缩魔法
还记得我们说过 Git 存储快照而不是差异吗?其实这不完全正确。Git 使用一种巧妙的技术称为“打包”来节省空间。
定期地,Git 会运行一个“垃圾回收”过程。它会查找任何从分支或标签可达的提交中未引用的对象。这些对象被打包到一个称为“包文件”的单个文件中。
在打包过程中,Git 还会查找相似的文件,并仅存储它们之间的差异(delta)。这就是 Git 在存储完整快照的同时仍能高效利用空间的原因。
变基 vs 合并:重写历史
Git 提供了两种主要方式将一个分支的更改集成到另一个分支中:合并和变基。
合并 创建一个新的“合并提交”,将两个分支的历史联系在一起。它是非破坏性的,但可能导致历史混乱。
变基,则将整个功能分支移动到主分支的顶端,实际上合并了所有的新提交。变基通过为原始分支中的每个提交创建全新的提交来重写项目历史。
以下是变基期间发生的事情的简化视图:
# 变基前
A---B---C topic
/
D---E---F---G master
# 变基后
A'--B'--C' topic
/
D---E---F---G master
带有撇号(')的提交是新的提交,具有与 A、B 和 C 相同的更改,但具有不同的父提交和 SHA-1 哈希。
远程仓库:分布式版本控制的实际应用
Git 的分布式特性意味着每个克隆都是一个完整的仓库,具有完整的历史。当你推送或拉取时,Git 只是同步仓库之间的对象。
在推送期间,Git 发送远程仓库中不存在的对象。它足够聪明,只发送必要的对象,即使对于大型仓库,推送也很高效。
而拉取则从远程检索新对象,但不将它们合并到你的工作文件中。这让你可以在决定合并之前检查更改。
总结:Git 内部的强大之处
了解 Git 的内部结构不仅仅是学术上的——它可以让你成为更有效的 Git 用户。了解 Git 如何跟踪更改可以帮助你更好地决定如何构建提交和分支。
下次当你与合并冲突作斗争或尝试优化工作流程时,记住 Git 对象模型的优雅简单。正是这个基础使得 Git 如此强大和灵活。
嘿,下次有人问你 Git 是如何工作的,你可以随意提到“内容可寻址文件系统”和“包文件”这样的术语。记得在说的时候会心一笑。
“一旦你理解了分支是映射希尔伯特空间子流形的同胚自函子,Git 就变得简单了。” - 匿名
开玩笑的!Git 的内部结构很复杂,但还没到那么复杂。祝编码愉快,愿你的提交总是原子化的,分支总是可合并的!