解析.git文件夹,深入了解git内部原理

2022-02-23

Copied from 解析.git文件夹,深入了解git内部原理. 侵删.

先从初始化项目入手,执行完git init git-demo(项目名),进入git-demo:

$ git init git-demo
$ cd git-demo/.git

$ ls -l
total 7
-rw-r--r-- 1 chenweihuan 1049089 130 11月  4 08:10 config
-rw-r--r-- 1 chenweihuan 1049089  73 11月  4 08:10 description
-rw-r--r-- 1 chenweihuan 1049089  23 11月  4 08:10 HEAD
drwxr-xr-x 1 chenweihuan 1049089   0 11月  4 08:10 hooks/
drwxr-xr-x 1 chenweihuan 1049089   0 11月  4 08:10 info/
drwxr-xr-x 1 chenweihuan 1049089   0 11月  4 08:10 objects/
drwxr-xr-x 1 chenweihuan 1049089   0 11月  4 08:10 refs/

1. config(配置)

config 文件顾名思义就是包含项目特有的配置选项。默认配置如下:

$ cat config
[core]
	repositoryformatversion = 0
	# 视文件权限的修改是否为差异。如果为true,git把文件权限也算作文件差异的一部分
	filemode = false
	# 裸仓库。在git init初始化的时候我们可以加上--bare参数来决定是否创建一个裸仓库。在普通的git存储库中,存储库中有一个.git文件夹,其中包含所有相关数据和所有其他文件,用于构建工作副本。在一个裸仓库中,没有.git文件夹存储工作副本,会把init初始化的7个文件或文件夹直接放在项目根目录上
	bare = false
	logallrefupdates = true
	symlinks = false
	# 忽略文件的大小写,如果ignorecase为true,当文件readme.md改为Readme.md,git会忽略这个改动
	ignorecase = true

我们用的比较多的是用户配置。例如提交commit需要的用户名和邮箱,使用–local设置当前提交的用户名:

$ git config --local user.name chenweihuan1

$ cat config
[core]
	repositoryformatversion = 0
	filemode = false
	bare = false
	logallrefupdates = true
	symlinks = false
	ignorecase = true
[user]
	name = chenweihuan1

直接修改 config 文件和使用命令行修改配置效果一样。

2. description(描述)

这个文件用于GitWeb。GitWeb 是 CGI 脚本(Common Gateway Interface,通用网关接口,简单地讲就是运行在Web服务器上的程序, 但由浏览器的输入触发),让用户在web页面查看git内容。如果我们要启动 GitWeb 可用如下命令:

# 确保lighttpd已安装: brew install lighttpd
$ git instaweb --start

默认会启动 lighttpd 服务并打开浏览器 http://127.0.0.1:1234,页面直接显示当前的git 仓库名称以及描述,默认的描述如下:

Unnamed repository; edit this file 'description' to name the repository.

上面这段话就是默认的 description 文件的内容,编辑这个文件会让你 GitWeb 描述更友好。除此之外没发现其它用处。

3. hooks/(钩子)

hooks里存放 git 提交的各个阶段文件,用于在 git 命令前后做检查或做些自定义动作。

# -F1:在列出的文件名称后加一符号;例如可执行档则加 "*", 目录则加 "/",1代表一个文件占据一行。
$ ls -F1 hooks
prepare-commit-msg.sample*  # git commit 之前,编辑器启动之前触发,传入 COMMIT_FILE,COMMIT_SOURCE,SHA1
commit-msg.sample*          # git commit 之前,编辑器退出后触发,传入 COMMIT_EDITMSG 文件名
pre-commit.sample*          # git commit 之前,commit-msg 通过后触发,譬如校验文件名是否含中文
pre-push.sample*            # git push 之前触发

pre-receive.sample*         # git push 之后,服务端更新 ref 前触发
update.sample*              # git push 之后,服务端更新每一个 ref 时触发,用于针对每个 ref 作校验等
post-update.sample*         # git push 之后,服务端更新 ref 后触发

pre-rebase.sample*          # git rebase 之前触发,传入 rebase 分支作参数
applypatch-msg.sample*      # 用于 git am 命令提交信息校验
pre-applypatch.sample*      # 用于 git am 命令执行前动作
fsmonitor-watchman.sample*  # 配合 core.fsmonitor 设置来更好监测文件变化

