2009年11月2日星期一

[译文] git 简要教程 (适用于 1.5.1 或更新版本)2

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

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

让我们从一个新项目开始,并加入少量的变更历史:
$ mkdir test-project$ cd test-project$ git initInitialized 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 54196cc2commit$ git-cat-file commit 54196cc2tree 92b8b694ffb1675e5975148e1121810081dbdffeauthor J. Bruce Fields <[email protected]> 1143414668 -0500committer J. Bruce Fields <[email protected]> 1143414668 -0500

initial commit

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

这样,我们看到这个 tree 里面有一个文件。文件对应的 SHA1 哈希值是该文件内容的索引:
$ git cat-file -t 3b18e512blob

blob 对应着文件数据,我们可以用 cat-file 来看其中的内容:
$ git cat-file blob 3b18e512hello 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/HEADref: refs/heads/master

可以看到,这个命令给出了我们的当前分支,并且给出了 .git 目录下的一个文件名,该文件中包含着指向对应的 commit 对象的 SHA1 名字,这样我们就可以通过 cat-file 命令来查看这个 commit:
$ cat .git/refs/heads/masterc4d59f390b9cfd4318117afde11d601c1085f241$ git cat-file -t c4d59f39commit$ git cat-file commit c4d59f39tree d0492b368b66bdabf2ac1fd8c92b39d3db916e59parent 54196cc2703dc165cbd373a65a4dcf22d50ae7f7author J. Bruce Fields <[email protected]> 1143418702 -0500committer J. Bruce Fields <[email protected]> 1143418702 -0500

add emphasis

这里的 tree 对象对应着新的 tree 的状态:
$ git ls-tree d0492b36100644 blob a0423896973644771497bdc03eb99d5281615b51 file.txt$ git cat-file blob a0423896hello world!

而 parent 对象对应着上一个 commit:
$ git-cat-file commit 54196cc2tree 92b8b694ffb1675e5975148e1121810081dbdffeauthor J. Bruce Fields <[email protected]> 1143414668 -0500committer J. Bruce Fields <[email protected]> 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/[email protected]@ -1 +1,2 @@ hello world!+hello world, again$ git add file.txt$ git diff

最后一个 diff 是空的,不过还没有进行 commit 呢,head 还没有包含这行新的内容呢:
$ git-diff HEADdiff --git a/file.txt b/file.txtindex a042389..513feba 100644--- a/file.txt+++ b/[email protected]@ -1 +1,2 @@ hello world!+hello world, again

所以,git diff 实际是在和 head 之外的什么东西比较。实际上,这个被比较的东西是索引文件,它以二进制形式存储在 .git/index 之中,其内容可以用 ls-files 查看:
$ git ls-files --stage100644 513feba2e53ebbd2532419ded848ba19de88ba00 0 file.txt$ git cat-file -t 513feba2blob$ git cat-file blob 513feba2hello world!hello world, again

我们的 git add 的实际工作是存储一个新的 blob ,之后将其索引信息放到索引文件中。如果我们再次修改文件,将会在 git-diff 中看到这些变更:
$ echo 'again?' >>file.txt$ git diffindex 513feba..ba3da7b 100644--- a/file.txt+++ b/[email protected]@ -1,2 +1,3 @@ hello world! hello world, again+again?

使用正确的参数,git diff 也可以显示出上次 commit 以来的变更或是索引和最后一次 commit 之间的变更:
$ git diff HEADdiff --git a/file.txt b/file.txtindex a042389..ba3da7b 100644--- a/file.txt+++ b/[email protected]@ -1 +1,3 @@ hello world!+hello world, again+again?$ git diff --cacheddiff --git a/file.txt b/file.txtindex a042389..513feba 100644--- a/file.txt+++ b/[email protected]@ -1 +1,2 @@ hello world!+hello world, again

我们随时都可以使用 (没有 -a 参数的) git commit 创建一个新的 commit,只提交包含在索引文件中的变更,而不包含工作拷贝中的其他变更:
$ git commit -m "repeat"$ git diff HEADdiff --git a/file.txt b/file.txtindex 513feba..ba3da7b 100644--- a/file.txt+++ b/[email protected]@ -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 --stage100644 8b9743b20d4b15be3955fc8d5cd2b09cd2336138 0 closing.txt100644 513feba2e53ebbd2532419ded848ba19de88ba00 0 file.txt

并且,由于能够通过 cat-file 查看,新的一项指向了当前的文件内容:
$ git cat-file blob 8b9743b2goodbye, 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日 翻译

没有评论:

发表评论