.git 文件夹里包含了什么?

大多数人几乎每天都在使用 git,但有没有真正看过 git 生成的 .git 文件夹中的内容呢?现在,让我们一同探索,并理解里面发生了什么。

1. 开始初始化

大家都知道,在开始使用 git 时,我们首先要做的就是执行 git init。这个指令会显示出一个我们都很熟悉的提示,特别是对于那些经常启动但又很快放弃的项目。

Initialized empty Git repository in /home/meain/dev/src/git-talk/.git/

   
   

接下来,我们来探索一下 .git 仓库里都有些什么。


   
   
  1. $ tree .git
  2. .git
  3. ├── config
  4. ├── HEAD
  5. ├── hooks
  6. │ └── prepare - commit -msg.msample
  7. ├── objects
  8. │ ├── info
  9. │ └── pack
  10. └── refs
  11. ├── heads
  12. └── tags

如上所示,创建了多个文件和文件夹。它们分别扮演什么角色呢?下面我们来逐一了解。

  • config 是一个 txt 文件,里面记录了当前仓库的 git 设置,如作者信息、文件模式等。

  • HEAD 表示仓库的当前 head。根据你设置的默认分支,它可能是 refs/heads/master 或 refs/heads/main 或其他你设定的名字。实际上,它指向 refs/heads 这个文件夹,并关联了一个名为 master 的文件,但该文件目前还不存在。只有在你完成首次提交后,master 文件才会生成。

  • hooks 是一个特殊的目录,其中包含了可以在 git 执行任何操作前后运行的脚本。如果你对此感兴趣,我在这里写了一篇更详细的文章,介绍了 git 钩子的工作方式。(https://blog.meain.io/2019/making-sure-you-wont-commit-conflict-markers/)

  • objects 存放的是 git 的对象,比如关于仓库中的文件、提交等的数据。我们稍后会对此进行深入探讨。

  • refs 正如我们之前提到的,是用来存放引用的目录。例如,refs/heads 里存放的是分支的引用,而 refs/tags 则存放的是标签的引用。我们将进一步深入了解这些文件的内容。

1.1 加入一个新文件的操作

了解了 .git 中的初始文件集后,我们来进行第一个操作,将内容添加到 .git 目录。现在我们将创建并加入一个文件(此刻还未提交)。


   
   
  1. echo 'meain.io' > file
  2. git add file

执行后,变动如下:


   
   
  1. --- init 2023- 07- 02 15: 14: 00.584674816 + 0530
  2. + + + add 2023- 07- 02 15: 13: 53.869525054 + 0530
  3. @@ - 3,7 + 3,10 @@
  4. ├── HEAD
  5. ├── hooks
  6. │ └── prepare-commit-msg.msample
  7. +├── index
  8. ├── objects
  9. +│ ├── 4c
  10. +│ │ └── 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de
  11. │ ├── info
  12. │ └── pack
  13. └── refs

此操作主要引发了两个变化。首先,文件 index 被修改。index 是记录当前暂存信息的地方,这表明名为 file 的文件已经被加入到索引中。

更为关键的是,新建了一个 objects/4c 文件夹,并在其中添加了 5b58f323d7b459664b5d3fb9587048bb0296de 文件。

1.2 这个文件里都保存了什么内容?

为了深入理解 git 的存储机制,我们先来看看这个文件具体包含了什么信息。


   
   
  1. $ file .git /objects / 4c / 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de
  2. .git /objects / 4c / 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de: zlib compressed data

那么,这个用 zlib 压缩的数据中具体包含了什么呢?


   
   
  1. $ zlib-flate -uncompress <.git /objects / 4c / 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de
  2. blob 9\ 0meain.io

从结果可以看出,这个文件记录了我们之前通过 git add 命令添加的 file 文件的相关信息,包括文件的类型、大小和内容。具体地说,文件类型为 blob,大小为 9,内容则是 meain.io

1.3 那个文件名是如何得来的?

这确实是个有趣的问题。这个文件名其实是基于内容的 sha1 哈希值生成的。通过对 zlib 压缩的数据进行 sha1sum 处理,我们就可以得到这样的文件名。


   
   
  1. $ zlib-flate -uncompress <.git /objects / 4c / 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de|sha 1 sum
  2. 4c 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de

git 在存储内容时,会使用内容的 sha1 哈希值,取其前两个字符作为文件夹名(如 4c),余下的部分作为文件名。这种方式是为了确保在 objects 文件夹中不会有过多的文件,从而使文件系统保持高效。

1.4 了解 git cat-file

实际上,由于这是 git 中的一个更为重要的部分,git 提供了一个基础命令 git cat-file,让我们可以更直观地查看它。通过 -t 参数,你可以查询对象的类型;使用 -s 参数,你可以得知对象的大小;而 -p 参数则能让你直观地查看对象的具体内容。


   
   
  1. $ git cat-file -t 4c 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de
  2. blob
  3. $ git cat-file -s 4c 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de
  4. 9
  5. $ git cat-file -p 4c 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de
  6. meain.io

2. 开始提交

现在我们已经了解当增加一个文件时,git 会有哪些变化,接下来,我们将通过进行"提交"操作来进行下一步探索。


   
   
  1. $ git commit -m 'Initial commit'
  2. [master (root-commit) 4c 201df] Initial commit
  3. 1 file changed, 1 insertion( +)
  4. create mode 100644 file

以下是相关的变动:


   
   
  1. --- init 2023- 07- 02 15: 14: 00.584674816 + 0530
  2. + + + commit 2023- 07- 02 15: 33: 28.536144046 + 0530
  3. @@ - 1,11 + 1,25 @@
  4. .git
  5. +├── COMMIT_EDITMSG
  6. ├── config
  7. ├── HEAD
  8. ├── hooks
  9. │ └── prepare-commit-msg.msample
  10. ├── index
  11. +├── logs
  12. +│ ├── HEAD
  13. +│ └── refs
  14. +│ └── heads
  15. +│ └── master
  16. ├── objects
  17. +│ ├── 3c
  18. +│ │ └── 201df 6a 1c 4d 4c 87177e 30e 93be 1df 8bfe 2fe 19
  19. │ ├── 4c
  20. │ │ └── 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de
  21. +│ ├── 62
  22. +│ │ └── 901 ec 0eca 9faceb 8fe 0a 9870b 9b 6cde 75a 9545
  23. │ ├── info
  24. │ └── pack
  25. └── refs
  26. ├── heads
  27. + │ └── master
  28. └── tags

变化还真多。首先有一个新文件 COMMIT_EDITMSG,顾名思义,它保存了最新的提交信息。

若直接运行 git commit 未带 -m 参数,git 会启动一个编辑器并加载 COMMIT_EDITMSG 文件,方便用户编辑提交信息。编辑完成后,git 就采用该文件内容作为提交信息。

此外,新增了一个 logs 目录,git 通过它来记录所有的提交变动。在此,你可以查看所有引用(refs)及 HEAD 的提交记录。

object 文件夹也发生了些变化,但首先,我希望你关注一下 refs/heads 目录,里面现有一个 master 文件。毫无疑问,这就是 master 分支的引用。来,我们进一步了解其中的内容。


   
   
  1. $ cat refs /heads /master
  2. 3c 201df 6a 1c 4d 4c 87177e 30e 93be 1df 8bfe 2fe 19

显然,它是指向了一个新的对象。我们有方法查看这类对象,接着来试试。


   
   
  1. $ git cat-file -t 3c 201df 6a 1c 4d 4c 87177e 30e 93be 1df 8bfe 2fe 19
  2. commit
  3. $ git cat-file -p 3c 201df 6a 1c 4d 4c 87177e 30e 93be 1df 8bfe 2fe 19
  4. tree 62902 ec 0eca 9faceb 8fe 0a 9870b 9b 6cde 75a 9545
  5. author Abin Simon <mail@meain.io > 1688292123 + 0530
  6. committer Abin Simon <mail@meain.io > 1688292123 + 0530
  7. Initial commit

你同样可以使用 git cat-file -t refs/heads/master 命令来查看。

看起来,这是我们未曾遇见的新对象类型:commit。从 commit 的内容中,我们得知它包含了一个哈希值为 62902ec0eca9faceb8fe0a9870b9b6cde75a9545 的 tree 对象,这与我们在提交时新加的对象相似。commit 还显示了这次提交的作者和提交者信息,这里都是我。最后,它还展示了这次提交的信息。

接下来,让我们看一下 tree 对象中包含的内容。


   
   
  1. $ git cat-file -t 62902 ec 0eca 9faceb 8fe 0a 9870b 9b 6cde 75a 9545
  2. tree
  3. $ git cat-file -p 62901 ec 0eca 9faceb 8fe 0a 9870b 9b 6cde 75a 9545
  4. 100644 blob 4c 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de file

tree 对象中会通过其他 tree 和 blob 对象的形式呈现工作目录的状态。在这个示例中,因为我们仅有一个名为 file 的文件,所以你只能见到一个对象。细看的话,你会发现这个文件指向了我们在执行 git add file 时加入的那个初始对象。

下面展示了一个更为成熟的仓库中的 tree 示意。在 commit 对象关联的 tree 对象中,嵌套有更多的 tree 对象,用以标识不同的文件夹。


   
   
  1. $ git cat-file -p 2e 5e 84c 3ee 1f 7e 4cb 3f 709ff 5ca 0ddfc 259a 8d 04
  2. 100644 blob 3 cf 56579491f 151d 82b 384c 211 cf 1971c 300fbf 8 .dockerignore
  3. 100644 blob 02c 348c 202dd 41f 90e 66cfeb 36ebbd 928677cff 6 .gitattributes
  4. 040000 tree ab 2ba 080c 4c 3e 4f 2bc 643ae 29d 5040f 85aca 2551 .github
  5. 100644 blob bdda 0724b 18c 16e 69b 800e 5e 887ed 2a 8a 210c 936 .gitignore
  6. 100644 blob 3a 592bc 0200af 2 fd 5e 3e 9d 2790038845f 3a 5 cf 9b CHANGELOG.md
  7. 100644 blob 71a 7a 8c 5aacbcaccf 56740ce 16a 6c 5544783d 095 CODE_ OF_CONDUCT.md
  8. 100644 blob f 433b 1a 53f 5b 830a 205 fd 2df 78e 2b 34974656c 7b LICENSE
  9. 100644 blob 413072d 502db 332006536e 1af 3fad 0dce 570e 727 README.md
  10. 100644 blob 1dd 7ed 99019efd 6d 872d 5f 6764115a 86b 5121ae 9 SECURITY.md
  11. 040000 tree 918756f 1a 4e 5d 648ae 273801359c 440c 951555f 9 build
  12. 040000 tree 219a 6e 58af 53f 2e 53b 14b 710a 2dd 8cbe 9fea 15f 5 design
  13. 040000 tree 5810c 119dd 4d 9a 1c 033c 38c 12fae 781aeffeafc 1 docker
  14. 040000 tree f 09c 5708676cdca 6562f 10e 1f 36c 9cfd 7ee 45e 07 src
  15. 040000 tree e 6e 1595f 412599d 0627a 9e 634007fcb 2e 32b 62e 5 website

3. 进行修改

让我们对文件进行修改,并观察这样做的结果。


   
   
  1. $ echo 'blog.meain.io' > file
  2. $ git commit -am 'Use blog link'
  3. [master 68ed 5aa] Use blog link
  4. 1 file changed, 1 insertion( +), 1 deletion(-)

更改内容如下:


   
   
  1. --- commit 2023- 07- 02 15: 33: 28.536144046 + 0530
  2. + + + update 2023- 07- 02 15: 47: 20.841154907 + 0530
  3. @@ - 17,6 + 17,12 @@
  4. │ │ └── 5b 58f 323d 7b 459664b 5d 3fb 9587048bb 0296 de
  5. │ ├── 62
  6. │ │ └── 901 ec 0eca 9faceb 8fe 0a 9870b 9b 6cde 75a 9545
  7. +│ ├── 67
  8. +│ │ └── ed 5aa 2372445 cf 2249d 85573ade 1c 0cbb 312b 1
  9. +│ ├── 8a
  10. +│ │ └── b 377e 2f 9acd 9eaca 12e 750a 7d 3cb 345065049e
  11. +│ ├── e 5
  12. +│ │ └── ec 63cd 761e 6ab 9d 11e 7dc 2c 4c 2752d 682b 36e 2
  13. │ ├── info
  14. │ └── pack
  15. └── refs

总的来说,我们新增了三个对象。一个是含有文件新内容的 blob 对象,还有一个是 tree 对象,以及一个 commit 对象。

我们再次从 HEAD 或 refs/heads/master 开始追踪这些对象。


   
   
  1. $ git cat-file -p refs /heads /master
  2. tree 9ab 377e 2f 9acd 9eaca 12e 750a 7d 3cb 345065049e
  3. parent 3c 201df 6a 1c 4d 4c 87177e 30e 93be 1df 8bfe 2fe 19
  4. author Abin Simon <mail@meain.io > 1688292975 + 0530
  5. committer Abin Simon <mail@meain.io > 1688292975 + 0530
  6. Use blog link
  7. $ git cat-file -p 9ab 377e 2f 9acd 9eaca 12e 750a 7d 3cb 345065049e
  8. 100644 blob e 5 ec 63cd 761e 6ab 9d 11e 7dc 2c 4c 2752d 682b 36e 2 file
  9. $ git cat-file -p e 6 ec 63cd 761e 6ab 9d 11e 7dc 2c 4c 2752d 682b 36e 2
  10. blog.meain.io

仔细观察的话,你会注意到 commit 对象现在有了一个额外的键名为 parent,它链接到上一个提交,因为当前提交是基于上一个提交创建的。

4. 创建新分支

现在我们需要创建一个新的分支。我们将使用 git branch fix-url 来完成这个操作。


   
   
  1. --- update 2023- 07- 02 15: 47: 20.841154907 + 0530
  2. + + + branch 2023- 07- 02 15: 55: 25.165204941 + 0530
  3. @@ - 27,5 + 28,6 @@
  4. │ └── pack
  5. └── refs
  6. ├── heads
  7. + │ ├── fix-url
  8. │ └── master
  9. └── tags
  10. ...
  11. 此操作会在 `refs /heads` 目录下加入一个新的文件。该文件的名称就是我们新建的分支名,而内容则是最新的提交标识 id。
  12. ```batch
  13. $ cat .git /refs /heads /fix-url
  14. 68ed 5aa 2372445 cf 2249d 85573ade 1c 0cbb 312b 1

这基本上就是创建分支的全部内容。在 git 中,分支是相当轻便的。另外,标签的创建也是类似的操作,但它们是被创建在 refs/tags 目录下。

在 logs 目录下也新增了一个文件,该文件用于记录与 master 分支类似的提交历史信息。

5. 分支切换

在 git 中进行分支切换实际上是让 git 获取某个提交的 tree 对象,并更新工作区中的文件,使其与其中记录的状态相匹配。在此例中,由于我们是从 master 分支切换到 fix-url 分支,而这两个分支都指向同一个 commit 和它的 tree 对象,因此 git 在工作区的文件上并没有任何更改。

git checkout fix-url

   
   

在进行分支切换时,.git 目录中唯一发生的变化是 .git/HEAD 文件的内容,现在它指向 fix-url 分支。


   
   
  1. $ cat .git /HEAD
  2. ref: refs /heads /fix-url

既然我们在这里,我将进行一个提交操作。这将有助于我稍后展示合并的效果。


   
   
  1. $ echo 'https://blog.meain.io' > file
  2. $ git commit -am 'Fix url'

6. 合并操作

有三种主要的合并方法。

  1. 最简单且直观的是快进式合并。这种方式中,你只是更新一个分支的提交,使其指向另一个分支的提交。具体操作就是把 refs/heads/fix-url 中的哈希值复制到 refs/heads/master

  2. 第二种是变基(rebase)合并。在这种方式中,我们首先将更改依次应用到主分支当前的提交上,然后进行类似于快进式的合并。

  3. 第三种是通过创建一个独立的合并来合并两个分支。这种方法与前两者略有不同,因为它的提交对象会有两个 parent 条目。我们稍后会详细探讨这种方法。

首先,我们来看看合并前的 graph。


   
   
  1. git log --graph --oneline -- all
  2. * 42c 6318 (fix-url) Fix url
  3. * 67ed 5aa (HEAD - > master) Use blog link
  4. * 3c 201df Initial commit

接下来进行合并:

$ git merge fix-url # updates refs/heads/master to the hash in refs/heads/fix-url

   
   

我们再来看看合并后的 graph。


   
   
  1. $ git log --graph --oneline -- all
  2. * 42c 6318 (HEAD - > master) (fix-url) Fix url
  3. * 67ed 5aa Use blog link
  4. * 3c 201df Initial commit

7. 执行推送

在我们对本地 git 仓库进行了一系列操纵之后,现在我们来看看进行推送时会发生什么事情。远程 git 仓库会接收哪些数据?

为了演示这个过程,我首先创建了一个新的 git 仓库作为这个仓库的远程连接。


   
   
  1. $ mkdir git-talk- 2
  2. $ cd git-talk- 2 & & git init --bare
  3. $ cd .. /git-talk & & git remote add origin .. /git-talk- 2

另外,添加新的远程仓库实际上是修改了配置文件,你可以在 .git/config 中查看这个变更。具体做了哪些修改,我鼓励你自己去探索。

接下来,执行推送操作。

$ git push origin master

   
   

我们再来检查一下本地仓库发生了哪些改变。


   
   
  1. --- branch 2023- 07- 02 15: 55: 25.165204941 + 0530
  2. + + + remote 2023- 07- 02 17: 41: 05.170923141 + 0530
  3. @@ - 22,12 + 29,18 @@
  4. │ ├── e 5
  5. │ │ └── ec 63cd 761e 6ab 9d 11e 7dc 2c 4c 2752d 682b 36e 2
  6. │ ├── info
  7. │ └── pack
  8. ├── ORIG_HEAD
  9. └── refs
  10. ├── heads
  11. │ ├── fix-url
  12. │ └── master
  13. + ├── remotes
  14. + │ └── origin
  15. + │ └── master
  16. └── tags

你会发现新增了一个新的 refs/remotes 目录,这是用来存储不同远程仓库相关信息的。

但是,实际上传送到远程 git 仓库的数据是什么呢?那就是 objects 文件夹内的所有数据,以及你明确推送的 refs 下的分支和标签。仅凭这些,远程的 git 就能完整地构建出你的所有 git 历史记录。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值