如果要启用某个 hook,只需把 .sample(样本) 删除即可,然后编辑其内容来实现相应的逻辑。

例如团队规定每次commit都有一个固定的格式,“[姓名]+具体信息”的格式,如果不使用hooks,每次都需要重复去写上”[姓名]”,但利用Git的hooks功能处理每一条commit信息,在每条信息前自动添加”[姓名]”。那么具体的步骤就是在项目目录下找到.git/hooks/commit-msg.sample文件,将该文件重命名为commit-msg,修改该文件的内容为:

name=[`git config user.name`]
commit=${name}$(cat $1)
echo "${commit}" > "$1"

这样的话,在commit时,只需要git commit -m'message',hooks会把”message”修改为”[姓名]msssage”。

现在比较流行使用husky来做hook,husky可以让我们在package.json里配置git hooks,使git hooks的使用变得更简单方便。运行npm install husky,它会在我们项目根目录下面的.git/hooks文件夹下面创建pre-commit、pre-push等hooks,只需配置json即可:

{
    ...
    "husky": {
        "hooks": {
          "pre-commit": "npm test"
        }
      }
}

4. info/

(1)info/exclude,初始化时只有这个文件,用于排除提交规则,与 .gitignore 功能类似。他们的区别在于.gitignore 这个文件本身会提交到版本库中去,用来保存的是公共需要排除的文件;而info/exclude 这里设置的则是你自己本地需要排除的文件,他不会影响到其他人,也不会提交到版本库中去。 (2)info/refs,如果新建了分支后,还会有info/refs文件 ,用于跟踪各分支的信息。此文件一般通过命令 git update-server-info 生成,里面的内容:

$ git update-server-info

$ cat info/refs
a0ce8c3e3f8cfbc8f2e88ffd49ce45941f7d7af3	refs/heads/master

当你创建了多个分支并更新,会把新分支的信息添加上去。

$ git checkout -b 'feature/1'
$ git update-server-info

info/refs的内容就变成:

$ cat info/refs
a0ce8c3e3f8cfbc8f2e88ffd49ce45941f7d7af3	refs/heads/feature/1
a0ce8c3e3f8cfbc8f2e88ffd49ce45941f7d7af3	refs/heads/master

info/refs其中一个作用就是用于git clone过程。执行git clone...后,它做的第一件事就是获取 info/refs 文件,这样就知道远程仓库的所有分支和引用信息。这个文件是在服务端运行了 update-server-info 所生成的。

5. HEAD

此文件永远存储当前位置指针,指向当前工作区的分支。就像 linux 中的 $PWD 变量(指向当前目录)一样,永远指向当前位置,表明当前的工作位置。当我们在master分支时,HEAD文件的内容为:

$ git branch
* master

$ cat HEAD
ref: refs/heads/master

6. index(暂存区)

index也称为stage,是一个索引文件。当执行git add后,文件就会存入Git的对象objects里,并使用索引进行定位。所以,只要执行了git add,就算工作目录中的文件被误删除,也不会引起文件的丢失;创建了一个提交(commit), 那么提交的是当前索引(index)里的内容, 而不是工作目录中的内容。

.git/index是一个二进制文件,无法用编辑器直接打开查看内容。我们可以通过git ls-files --stage命令看到仓库中每一个文件及其所对应的文件对象。

$ git add .

$ git ls-files --stage
100644 6dc700c37fb6af03239b8ea6f1d58db1a8819464 0       .vscode/settings.json

7. objects/

在初始化的时候,objects里有两个空的文件夹:info和pack,后面具体介绍。 在执行了git add之后,文件就已经存入objects里。

$ ls -l
total 0
drwxr-xr-x 1 chenweihuan 1049089 0 11月  4 09:06 6d
drwxr-xr-x 1 chenweihuan 1049089 0 11月  4 08:10 info
drwxr-xr-x 1 chenweihuan 1049089 0 11月  4 08:10 pack

$ cd 6d

$ ls -l
total 1
-r--r--r-- 1 chenweihuan 1049089 59 11月  4 09:06 c700c37fb6af03239b8ea6f1d58db1a8819464

# 文件名 + hash
$ git cat-file -t 6dc700c37fb6af03239b8ea6f1d58db1a8819464
blob

$ git cat-file -p 6dc700c37fb6af03239b8ea6f1d58db1a8819464
{
  "files.exclude": {
    "**/.git": false
  }
}

