== 揭开面纱 ==

我们揭开Git神秘面纱，往里瞧瞧它是如何创造奇迹的。我会跳过细节。更深入的描述参
见 http://www.kernel.org/pub/software/scm/git/docs/user-manual.html[ 用户手
册]。

=== 大象无形 ===

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的源起 ===

这个 http://lkml.org/lkml/2005/4/6/121[ 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压缩，因此你不应该直接查看他们。
可以通过http://www.zlib.net/zpipe.c[zpipe -d] 管道， 或者键入：

 $ git cat-file -p aa823728ea7d592acc69b36875a482cdf3fd5c8d

这漂亮地打印出给定的对象。注意，上面的cat-file命令中，aa是目录名。

=== 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+ ，但它们由一套不同的命令更新。
