我有分寸

GIT简要教程:第二部分

gnawux gittranslationvcs

阅读本教程之前,你应该已经阅读过《GIT简要教程》(译注:本人曾经翻译过)了。

本教程将介绍GIT架构中的两个基础概念——对象数据库和索引文件——并为读者提供阅读其他GIT文档所需的基础知识。

GIT对象数据库

让我们从一个新项目开始,并加入少量的变更历史:

$ mkdir test-project
$ cd test-project
$ git init
Initialized empty Git repository in .git/
$ echo 'hello world' > file.txt
$ git add .
$ git commit -a -m "initial commit"
Created initial commit 54196cc2703dc165cbd373a65a4dcf22d50ae7f7
create mode 100644 file.txt
$ echo 'hello world!' >file.txt
$ git commit -a -m "add emphasis"
Created commit c4d59f390b9cfd4318117afde11d601c1085f241

git 响应 commit 时给出的 40 位 16 进制数是什么?

在教程的第一部分里我们就看到,commit 都有类似的 40 位 16 进制的名字。 git的版本历史中,每个对象都被使用这样一个字,它是对象内容的 SHA1 哈希结果;对于变更和其他各种东西来说,这样的名字保证了 git 永远不会把同样的东西保存两次 (因为同样的内容会产生同样的 SHA1 结果),并且,一个 git 对象的内容的永远都不会改变 (因为内容改变了名字也就肯定会跟着变了)。

当然,如果你重复上面的例子的话,将会得到不一样的 SHA1 哈希结果,因为创建 commit 的时间和人都不一样。

我们可以用 cat-file 命令来向 git 查询这个对象。不要用上面那 40 位数字,应该用你自己的 commit 名字来做这个操作。事实上,你可以用开头几位数字,而不一定要敲全整个 40 位:

$ git-cat-file -t 54196cc2
commit
$ git-cat-file commit 54196cc2
tree 92b8b694ffb1675e5975148e1121810081dbdffe
author J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500
committer J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500

initial commit

一个 tree 可以对应一个或多个 blob 对象,每个都对应于一个文件。此外,一个 tree 还可以对应于多个 tree 对象,从而可以创建一个目录树。可以通过 ls-tree 命令来查看树中的内容 (注意,一个足够长的 tree 名字的开始部分就足矣了):

$ git ls-tree 92b8b694
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad file.txt

这样,我们看到这个 tree 里面有一个文件。文件对应的 SHA1 哈希值是该文件内容的索引:

$ git cat-file -t 3b18e512
blob

blob 对应着文件数据,我们可以用 cat-file 来看其中的内容:

$ git cat-file blob 3b18e512
hello world

注意,这是那个老文件的内容;即 initial tree 这个 commit 对象里所对应的 tree 是当时记录下来的目录的状态。

所有的对象都在 git 目录之中,以其 SHA1 名称存放:

$ find .git/objects/
.git/objects/
.git/objects/pack
.git/objects/info
.git/objects/3b
.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
.git/objects/92
.git/objects/92/b8b694ffb1675e5975148e1121810081dbdffe
.git/objects/54
.git/objects/54/196cc2703dc165cbd373a65a4dcf22d50ae7f7
.git/objects/a0
.git/objects/a0/423896973644771497bdc03eb99d5281615b51
.git/objects/d0
.git/objects/d0/492b368b66bdabf2ac1fd8c92b39d3db916e59
.git/objects/c4
.git/objects/c4/d59f390b9cfd4318117afde11d601c1085f241

并且,这些文件的内容只是压缩数据和用来标识它们的类型和长度的头部。可能的类型包括 blob, tree, commit 和 tag。

最容易被找到的 commit 是 HEAD commit,可以通过 .git/HEAD 找到:

$ cat .git/HEAD
ref: refs/heads/master

可以看到,这个命令给出了我们的当前分支,并且给出了 .git 目录下的一个文件名,该文件中包含着指向对应的 commit 对象的 SHA1 名字,这样我们就可以通过 cat-file 命令来查看这个 commit:

$ cat .git/refs/heads/master
c4d59f390b9cfd4318117afde11d601c1085f241
$ git cat-file -t c4d59f39
commit
$ git cat-file commit c4d59f39
tree d0492b368b66bdabf2ac1fd8c92b39d3db916e59
parent 54196cc2703dc165cbd373a65a4dcf22d50ae7f7
author J. Bruce Fields <bfields@puzzle.fieldses.org> 1143418702 -0500
committer J. Bruce Fields <bfields@puzzle.fieldses.org> 1143418702 -0500

add emphasis

这里的 tree 对象对应着新的 tree 的状态:

$ git ls-tree d0492b36
100644 blob a0423896973644771497bdc03eb99d5281615b51 file.txt
$ git cat-file blob a0423896
hello world!

而 parent 对象对应着上一个 commit:

$ git-cat-file commit 54196cc2
tree 92b8b694ffb1675e5975148e1121810081dbdffe
author J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500
committer J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500

initial commit

这里,tree 对象就是我们开始时看过的那个,这个 commit 比较特殊,没有 parent。

大部分的 commit 都有且仅有一个 parent,不过一个 commit 有几个 parent 的情况也不少见。当一个 commit 表示一次合并的时候,parent 将指向被合并的各个分支的头部。

介绍了 blob, tree 和 commit,惟一还没有介绍的对象类型就是 tag 了,这个我们就不在这里介绍了,可以参考它的手册页 git-tag(1)