到这里,我们可以清晰的知道我们编写的文件代码具体存储在哪里了。 下面看一下pack和info文件夹。Git 往磁盘保存对象时默认使用的格式叫松散对象 (loose object) 格式,当你对同一个文件修改哪怕一行,git 都会使用全新的文件存储这个修改了的文件,放在了objects中。Git 时不时地将这些对象打包至一个叫 packfile 的二进制文件以节省空间并提高效率,当版本库中有太多的松散对象,或者你手动执行 git gc 命令,或者你向远程服务器执行推送时,Git 都会这样做。

$ find .git/objects -type f
.git/objects/6d/c700c37fb6af03239b8ea6f1d58db1a8819464

$ git gc
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), done.
Total 1 (delta 0), reused 0 (delta 0)

$ find .git/objects -type f
.git/objects/info/packs
.git/objects/pack/pack-d9059144205ae43ab3472bebfd7976a8f52de3c2.idx
.git/objects/pack/pack-d9059144205ae43ab3472bebfd7976a8f52de3c2.pack

$ cat .git/objects/info/packs
P pack-d9059144205ae43ab3472bebfd7976a8f52de3c2.pack

这里只有6d一个文件夹,已经成功打包到pack里了,即使有很多很多文件对象,执行 git gc 后都会全部打包到 pack 里。.pack 存储对象文件,.idx 是索引文件,用于允许它们被随机访问;info 文件夹记录对象存储的附加信息,这里存储着打包后的文件名。

8. COMMIT_EDITMSG(commit-editmsg,最后一次commit的message)

执行commit,把代码提交到本地仓库:

$ git commit -m 'add setting.json'
[master (root-commit) 4a51f60] add setting.json
 1 file changed, 5 insertions(+)
 create mode 100644 .vscode/settings.json

多了两个文件或文件夹:COMMIT-EDITMSG、logs/。 COMMIT-EDITMSG是一个临时文件,存储最后一次提交的message,当敲入git commit命令,不加-m的话, 会打开编辑器,其实就是在编辑此文件,而你退出编辑器后,git 会把此文件内容写入 commit 记录。 而执行git commit -m 'message'时,mssage就是COMMIT_EDITMSG的文件内容。

$ cat .git/COMMIT_EDITMSG
add setting.json

该文件的一个应用场景:当你git pull 远程仓库后,新增了很多提交,淹没了本地提交记录,直接 cat .git/COMMIT_EDITMSG 就可以弄清楚自己最后工作的位置了。

9. refs/(引用)

refs文件夹存储着分支和标签的引用。下面创建一个分支feature/1,还有给master分支打了一个tag为“v1.0”,那么 现在有两个分支,分别是master和feature/1:

$ git checkout -b feature/1
Switched to a new branch 'feature/1'

$ git checkout master
Switched to branch 'master'

$ git tag v1.0

那么refs目录的项目结构为:

|-- heads
|   |-- master
|   |-- feature
|      |-- 1
|-- tags
    |-- v1.0
复制代码
$ cat .git/refs/heads/master
4a51f60e47ca4c0878ff0fb1524d4413fd7ac459

$ git cat-file -t 4a51f60e47ca4c
commit

$ git branch -v
  feature/1 4a51f60 add setting.json
* master    4a51f60 add setting.json

$ cat .git/refs/tags/v1.0
4a51f60e47ca4c0878ff0fb1524d4413fd7ac459

可以看到 master 和 v1.0 都指向 4a51f60e47ca4c0878ff0fb1524d4413fd7ac459 这个 commit。 refs/heads/ 文件夹内的 ref 一般通过 git branch 生成。git show-ref --heads 可以查看; refs/tags/ 文件夹内的 ref 一般通过 git tag 生成git show-ref --tags 可以查看。

10. logs/

logs就是用来记录操作信息的,git reflog 命令以及像 HEAD@{1} 形式的路径会用到。经过上面例子的操作后,它的目录如下:

| -- refs
|   -- heads
|       -- master
|       -- feature
|           -- 1
| -- HEAD

HEAD直接记录在所有分支上的操作:

$ cat .git/logs/HEAD
... chenweihuan <chenweihuan@....com> 1572834991 +0800        commit (initial): add setting.json
... chenweihuan <chenweihuan@....com> 1572835207 +0800        checkout: moving from master to feature/1
... chenweihuan <chenweihuan@....com> 1572835305 +0800        checkout: moving from feature/1 to master

