简体中文 ▾ 主题 ▾ 最新版本 ▾ 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

一个树可以引用一个或多个“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

如您所见,这告诉我们当前所在的 branch,它通过 .git 目录下的一个文件来告诉我们,该文件本身包含一个指向 commit 对象的 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 的当前状态已缓存到索引文件中,因此它被列为“要提交的更改”。由于 file.txt 在工作目录中有更改但未反映在索引中,因此它被标记为“已更改但未更新”。此时,运行“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] 套件的一部分