现在,我们总结一下 git 的版本历史中如何使用对象数据库:

  • commit 对象会指向一个 tree 对象,即当时的目录树的镜像,还会指向 parent commit,以表征项目的版本历史。

  • tree 对象代表一个目录的一个状态,将目录名与包含文件内容的 blob 对象和包含子目录信息的 tree 对象联系在一起。

  • blob 对象包含文件内容,没有其他结构。

  • 每个分支的头部的 commit 对象的引用位于 .git/ref/heads/ 目录。

  • 当前分支的名字存储在 .git/HEAD 文件之中。

顺便提一句,很多命令都用一个 tree 作为参数。不过上面已经看到,tree 可以有多种指代方式——tree 的 SHA1 名称、指向该 tree 的 commit 名称,当该 tree 恰好是某分支头部时,该分支的名称,等等——大部分这样的命令都接受这些名字作为参数。

在命令格式描述中,tree-ish 有时被用于表示这样的参数。

索引文件

上文中用于提交 commit 的主要方法是 "git commit -a",这条命令将所有我们的工作拷贝中改动过的文件做成一个 commit。不过,如果我们只想提交部分文件怎么办?部分文件中的部分改动呢?

如果看看幕后 commit 是怎么产生的,那我们将可以得到生成 commit 的更灵活的方法。

继续我们的测试项目,现在再次修改 file.txt:

$ echo "hello world, again" >>file.txt

不过这次,我们不立刻 commit,让我们多做一个中间步骤,并查看 diff,看看都发生了什么:

$ git diff
--- a/file.txt
+++ b/file.txt
@@ -1 +1,2 @@
hello world!
+hello world, again
$ git add file.txt
$ git diff

最后一个 diff 是空的,不过还没有进行 commit 呢,head 还没有包含这行新的内容呢:

$ git-diff HEAD
diff --git a/file.txt b/file.txt
index a042389..513feba 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1,2 @@
hello world!
+hello world, again

所以,git diff 实际是在和 head 之外的什么东西比较。实际上,这个被比较的东西是索引文件,它以二进制形式存储在 .git/index 之中,其内容可以用 ls-files 查看:

$ git ls-files --stage
100644 513feba2e53ebbd2532419ded848ba19de88ba00 0 file.txt
$ git cat-file -t 513feba2
blob
$ git cat-file blob 513feba2
hello world!
hello world, again

我们的 git add 的实际工作是存储一个新的 blob ,之后将其索引信息放到索引文件中。如果我们再次修改文件,将会在 git-diff 中看到这些变更:

$ echo 'again?' >>file.txt
$ git diff
index 513feba..ba3da7b 100644
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,3 @@
hello world!
hello world, again
+again?

使用正确的参数,git diff 也可以显示出上次 commit 以来的变更或是索引和最后一次 commit 之间的变更:

$ git diff HEAD
diff --git a/file.txt b/file.txt
index a042389..ba3da7b 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1,3 @@
hello world!
+hello world, again
+again?
$ git diff --cached
diff --git a/file.txt b/file.txt
index a042389..513feba 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1,2 @@
hello world!
+hello world, again

我们随时都可以使用 (没有 -a 参数的) git commit 创建一个新的 commit,只提交包含在索引文件中的变更,而不包含工作拷贝中的其他变更:

$ git commit -m "repeat"
$ git diff HEAD
diff --git a/file.txt b/file.txt
index 513feba..ba3da7b 100644
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,3 @@
hello world!
hello world, again
+again?

缺省地,git commit 使用索引文件创建 commit,而不是工作拷贝;-a 参数的提交是首先使用工作拷贝中的全部变更更新索引,然后提交 commit。

最后,有必要看看 git add 对索引文件的效果:

$ echo "goodbye, world" >closing.txt
$ git add closing.txt

The effect of the "git add" was to add one entry to the index file:

$ git ls-files --stage
100644 8b9743b20d4b15be3955fc8d5cd2b09cd2336138 0 closing.txt
100644 513feba2e53ebbd2532419ded848ba19de88ba00 0 file.txt

并且,由于能够通过 cat-file 查看,新的一项指向了当前的文件内容:

$ git cat-file blob 8b9743b2
goodbye, world

status 命令是一个很有用的快速查看状态的方法:

$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: closing.txt
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: file.txt
#

由于 closing.txt 已经被缓存在索引文件中了,所以被列为将要提交的改动。而 file.txt 已经改动了但没被包含在索引中,它被标记为改动了但没有更新的。这时使用 git commit 将创建一个 commit 来添加 closing.txt (使用它的新内容),但不会修改 file.txt 。

此外,请记住 git diff 可以显示 file.txt 的变化,但不会显示 closing.txt 的变更,因为当前的 closing.txt 和索引文件中的是完全一致的。

除了作为新 commit 的中间环节,索引文件还在 check out 一个新分支的时候从对象数据库中取出,也用于合并操作时保存相关的分支。这里请参考相关手册页和核心教程

下面是?

现在,你已经了解了阅读 git 所有手册页所需的全部知识;一个不错的起点是学习每日 git中的命令。不知道的名词应该可以从词汇表中找到.

Git 手册包含了 git 的更全面介绍。

CVS 迁移 文档解释了如何将一个 CVS 仓库加入到 git 中,以及如何以 CVS 的方式使用 git。

对于一些有趣的 git 例子,可以看看HOWTO

对于开发者,核心教程 深度介绍了 git 的底层机制,比如,创建一个新的 commit。

原文最后更新时间: 20-May-2007 09:08:18 UTC

王旭 (gnawux<at>gmail.com) 2008年5月1日 翻译

gnawux
me!#$!@#$@#$wangxu!@#$%^&*()_me