refs/heads里还有master和feature/1文件,记录各自分支的操作记录:

$ cat .git/logs/refs/heads/master
... chenweihuan <chenweihuan@....com> 1572834991 +0800        commit (initial): add setting.json

$ cat .git/logs/refs/heads/feature/1
... chenweihuan <chenweihuan@....com> 1572835207 +0800        branch: Created from HEAD

下面是添加一个readme.md文件后进行reset操作,然后又撤销reset操作:

$ git log
commit bdbf62cd18dfd1a9ef1733ec2e157c151c35a1af (HEAD -> master)
Author: chenweihuan <chenweihuan@....com>
Date:   Mon Nov 4 11:03:14 2019 +0800
    add readme.md

commit 4a51f60e47ca4c0878ff0fb1524d4413fd7ac459 (tag: v1.0, feature/1)
Author: chenweihuan <chenweihuan@....com>
Date:   Mon Nov 4 10:36:31 2019 +0800
    add setting.json
    
$ git reset --hard HEAD~1
HEAD is now at 4a51f60 add setting.json

$ git log
commit 4a51f60e47ca4c0878ff0fb1524d4413fd7ac459 (HEAD -> master, tag: v1.0, feature/1)
Author: chenweihuan <chenweihuan@....com>
Date:   Mon Nov 4 10:36:31 2019 +0800
    add setting.json
    
$ git reflog
# 这里能打印出所有操作,包括reset操作,其实就是从logs文件夹获取的。
4a51f60 (HEAD -> master, tag: v1.0, feature/1) HEAD@{0}: reset: moving to HEAD~1
bdbf62c HEAD@{1}: commit: add readme.md
4a51f60 (HEAD -> master, tag: v1.0, feature/1) HEAD@{2}: reset: moving to HEAD
4a51f60 (HEAD -> master, tag: v1.0, feature/1) HEAD@{3}: checkout: moving from feature/1 to master
4a51f60 (HEAD -> master, tag: v1.0, feature/1) HEAD@{4}: checkout: moving from master to feature/1
4a51f60 (HEAD -> master, tag: v1.0, feature/1) HEAD@{5}: commit (initial): add setting.json

$ git reset --hard bdbf62c
HEAD is now at bdbf62c add readme.md

$ git log
commit bdbf62cd18dfd1a9ef1733ec2e157c151c35a1af (HEAD -> master)
Author: chenweihuan <chenweihuan@....com>
Date:   Mon Nov 4 11:03:14 2019 +0800
    add readme.md

commit 4a51f60e47ca4c0878ff0fb1524d4413fd7ac459 (tag: v1.0, feature/1)
Author: chenweihuan <chenweihuan@....com>
Date:   Mon Nov 4 10:36:31 2019 +0800
    add setting.json

如果删除此文件夹(危险!),那么依赖于 reflog 的命令就会报错。

mv .git/logs .git/logs_bak
git checkout HEAD@{1}

报错信息如下:

error: pathspec ‘HEAD@{1}’ did not match any file(s) known to git

最后总结一下

└── .git
    ├── COMMIT_EDITMSG    # 保存最新的commit message
    ├── config    # 仓库的配置文件
    ├── description    # 仓库的描述信息,主要给gitweb使用
    ├── HEAD    # 指向当前分支
    ├── hooks    # 存放一些shell脚本,可以设置特定的git命令后触发相应的脚本
    ├── index    # 二进制暂存区(stage)
    ├── info    # 仓库的其他信息
    │   └── exclude # 本地的排除文件规则,功能和.gitignore类似
    ├── logs    # 保存所有更新操作的引用记录,主要用于git reflog等
    ├── objects    # 所有文件的存储对象
    └── refs    # 具体的引用,主要存储分支和标签的引用

当然除了上面说到的只是列举了一部分,执行一些特定的命令时,还会有别的文件出现。例如:

  1. ORIG_HEAD(此文件会在你进行危险操作时备份 HEAD,如git reset、git merge、git rebase、git pull
  2. FETCH_HEAD(这个文件作用在于追踪远程分支的拉取与合并,与其相关的命令有 git pull/fetch/merge)

… 如果你觉得有什么不对或疑惑的地方,欢迎纠正或提出问题,感谢!!

参考:

.git文件夹探秘,理解git运作机制 git原理入门 深入Git索引(一):索引文件结构篇 Git Pro