English ▾ 主题 ▾ 最新版本 ▾ 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 在其对初始树的响应中命名的对象是一棵树,其中包含第一个提交记录的目录状态的快照。

所有这些对象都存储在 Git 目录下的 SHA-1 名称下

$ 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 名称,该名称引用一个 commit 对象,我们可以使用 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

但这次,我们不要立即提交,而是采取一个中间步骤,并要求沿途进行差异,以跟踪发生了什么

$ 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使用索引来创建提交,而不是工作树; 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 使用的一些有趣示例,请参阅操作指南

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

GIT

属于 git[1] 套件的一部分

scroll-to-top