好吧,我想你们中的大多数人每天都或多或少地使用 git,但是您是否研究过 git 创建的 .git 文件夹中的内容?本文我们将一起探索一下,了解里面到底发生了什么。
git 在基本层面上只是一堆通过文件名相互链接的文本文件。
init
众所周知,我们从 git init
开始我们的 git 之旅。这给出了我们现在可能已经习惯的信息,特别是如果你开始并放弃了很多副项目。
Initialized empty Git repository in /home/meain/dev/src/git-talk/.git/
让我们看看目前 .git 存储库中有什么。
$ tree .git
.git
├── config
├── HEAD
├── hooks
│ └── prepare-commit-msg.msample
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
它似乎创建了一堆文件和文件夹。这些都是什么?让我们一一回顾一下。
config
是一个文本文件,其中包含当前存储库的 git 配置。如果你仔细研究它,你会看到你的存储库的一些基本设置,如作者、文件模式等。HEAD
包含存储库的当前头。根据您设置的“默认”分支,它将是 refs/heads/master 或 refs/heads/main 或您设置的任何其他分支。正如您可能已经猜到的,这指向您可以在下面看到的 refs/heads 文件夹,并指向一个名为 master 的文件,该文件目前还不存在。该文件主文件仅在您第一次提交后才会显示。hooks
包含可以在 git 执行任何操作之前/之后运行的任何脚本。objects
包含 git 对象,即存储库中有关文件、提交等的数据。refs
存储引用(指针)。 refs/heads 包含指向分支的指针,refs/tags 包含指向标签的。
add
现在您已经了解 .git 中的初始文件集是什么,让我们执行第一个操作,将某些内容添加到 .git 目录中。让我们创建一个文件并添加它(我们还没有提交它)。
echo 'meain.io' > file
git add file
这会执行以下操作:
--- init 2024-07-02 15:14:00.584674816 +0530
+++ add 2023-07-02 15:13:53.869525054 +0530
@@ -3,7 +3,10 @@
├── HEAD
├── hooks
│ └── prepare-commit-msg.msample
+├── index
├── objects
+│ ├── 4c
+│ │ └── 5b58f323d7b459664b5d3fb9587048bb0296de
│ ├── info
│ └── pack
└── refs
如您所见,这会导致两个主要变化。它修改的第一件事是索引文件。索引存储有关当前暂存内容的信息。这用于表示名为 file 的文件已添加到索引中。
第二个也是更重要的变化是在其中添加了一个新文件夹objects/4c和一个文件5b58f323d7b459664b5d3fb9587048bb0296de。
文件里有什么?
这是我们详细了解 git 如何存储内容的地方。让我们首先看看其中存在哪些类型的数据。
$ file .git/objects/5c/5b58f323d7b459664b5d3fb9587048bb0296de
.git/objects/4c/5b58f323d7b459664b5d3fb9587048bb0296de: zlib compressed data
嗯,但是 zlib 压缩数据是什么?
$ zlib-flate -uncompress <.git/objects/4c/5b58f323d7b459664b5d3fb9587048bb0296de
blob 9\0meain.io
看起来它包含我们执行 git add 的名为 file 的文件的类型、大小和数据。在本例中,数据表明它是一个大小为 9 的 blob,内容是 meain.io。
文件名是什么?
嗯,好问题。它来自于内容的sha1。如果您获取 zlib 压缩数据并通过 sha1sum 进行管道传输,您将获得文件名。
$ zlib-flate -uncompress <.git/objects/4c/5b58f323d7b459664b5d3fb9587048bb0296de|sha1sum
4c5b58f323d7b459664b5d3fb9587048bb0296de
git 获取要写入的内容的 sha1,获取前两个字符(在本例中为 4c),创建一个文件夹,然后使用其余部分作为文件名。 git 从前两个字符创建文件夹,以确保单个对象文件夹下没有太多文件。
向 git cat 文件问好
事实上,由于这是 git 中比较重要的部分之一,因此 git 还有一个管道命令来查看对象的内容。您可以使用 git cat-file,其中 -t 表示类型,-s 表示大小,-p 表示内容。
$ git cat-file -t 4c5b58f323d7b459664b5d3fb9587048bb0296de
blob
$ git cat-file -s 4c5b58f323d7b459664b5d3fb9587048bb0296de
9
$ git cat-file -p 4c5b58f323d7b459664b5d3fb9587048bb0296de
meain.io
commit
现在我们知道添加文件时会发生什么变化,让我们通过提交将其提升到一个新的水平。
$ git commit -m 'Initial commit'
[master (root-commit) 4c201df] Initial commit
1 file changed, 1 insertion(+)
create mode 100644 file
以下是发生的变化:
--- init 2024-07-02 15:14:00.584674816 +0530
+++ commit 2023-07-02 15:33:28.536144046 +0530
@@ -1,11 +1,25 @@
.git
+├── COMMIT_EDITMSG
├── config
├── HEAD
├── hooks
│ └── prepare-commit-msg.msample
├── index
+├── logs
+│ ├── HEAD
+│ └── refs
+│ └── heads
+│ └── master
├── objects
+│ ├── 3c
+│ │ └── 201df6a1c4d4c87177e30e93be1df8bfe2fe19
│ ├── 4c
│ │ └── 5b58f323d7b459664b5d3fb9587048bb0296de
+│ ├── 62
+│ │ └── 901ec0eca9faceb8fe0a9870b9b6cde75a9545
│ ├── info
│ └── pack
└── refs
├── heads
+ │ └── master
└── tags
哇,看来有很多变化。让我们一一浏览它们。第一个是新文件 COMMIT_EDITMSG。顾名思义,它包含(最后的)提交消息。
如果您在不带 -m 标志的情况下运行 git commit 命令,那么 git 获取提交消息的方式是使用 COMMIT_EDITMSG 文件打开编辑器,让用户编辑提交消息,一旦用户更新并退出编辑器,git 使用文件的内容作为提交消息。
它还添加了一个全新的文件夹日志。这是 git 记录存储库中所有提交更改的一种方式。您将能够在此处看到所有引用和 HEAD 的提交更改。
对象目录也进行了一些更改,但我希望您首先查看 refs/heads 目录,其中我们现在有文件 master.txt。您可能已经猜到这是对 master 分支的引用。让我们看看里面有什么。
$ cat refs/heads/master
3c201df6a1c4d4c87177e30e93be1df8bfe2fe19
看起来它指向新对象之一。我们知道如何观察物体,让我们这样做吧。
$ git cat-file -t 3c201df6a1c4d4c87177e30e93be1df8bfe2fe19
commit
$ git cat-file -p 3c201df6a1c4d4c87177e30e93be1df8bfe2fe19
tree 62902ec0eca9faceb8fe0a9870b9b6cde75a9545
author Abin Simon <mail@meain.io> 1688292123 +0530
committer Abin Simon <mail@meain.io> 1688292123 +0530
Initial commit
你也可以这样做 git cat-file -t refs/heads/master
嗯,看起来那是一种新的对象。这似乎是一个提交对象。提交对象的内容告诉我们,它包含一个哈希值为 62902ec0eca9faceb8fe0a9870b9b6cde75a9545 的树对象,它看起来像我们提交时添加的另一个对象。提交对象还包含有关作者和提交者是谁的信息,在本例中都是我。最后还显示了此提交的提交消息是什么。
现在让我们看看树对象包含什么。
$ git cat-file -t 62902ec0eca9faceb8fe0a9870b9b6cde75a9545
tree
$ git cat-file -p 62901ec0eca9faceb8fe0a9870b9b6cde75a9545
100644 blob 4c5b58f323d7b459664b5d3fb9587048bb0296de file
树对象将以其他树和 blob 对象的形式包含工作目录的状态。在本例中,由于我们只有一个名为 file 的文件,因此您只会看到一个对象。如果您看到的话,该文件指向我们执行 git add 文件时添加的原始对象。
这是更成熟的仓库的树的样子。更多的树对象用于从提交对象链接的树对象内部来表示文件夹。
$ git cat-file -p 2e5e84c3ee1f7e4cb3f709ff5ca0ddfc259a8d04
100644 blob 3cf56579491f151d82b384c211cf1971c300fbf8 .dockerignore
100644 blob 02c348c202dd41f90e66cfeb36ebbd928677cff6 .gitattributes
040000 tree ab2ba080c4c3e4f2bc643ae29d5040f85aca2551 .github
100644 blob bdda0724b18c16e69b800e5e887ed2a8a210c936 .gitignore
100644 blob 3a592bc0200af2fd5e3e9d2790038845f3a5cf9b CHANGELOG.md
100644 blob 71a7a8c5aacbcaccf56740ce16a6c5544783d095 CODE_OF_CONDUCT.md
100644 blob f433b1a53f5b830a205fd2df78e2b34974656c7b LICENSE
100644 blob 413072d502db332006536e1af3fad0dce570e727 README.md
100644 blob 1dd7ed99019efd6d872d5f6764115a86b5121ae9 SECURITY.md
040000 tree 918756f1a4e5d648ae273801359c440c951555f9 build
040000 tree 219a6e58af53f2e53b14b710a2dd8cbe9fea15f5 design
040000 tree 5810c119dd4d9a1c033c38c12fae781aeffeafc1 docker
040000 tree f09c5708676cdca6562f10e1f36c9cfd7ee45e07 src
040000 tree e6e1595f412599d0627a9e634007fcb2e32b62e5 website
change
让我们对文件进行更改,看看它是如何工作的。
$ echo 'blog.meain.io' > file
$ git commit -am 'Use blog link'
[master 68ed5aa] Use blog link
1 file changed, 1 insertion(+), 1 deletion(-)
它的作用如下:
--- commit 2024-07-02 15:33:28.536144046 +0530
+++ update 2023-07-02 15:47:20.841154907 +0530
@@ -17,6 +17,12 @@
│ │ └── 5b58f323d7b459664b5d3fb9587048bb0296de
│ ├── 62
│ │ └── 901ec0eca9faceb8fe0a9870b9b6cde75a9545
+│ ├── 67
+│ │ └── ed5aa2372445cf2249d85573ade1c0cbb312b1
+│ ├── 8a
+│ │ └── b377e2f9acd9eaca12e750a7d3cb345065049e
+│ ├── e5
+│ │ └── ec63cd761e6ab9d11e7dc2c4c2752d682b36e2
│ ├── info
│ └── pack
└── refs
好吧,我们添加了 3 个新对象。其中之一是包含文件新内容的 blob 对象,一个是树对象,最后一个是提交对象。
让我们从 HEAD 或 refs/heads/master 再次追踪它们。
$ git cat-file -p refs/heads/master
tree 9ab377e2f9acd9eaca12e750a7d3cb345065049e
parent 3c201df6a1c4d4c87177e30e93be1df8bfe2fe19
author Abin Simon <mail@meain.io> 1688292975 +0530
committer Abin Simon <mail@meain.io> 1688292975 +0530
Use blog link
$ git cat-file -p 9ab377e2f9acd9eaca12e750a7d3cb345065049e
100644 blob e5ec63cd761e6ab9d11e7dc2c4c2752d682b36e2 file
$ git cat-file -p e6ec63cd761e6ab9d11e7dc2c4c2752d682b36e2
blog.meain.io
那些关注的人可能已经注意到,提交对象现在有一个名为parent的附加键,它链接到先前的提交,因为此提交是在先前的提交之上创建的。
branch
我们是时候创建一个分支了。让我们使用 git branch fix-url
来做到这一点。
--- update 2024-07-02 15:47:20.841154907 +0530
+++ branch 2023-07-02 15:55:25.165204941 +0530
@@ -27,5 +28,6 @@
│ └── pack
└── refs
├── heads
+ │ ├── fix-url
│ └── master
└── tags
这会在 refs/heads 文件夹下添加一个新文件,其中文件作为分支名称,内容作为最新提交的 id。
$ cat .git/refs/heads/fix-url
68ed5aa2372445cf2249d85573ade1c0cbb312b1
这几乎就是创建分支的全部内容。 git 中的分支确实很便宜。标签的行为方式也相同,只不过它们是在 refs/tags 下创建的。
在logs目录下也添加了一个文件,用于存储类似于master分支的提交历史数据。
检查分支
在 git 中签出是指 git 获取提交的树对象并更新工作树中的文件以匹配其中记录的状态。在这种情况下,由于我们从 master 切换到 fix-url,两者都指向相同的提交和底层树对象,因此 git 在工作树中没有任何事情可做。
git checkout fix-url
当您在 .git 中进行签出时发生的唯一变化是 .git/HEAD 文件现在将指向 fix-url。
$ cat .git/HEAD
ref: refs/heads/fix-url
如果我们在这里,做出commit
。我需要这个来展示稍后合并的作用。
$ echo 'https://blog.meain.io'>file
$ git commit -am 'Fix url'
合并
主要有3种合并方式。
- 最简单也是最容易的就是快进合并。在这种情况下,您只需更新一个分支指向另一个分支指向的提交。这几乎涉及将 refs/heads/fix-url 中的哈希复制到 refs/heads/master。
- 第二个是变基合并。在这种情况下,我们首先将更改应用到 main 当前一次指向一个提交的内容之上,然后执行类似于快进合并的操作。
- 最后一种是使用单独的合并提交来合并两个分支。这有点不同,因为它的提交对象中有两个父条目。我们将在最后进一步讨论这一点。
首先让我们看看合并之前的图表是什么样子的。
git log --graph --oneline --all
* 42c6318 (fix-url) Fix url
* 67ed5aa (HEAD -> master) Use blog link
* 3c201df Initial commit
现在执行合并:
$ git merge fix-url # updates refs/heads/master to the hash in refs/heads/fix-url
$ git log --graph --oneline --all
* 42c6318 (HEAD -> master) (fix-url) Fix url
* 67ed5aa Use blog link
* 3c201df Initial commit
push
现在我们已经使用本地 git 存储库一段时间了,让我们看看推送它时会发生什么。什么正在发送到另一端的 git 存储库?
为了展示这一点,首先让我创建另一个 git 存储库,它可以用作此存储库的远程。
$ mkdir git-talk-2
$ cd git-talk-2 && git init --bare
$ cd ../git-talk && git remote add origin ../git-talk-2
现在push
$ git push origin master
让我们看看我们的仓库发生了什么变化。
--- branch 2023-07-02 15:55:25.165204941 +0530
+++ remote 2023-07-02 17:41:05.170923141 +0530
@@ -22,12 +29,18 @@
│ ├── e5
│ │ └── ec63cd761e6ab9d11e7dc2c4c2752d682b36e2
│ ├── info
│ └── pack
├── ORIG_HEAD
└── refs
├── heads
│ ├── fix-url
│ └── master
+ ├── remotes
+ │ └── origin
+ │ └── master
└── tags
它添加了一个新的 refs/remotes 来存储有关不同remote
中所有可用内容的信息。
但是什么会被发送到另一个 git 存储库呢?它是对象中和引用下的所有内容。这就是其他 git 实例获取整个 git 历史记录所需的全部内容。