简体中文 ▾ 主题 ▾ 最新版本 ▾ gittutorial-2 最后更新于 2.23.0

名称

gittutorial-2 - Git 教程介绍:第二部分

概要

git *

描述

在阅读本教程之前,您应该先完成 gittutorial[7]

本教程的目标是介绍 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"
[master (root-commit) 54196cc] initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 file.txt
$ echo 'hello world!' >file.txt
$ git commit -a -m "add emphasis"
[master c4d59f3] add emphasis
 1 file changed, 1 insertion(+), 1 deletion(-)

Git 对提交响应的 7 位十六进制数字是什么?

我们在教程的第一部分中看到,提交有这样的名称。事实证明,Git 历史记录中的每个对象都以 40 位十六进制名称存储。该名称是对象内容的 SHA-1 哈希值;除此之外,这确保了 Git 永远不会存储两次相同的数据(因为相同的数据会得到相同的 SHA-1 名称),并且 Git 对象的内容永远不会改变(因为那也会改变对象的名称)。这里的 7 个字符的十六进制字符串仅仅是这种 40 个字符长字符串的缩写。只要它们不含糊,缩写可以在任何可以使用 40 个字符字符串的地方使用。

预期您在遵循上述示例时创建的提交对象的内容会生成与上面显示的不同 SHA-1 哈希值,因为提交对象记录了创建时间和执行提交的人的姓名。

我们可以使用 cat-file 命令向 Git 查询此特定对象。不要复制此示例中的 40 位十六进制数字,而是使用您自己版本中的。请注意,您可以将其缩短到仅几个字符,以节省输入所有 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”对象,每个对象对应一个文件。此外,一个树也可以引用其他树对象,从而创建目录层次结构。您可以使用 ls-tree 检查任何树的内容(请记住,足够长的 SHA-1 初始部分也可以工作)。

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

因此我们看到这个树中有一个文件。SHA-1 哈希值是对该文件数据的引用。

$ git cat-file -t 3b18e512
blob

“blob”只是文件数据,我们也可以用 cat-file 检查它。

$ git cat-file blob 3b18e512
hello world

请注意,这是旧的文件数据;因此,Git 在响应初始树时命名的对象是一个树,其中包含由第一次提交记录的目录状态快照。

所有这些对象都以其 SHA-1 名称存储在 Git 目录中。

$ 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。

最简单的提交是 HEAD 提交,我们可以从 .git/HEAD 中找到它。

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

如您所见,这告诉我们当前在哪一个分支上,它通过命名 .git 目录下的一个文件来告诉我们,该文件本身包含一个指向提交对象的 SHA-1 名称,我们可以使用 cat-file 检查它。

$ 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”对象指的是树的新状态。

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

而“parent”对象指的是前一个提交。

$ 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

这个树对象是我们首先检查的那个树,而这个提交不同寻常之处在于它没有任何父提交。

大多数提交只有一个父提交,但提交有多个父提交也很常见。在这种情况下,提交代表一次合并,父引用指向合并分支的头部。

除了 blob、tree 和 commit 之外,唯一剩下的对象类型是“tag”,我们在此不讨论;详情请参阅 git-tag[1]

所以现在我们知道 Git 如何使用对象数据库来表示项目的历史。

  • “commit”对象引用表示目录树在历史中特定时间点快照的“tree”对象,并引用“parent”提交以显示它们如何连接到项目历史中。

  • “tree”对象表示单个目录的状态,将目录名与包含文件数据的“blob”对象和包含子目录信息的“tree”对象关联起来。

  • “blob”对象包含文件数据,不带任何其他结构。

  • 每个分支头部指向提交对象的引用存储在 .git/refs/heads/ 下的文件中。

  • 当前分支的名称存储在 .git/HEAD 中。

顺便提一下,许多命令都接受一个树作为参数。但正如我们上面所看到的,一个树可以通过许多不同的方式引用——​通过该树的 SHA-1 名称、通过引用该树的提交名称、通过其头部引用该树的分支名称等等——​大多数此类命令都可以接受其中任何一种名称。

在命令概要中,“tree-ish”一词有时用于指定此类参数。

索引文件

我们一直用来创建提交的主要工具是 git-commit -a,它会创建一个包含您对工作树所做所有更改的提交。但是,如果您只想提交对某些文件的更改怎么办?或者只想提交对某些文件的特定更改怎么办?

如果我们看看提交在底层是如何创建的,我们会发现有更灵活的方式来创建提交。

接着我们的测试项目,我们再次修改 file.txt。

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

但这次我们不立即进行提交,而是采取一个中间步骤,并一路要求 diff,以跟踪正在发生的事情。

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

最后一次 diff 是空的,但没有创建新的提交,并且 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 还可以显示工作目录与上次提交之间的差异,或者索引与上次提交之间的差异。

$ 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

随时,我们可以使用 git commit(不带“-a”选项)创建一个新的提交,并验证提交的状态只包含存储在索引文件中的更改,而不包含仍在工作树中的额外更改。

$ 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 使用索引来创建提交,而不是工作树;提交命令的“-a”选项告诉它首先用工作树中的所有更改更新索引。

最后,值得看一下 git add 对索引文件的影响。

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

git add 的作用是向索引文件添加一个条目。

$ 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 restore --staged <file>..." to unstage)

	new file:   closing.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)

	modified:   file.txt

由于 closing.txt 的当前状态缓存在索引文件中,因此它被列为“待提交的更改”(Changes to be committed)。由于 file.txt 在工作目录中有更改但未反映在索引中,因此它被标记为“已更改但未更新”(changed but not updated)。此时,运行“git commit”将创建一个添加 closing.txt(及其新内容)的提交,但不会修改 file.txt。

另外,请注意,裸 git diff 显示 file.txt 的更改,但不显示 closing.txt 的添加,因为索引文件中的 closing.txt 版本与工作目录中的版本相同。

除了作为新提交的暂存区之外,索引文件在检出分支时也会从对象数据库中填充,并用于保存合并操作中涉及的树。有关详细信息,请参阅 gitcore-tutorial[7] 和相关的手册页。

接下来是什么?

此时,您应该了解阅读任何 git 命令手册页所需的一切;一个好的起点是 giteveryday[7] 中提到的命令。您应该能够在 gitglossary[7] 中找到任何不熟悉的术语。

Git 用户手册 提供了对 Git 更全面的介绍。

gitcvs-migration[7] 解释了如何将 CVS 仓库导入 Git,并展示了如何以类似 CVS 的方式使用 Git。

有关 Git 使用的一些有趣示例,请参阅 howtos

对于 Git 开发者,gitcore-tutorial[7] 详细介绍了 Git 低级机制,例如,创建新提交所涉及的机制。

GIT

Git[1] 套件的一部分

scroll-to-top