揭开面纱
我们揭开 Git 神秘面纱,往裡瞧瞧它是如何创造奇蹟的。我会跳过细节。更深入的描述参 见 用户手 册 。
大象无形
Git 怎麽这麽谦逊寡言呢?除了偶尔提交和合併外,你可以如常工作,就像不知道版本控 制系统存在一样。那就是,直到你需要它的时候,而且那是你欢欣的时候,Git 一直默默 注视着你。
其他版本控制系统强迫你与繁文缛节和官僚主义不断斗争。档案的权限可能是隻读的, 除非你显式地告诉中心伺服器哪些档案你打算编辑。即使最基本的命令,随着用户数目 的增多,也会慢的像爬一样。中心伺服器可能正跟踪什麽人,什麽时候 check out 了什麽 代码。当网络连接断了的时候,你就遭殃了。开发人员不断地与这些版本控制系统的种 种限制作斗争。一旦网络或中心伺服器瘫痪,工作就嘎然而止。
与之相反,Git 简单地在你工作目录下的 .git`目录保存你项目的历史。这是你自己的历 史拷贝,因此你可以保持离线,直到你想和他人沟通为止。你拥有你的档案命运完全的 控制权,因为 Git 可以轻易在任何时候从
.git`重建一个保存状态。
数据完整性
很多人把加密和保持信息机密关联起来,但一个同等重要的目标是保证信息安全。合理 使用哈希加密功能可以防止无意或有意的数据损坏行为。
一个 SHA1 哈希值可被认为是一个唯一的 160 位 ID 数,用它可以唯一标识你一生中遇到的每 个位元组串。 实际上不止如此:每个位元组串可供任何人用好多辈子。
对一个档案而言,其整体内容的哈希值可以被看作这个档案的唯一标识 ID 数。
因为一个 SHA1 哈希值本身也是一个位元组串,我们可以哈希包括其他哈希值的位元组串。这 个简单的观察出奇地有用:查看“哈希链”。我们之后会看 Git 如何利用这一点来高效地 保证数据完整性。
简言之,Git 把你数据保存在`.git/objects`子目录,那裡看不到正常档案名,相反你只 看到 ID。通过用 ID 作为档案名,加上一些档案锁和时间戳技巧,Git 把任意一个原始的文 件系统转化为一个高效而稳定的资料库。
智能
Git 是如何知道你重命名了一个档案,即使你从来没有明确提及这个事实?当然,你或许 是运行了 git mv ,但这个命令和 git add 紧接 git rm 是完全一样的。
Git 启发式地找出相连版本之间的重命名和拷贝。实际上,它能检测档案之间代码块的移 动或拷贝!儘管它不能覆盖所有的情况,但它已经做的很好了,并且这个功能也总在改 进中。如果它在你那儿不工作的话,可以尝试打开开销更高的拷贝检测选项,并考虑升 级。
索引
为每个加入管理的档案,Git 在一个名为“index”的档案裡记录统计信息,诸如大小, 创建时间和最后修改时间。为了确定档案是否更改,Git 比较其当前统计信息与那些在索 引裡的统计信息。如果一致,那 Git 就跳过重新读档案。
因为统计信息的调用比读档案内容快的很多,如果你仅仅编辑了少数几个档案,Git 几乎 不需要什麽时间就能更新他们的统计信息。
我们前面讲过索引是一个中转区。为什麽一堆档案的统计数据是一个中转区?因为添加 命令将档案放到 Git 的资料库并更新它们的统计信息,而无参数的提交命令创建一个提交, 只基于这些统计信息和已经在资料库裡的档案。
Git 的源起
这个 Linux 内核邮件列表帖子 描述了导致 Git 的一系列事件。整个讨论线索是一个令人着迷的历史探究过程,对 Git 史学家而言。
对象资料库
你数据的每个版本都保存在“对象资料库”裡,其位于子目录 .git/objects`;其他位 于
.git/`的较少数据:索引,分支名,标籤,配置选项,日志,头提交的当前位置等。 对象资料库朴素而优雅,是 Git 的力量之源。
`.git/objects`裡的每个档案是一个对象。有 3 中对象跟我们有关:“blob”对象, “tree”对象,和“commit”对象。
Blob 对象
首先来一个小把戏。去一个档案名,任意档案名。在一个空目录:
$ echo sweet > YOUR_FILENAME
$ git init
$ git add .
$ find .git/objects -type f
你将看到 .git/objects/aa/823728ea7d592acc69b36875a482cdf3fd5c8d
。
我如何在不知道档案名的情况下知道这个?这是因为以下内容的 SHA1 哈希值:
"blob" SP "6" NUL "sweet" LF
是 aa823728ea7d592acc69b36875a482cdf3fd5c8d,这裡 SP 是一个空格,NUL 是一个 0 位元组, LF 是一个换行符。你可以验证这一点,键入:
$ printf "blob 6\000sweet\n" | sha1sum
Git 基于“内容定址”:档案并不按它们的档案名存储,而是按它们包含内容的哈希值, 在一个叫“blob 对象”的档案裡。我们可以把档案内容的哈希值看作一个唯一 ID,这样 在某种意义上我们通过他们内容放置档案。开始的“blob 6”只是一个包含对象类型与 其长度的头;它简化了内部存储。
这样我可以轻易语言你所看到的。档案名是无关的:只有裡面的内容被用作构建 blob 对象。
你可能想知道对相同的档案什麽会发生。试图加一个你档案的拷贝,什麽档案名都行。 在 .git/objects
的内容保持不变,不管你加了多少。Git 只存储一次数据。
顺便说一句,在 .git/objects
裡的档案用 zlib 压缩,因此你不应该直接查看他们。 可以通过 zpipe -d 管道, 或者键入:
$ git cat-file -p aa823728ea7d592acc69b36875a482cdf3fd5c8d
这漂亮地打印出给定的对象。
Tree 对象
但档案名在哪?它们必定在某个阶段保存在某个地方。Git 在提交时得到档案名:
$ git commit # 输入一些信息。
$ find .git/objects -type f
你应看到 3 个对象。这次我不能告诉你这两个新档案是什麽,因为它部分依赖你选择的文 件名。我继续进行,假设你选了‘`rose’'。如果你没有,你可以重写历史以让它看起来 像似你做了:
$ git filter-branch --tree-filter 'mv YOUR_FILENAME rose'
$ find .git/objects -type f
现在你硬看到档案 .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9
,因为这是以下内容的 SHA1 哈希值:
"tree" SP "32" NUL "100644 rose" NUL 0xaa823728ea7d592acc69b36875a482cdf3fd5c8d
检查这个档案真的包含上面内容通过键入:
$ echo 05b217bb859794d08bb9e4f7f04cbda4b207fbe9 | git cat-file --batch
使用 zpipe,验证哈希值是容易的:
$ zpipe -d < .git/objects/05/b217bb859794d08bb9e4f7f04cbda4b207fbe9 | sha1sum
与查看档案相比,哈希值验证更技巧一些,因为其输出不止包含原始未压缩档案。
这个档案是一个“tree”对象:一组数据包含档案类型,档案名和哈希值。在我们的例 子裡,档案类型是 100644,这意味着“rose”是一个一般档案,并且哈希值指 blob 对象, 包含“rose”的内容。其他可能档案类型有可执行,连结或者目录。在最后一个例子裡, 哈希值指向一个 tree 对象。
在一些过渡性的分支,你会有一些你不在需要的老的对象,儘管有宽限过期之后,它们 会被自动清除,现在我们还是将其删除,以使我们比较容易跟上这个玩具例子。
$ rm -r .git/refs/original
$ git reflog expire --expire=now --all
$ git prune
在真实项目裡你通常应该避免像这样的命令,因为你在破换备份。如果你期望一个乾淨 的仓库,通常最好做一个新的克隆。还有,直接操作 .git
时一定要小心:如果 Git 命令同时也在运行会怎样,或者突然停电?一般,引用应由 git update-ref -d 删除,儘管通常手工删除 refs/original
也是安全的。
Commit 对象
我们已经解释了三个对象中的两个。第三个是“commit”对象。其内容依赖于提交信息 以及其创建的日期和时间。为满足这裡我们所有的,我们不得不调整一下:
$ git commit --amend -m Shakespeare # 改提交信息
$ git filter-branch --env-filter 'export
GIT_AUTHOR_DATE="Fri 13 Feb 2009 15:31:30 -0800"
GIT_AUTHOR_NAME="Alice"
GIT_AUTHOR_EMAIL="alice@example.com"
GIT_COMMITTER_DATE="Fri, 13 Feb 2009 15:31:30 -0800"
GIT_COMMITTER_NAME="Bob"
GIT_COMMITTER_EMAIL="bob@example.com"' # Rig timestamps and authors.
$ find .git/objects -type f
你现在应看到 .git/objects/49/993fe130c4b3bf24857a15d7969c396b7bc187
是下列 内容的 SHA1 哈希值:
"commit 158" NUL
"tree 05b217bb859794d08bb9e4f7f04cbda4b207fbe9" LF
"author Alice <alice@example.com> 1234567890 -0800" LF
"committer Bob <bob@example.com> 1234567890 -0800" LF
LF
"Shakespeare" LF
和前面一样,你可以运行 zpipe 或者 cat-file 来自己看。
这是第一个提交,因此没有父提交,但之后的提交将总有至少一行,指定一个父提交。
没那麽神
Git 的秘密似乎太简单。看起来似乎你可以整合几个 shell 脚本,加几行 C 代码来弄起来, 也就几个小时的事:一个基本档案操作和 SHA1 哈希化的混杂,用锁档案装饰一下,档案 同步保证健壮性。实际上,这准确描述了 Git 的最早期版本。儘管如此,除了巧妙地打包 以节省空间,巧妙地索引以省时间,我们现在知道 Git 如何灵巧地改造档案系统成为一个 对版本控制完美的资料库。
例如,如果对象资料库裡的任何一个档案由于硬碟错误损毁,那麽其哈希值将不再匹配, 这个错误会报告给我们。通过哈希化其他对象的哈希值,我们在所有层面维护数据完整 性。Commit 对象是原子的,也就是说,一个提交永远不会部分地记录变更:在我们已经 存储所有相关 tree 对象,blob 对象和父 commit 对象之后,我们才可以计算提交的的哈希 值并将其存储在资料库,对象资料库不受诸如停电之类的意外中断影响。
我们打败即使是最狡猾的对手。假设有谁试图悄悄修改一个项目裡一个远古版本档案的 内容。为使对象据库看起来健康,他们也必须修改相应 blob 对象的哈希值,既然它现在 是一个不同的位元组串。这意味着他们讲不得不引用这个档案的 tree 对象的哈希值,并反 过来改变所有与这个 tree 相关的 commit 对象的哈希值,还要加上这些提交所有后裔的哈 希值。这暗示官方 head 的哈希值与这个坏仓库不同。通过跟踪不匹配哈希值线索,我 们可以查明残缺档案,以及第一个被破坏的提交。
总之,只要 20 个位元组代表最后一次提交的是安全的,不可能篡改一个 Git 仓库。
那麽 Git 的著名功能怎样呢?分支?合併?标籤?单纯的细节。当前 head 保存在档案 .git /HEAD
,其中包含了一个 commit 对象的哈希值。该哈希值在运行提交以及其他命 令是更新。分支几乎一样:它们是保存在 .git/refs/heads
的档案。标籤也是:它们 住在住在 .git/refs/tags
,但它们由一套不同的命令更新。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论