开发者社区> 开发与运维> 正文

.git文件夹探秘,理解git运作机制

简介: 近期需要给 git 仓库制作一个 commit-msg 钩子,进入 .git/hooks 文件夹正准备干活,突然想知道其它 git hooks 都是干啥的?.git 文件夹里面那么多文件,又都是干什么的呢?于是产生了这篇文章。另外,想要 git 进阶,了解 .git 文件夹也是最佳切入点,关于 git 运作机制的线索都可以在这里找到。.git

近期需要给 git 仓库制作一个 commit-msg 钩子,进入 .git/hooks 文件夹正准备干活,突然想知道其它 git hooks 都是干啥的?.git 文件夹里面那么多文件,又都是干什么的呢?于是产生了这篇文章。

另外,想要 git 进阶,了解 .git 文件夹也是最佳切入点,关于 git 运作机制的线索都可以在这里找到。

.git 文件夹创建

任意文件夹中,用 git init 命令初始化仓库,即可在此文件夹下创建 .git 文件夹(.打头为隐藏文件夹,所以平时可能看不到)。这个文件夹之外的部分叫做工作区(Working Directory),.git 文件夹我们称做 Git仓库 (Git Repository)。

如果出于某种原因,想要重新来过,rm -rf .git && git init,此仓库的git记录会归零! (提醒:慎用!!!)

.git 结构

随便初始化一个仓库,git init temp,运行 cd temp && ls -F1 .git,可以看到基本的 .git 目录结构:

HEAD
config
description
hooks/
info/
objects/
refs/

但这里面没有实质性内容,研究意义不大。我们找一个有过几次提交的仓库,运行 ls -F1 .git 可以看到更丰富的 .git 目录结构:

COMMIT_EDITMSG
HEAD
ORIG_HEAD
FETCH_HEAD
config
description
index
hooks/
info/
logs/
objects/
refs/

通常会有7个文件5个目录,分别如下:

重要:动手之前,请做好整个仓库的备份!!!

重要:动手之前,请做好整个仓库的备份!!!

重要:动手之前,请做好整个仓库的备份!!!

1. 文件 COMMIT_EDITMSG

此文件是一个临时文件,存储最后一次提交的信息内容,git commit 命令之后打开的编辑器就是在编辑此文件,而你退出编辑器后,git 会把此文件内容写入 commit 记录。

实际应用: git pull 远程仓库后,新增了很多提交,淹没了本地提交记录,直接 cat .git/COMMIT_EDITMSG 就可以弄清楚最后工作的位置了,是不是很实用?

2. 文件 HEAD

此文件永远存储当前位置指针,就像 linux 中的 $PWD 变量和命令提示符的箭头一样,永远指向当前位置,表明当前的工作位置。在 gitHEAD 永远指向当前正在工作的那个 commit

分支HEAD

HEAD 存储一个分支的 ref,运行:cat .git/HEAD 通常会显示:

ref: refs/heads/master

这说明你目前正在 master 分支工作。此时你的任何 commit,默认自动附加到 master 分支之上。

git cat-file -p HEAD, 显示详细的提交信息:

tree 0551ac8f8238aca193bafea5c1dabe94a3f61909
parent 12459bcc65db0f20e533078b313e8dc6747e4ea3
author jamesyang.yjm <jamesyang.yjm@alibaba-inc.com> 1561509340 +0800
committer jamesyang.yjm <jamesyang.yjm@alibaba-inc.com> 1561509340 +0800

docs: fix typo in README.md
孤立HEAD

HEAD 不关联任何分支,只指向某个 commit,运行 git checkout 12459b,你会看到如下信息:

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

相信很多人一开始使用 git 都会对这段信息头大,其实它只是告诉你 HEAD 这个文件中存储的信息已不再是一个分支信息,运行:cat .git/HEAD,看到:

12459bcc65db0f20e533078b313e8dc6747e4ea3

看到区别了吗?HEAD 指向一个40字符的 SHA-1 提交记录,git 已经不知道你在哪个分支工作了,所以你如果生成新的 commit,git 不知道往哪里 push,你只能做些实验性代码自嗨一把,无法影响到任何分支,也无法与人协同。这就是所谓的 'detached HEAD' state

image.png
(由分支HEAD 变为 孤立HEAD)

关于 HEAD 的用法示例:

git push origin HEAD
git checkout HEAD~1

3. 文件 ORIG_HEAD

正因为 HEAD 比较重要,此文件会在你进行危险操作时备份 HEAD,如以下操作时会触发备份:

git reset
git merge
git rebase
git pull

此文件的应用示例:

# 回滚到上一次的状态(慎用!!!)
git reset --hard ORIG_HEAD

4. 文件 FETCH_HEAD

这个文件作用在于追踪远程分支的拉取与合并,与其相关的命令有 git pull/fetch/merge

git pull 命令相当于执行以下两条命令:

$ git fetch
$ git merge FETCH_HEAD

# 显示如下>>>
From https://github.com/xxx/xxxx
* branch            master     -> FETCH_HEAD
Updating f785638..59db1b2

并且,此时会默默备份 HEADORIG_HEAD

看看 FETCH_HEAD 里面有什么内容:

$ cat .git/FETCH_HEAD
848d7701250d5fee1449c5355158f629f6564484        branch 'master' of https://github.com/xxxx/xxx

最前面是 hash值,最后面是需要 fetch 的分支信息。

此文件可能不止一行,比如:

$ cat .git/FETCH_HEAD
848d7701250d5fee1449c5355158f629f6564484           branch 'master' of https://github.com/xxxx/xxx
81d84ed74fc2b29c73d6ac82d681e5819b4d35d3               branch 'next' of https://github.com/xxxx/xxx
a25f5f1615a479e717a82bc4a10d816a44de6cd1        not-for-merge   branch 'add-i18n' of https://github.com/xxxx/xxx
065c1b268386d533be65f4ae34742b2f1780d589        not-for-merge   branch 'add-sche-catch' of https://github.com/xxxx/xxx

其中会有关键字 not-for-merge,由于 git pull 其实就是 fetch + merge,有这个标志就表明 git pull 时只 fetch,不 merge

此特性在 2015年 git 2.5 之后被加入,可以看看 源码,当 git pull 时,not-for-merge 会做为 magic string 来判定是否要从远程合并到本地分支。上面这段源码的注释写得恰到好处:

"Appends merge candidates from FETCH_HEAD that are not marked not-for-merge into merge_heads."

5. 文件 config

此文件存储项目本地的 git 设置,典型内容如下:

[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
        ignorecase = true
[remote "origin"]
        url = git@gitlab.xxxx.com/xxx.git
        fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
        remote = origin
        merge = refs/heads/master
[branch "v2.6.0"]
        remote = origin
        merge = refs/heads/v2.6.0
[branch "v2.8.0"]
        remote = origin
        merge = refs/heads/v2.8.0

这是典型的 INI 配置文件,每个 section 可包含多个 variable = value,其中 [core] 字段包含各种 git 的参数设置,如 ignorecase = true 表示忽略文件名大小写。

  • [core] 段的内容跟 git config 命令对应

执行以下命令:

git config user.name abc
git config user.email abc@abc.com

会在 config 文件中追加以下内容:

... ...
[user]
        name = abc
        email = abc@abc.com

git config --global 影响的则是全局配置文件 ~/.gitconfig

  • [remote] 段表示远程仓库配置

详见 Git Internals - The Refspec,注意这里的 +* 的含义。

  • [branch] 段表示分支同步设置

假设当前在 master 分支,执行 git pull 若出现以下提示:

There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

   git pull <remote> <branch>

就说明 .git/config 文件缺少对应的 [branch "master"] 字段。

解决方案为:

git branch -u origin/master master

# 或者执行一次 push
git push -u origin master

会出现提示:

Branch 'master' set up to track remote branch 'master' from 'origin'.

其实就是生成以下内容在 .git/config中:

[branch "master"]
        remote = origin
        merge = refs/heads/master

你去手动编辑 .git/config,效果一样。这就是 upstream 的真正含义,即生成 config 中的这段配置。

6. 文件 description

看到文档中有如下一段描述:

The description file is used only by the GitWeb program, so don’t worry about it.

说明这个文件主要用于 GitWeb 的描述,如果我们要启动 GitWeb 可用如下命令:

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

默认会启动 lighttpd 服务并打开浏览器 http://127.0.0.1:1234 (试着改成对外IP并分享给别人?)

以下显示当前的 git 仓库名称以及描述,默认的描述如下:

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

description

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

7. 文件夹 hooks/

存放 git hooks,用于在 git 命令前后做检查或做些自定义动作。运行 ls -F1 .git/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 设置来更好监测文件变化

参考:https://git-scm.com/docs/githooks

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

比如我们要校验每个 commit message 至少要包含两个单词,否则就提示并拒绝提交,将 commit-msg.sample 改为 commit-msg 后,编辑如下:

#!/bin/sh
grep -q '\S\s\+\S' $1 || { echo '提交信息至少为两个单词' && exit 1; }

这样当提交一个 commit 时,会执行 bash 命令: .git/hooks/commit-msg .git/COMMIT_EDITMSG,退出值不为 0,就拒绝提交。

8. 文件夹 info/

此文件夹基本就有两个文件:

  1. 文件 info/exclude 用于排除规则,与 .gitignore 功能类似。
  2. 可能会包含文件 info/refs ,用于跟踪各分支的信息。此文件一般通过命令 git update-server-info 生成,里面的内容:
94e1a0d952f577fe1348d828d145507d3709e11e    refs/heads/master
# object hash                                   # branch reference

这表示 master 分支所指向的文件对象 hash 值为:94e1a0d952f577fe1348d828d145507d3709e11e

运行 git cat-file -p 94e1a0d952f577fe1348d828d145507d3709e11e,可以看到 master 分支最后提交的记录信息。

同时:cat .git/objects/94/e1a0d952f577fe1348d828d145507d3709e11e 可以看到最后提交文件的二进制内容表示。

文件 info/refs 对于 搭建 git 服务器 来说至关重要。

9. 文件夹 logs/

记录了操作信息,git reflog 命令以及像 HEAD@{1} 形式的路径会用到。如果删除此文件夹(危险!),那么依赖于 reflog 的命令就会报错。

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

报错信息如下:

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

10. 文件夹 objects/

此文件夹简单说,就是 git的数据库,运行 tree .git/objects,可以看到目录结构:

.git/objects/
|-- 0c
|   `-- d370696b581c38ee01e62b148a759f80facc2d
|-- 59
|   `-- 3d5b490556791212acd5a516a37bbfa05d44dd
|-- 61
|   `-- be44eedde61d723e5761577a2b420ba0fc2794
|-- 64
|   `-- c0aed8ddcbb546bdcec2848938fc82348db227
|-- d4
|   `-- 9904676ce8ddde276bdbfa9bbec313e90e0f50
|-- info
`-- pack
    |-- pack-75e3f2aa378752ec93a8e9f375f01204d498605b.idx
    `-- pack-75e3f2aa378752ec93a8e9f375f01204d498605b.pack

这些文件分两种形式:pack压缩包 形式放在 pack/ 目录下,除此之外都是 hash文件 形式,被叫做 loost objects

这个文件夹以及相应的算法,我没找到独立的名称,就叫它 hash-object 体系吧。因为确实有个 git hash-object 命令存在,是一个底层的负责生成这些 loost objects 文件,如果要看到这些文件各自的含义,执行以下命令:

git cat-file --batch-check --batch-all-objects

可以看到

04c87c65f142f33945f2f5951cf7801a32dfa240 commit 194
098217953a6ca169bed33d2be8a07d584fcdaf30 tree 31
0cd370696b581c38ee01e62b148a759f80facc2d commit 245
2a810017bfc85d7db2627f4aabdaa1583212bda3 blob 19
3920a07c1d5694df6b8658592b0939241d70e9e5 tree 93
593d5b490556791212acd5a516a37bbfa05d44dd tag 148
61be44eedde61d723e5761577a2b420ba0fc2794 tree 154
... ...

但你会发现这个列表里有些值在文件夹中并不存在,因为除了 loost objects 它还汇总了 pack 文件中的内容。

hash文件

又称为 loose object,文件名称共由40字符的 SHA-1 hash 值组成,其中前两个字符为文件夹分桶,后38个字符为文件名称。

按文件内容可分为四种类型:commit, tree, blob, tag,若执行以下命令会生成所有四种类型:

echo -en 'xx\n' > xx  # 共 3 个字符
git add .
git commit -m 'update xx'
git tag -a 'v1.0' -m 'release: 1.0.0'

经过以上操作后,对比一下文件树,发现多了四个 hash文件

|-- 0c
|   `-- d370696b581c38ee01e62b148a759f80facc2d
|-- 18
|   `-- 143661f96845f11e0b4ab7312bdc0f356834ce
|-- 30
|   `-- 20feea86d222d83218eb3eb5aa9f58f73df04d
|-- 59
|   `-- 3d5b490556791212acd5a516a37bbfa05d44dd
|-- 61
|   `-- be44eedde61d723e5761577a2b420ba0fc2794
|-- 64
|   `-- c0aed8ddcbb546bdcec2848938fc82348db227
|-- ad
|   `-- f4c9afac7afae3ff3e95e6c4eefe009d547f00
|-- cc
|   `-- c9bd67dc5c467859102d53d54c5ce851273bdd
|-- d4
|   `-- 9904676ce8ddde276bdbfa9bbec313e90e0f50
|-- info
`-- pack
    |-- pack-75e3f2aa378752ec93a8e9f375f01204d498605b.idx
    `-- pack-75e3f2aa378752ec93a8e9f375f01204d498605b.pack

这四个 hash文件 分别是:

cc/c9bd67dc5c467859102d53d54c5ce851273bdd  # blob
30/20feea86d222d83218eb3eb5aa9f58f73df04d  # commit
ad/f4c9afac7afae3ff3e95e6c4eefe009d547f00  # tree
18/143661f96845f11e0b4ab7312bdc0f356834ce  # tag

我们想看下里面到底存的什么?其实这些文件都经过了压缩,压缩形式为 zlib。先安装一下解压工具 macOS 版 brew install pigz 或 windows 版 pigz,后执行:

$ pigz -d < .git/objects/cc/c9bd67dc5c467859102d53d54c5ce851273bdd

# BLOB类型,显示结果为>>>>(注意xx后有个\n)
blob 3xx
$pigz -d < .git/objects/30/20feea86d222d83218eb3eb5aa9f58f73df04d

# COMMIT类型,显示结果为>>>>
commit 248tree adf4c9afac7afae3ff3e95e6c4eefe009d547f00
parent 0cd370696b581c38ee01e62b148a759f80facc2d
author jamesyang.yjm <jamesyang.yjm@alibaba-inc.com> 1562044880 +0800
committer jamesyang.yjm <jamesyang.yjm@alibaba-inc.com> 1562044880 +0800

update xx
$ pigz -d < .git/objects/ad/f4c9afac7afae3ff3e95e6c4eefe009d547f00

# TREE类型,显示结果为>>>>
tree 154100644 abc*???]}?bJ?ڡX2??100644 asdf???CK?)?wZ???S?100644 iou???CK?)?wZ???S?100644 xx?ɽg?\FxY-S?L\?Q';?100644 yy???CK?)?wZ???S?
$ pigz -d < .git/objects/18/143661f96845f11e0b4ab7312bdc0f356834ce

# TAG类型,显示结果为>>>>
tag 155object 3020feea86d222d83218eb3eb5aa9f58f73df04d
type commit
tag v1.0
tagger jamesyang.yjm <jamesyang.yjm@alibaba-inc.com> 1562045942 +0800

release: 1.0.0

会发现,显示结果都是 type size+内容 形式,这就是 object 文件的存储格式:

[type] [size][NULL][content]

type 可选值:commit, tree, blob, tag,NULL 就是C语言里的字符结束符:\0size 就是 NULL后内容的字节长度。

type 的几种类型可以使用 git cat-file -t hash 看到,内容可以用 git cat-file -p hash 看到。

git cat-file -t ccc9bd67dc5c467859102d53d54c5ce851273bdd

# 显示结果为>>>>
blob
git cat-file -p ccc9bd67dc5c467859102d53d54c5ce851273bdd

# 显示结果为>>>>
xx

所以 blob 文件就是对原文件内容的全量拷贝,同时前面加了 blob size\0,而文件名称的 hash 值计算是计算整体字符的 SHA-1 值:

echo -en 'blob 3\0xx\n' | shasum
# 显示结果为>>>>
ccc9bd67dc5c467859102d53d54c5ce851273bdd  -

知道原理后,其它类型格式请自行参考 斯坦福 Ben Lynn 所著的 GitMagic

所以,当我们 git show 3020feea86d222d83218eb3eb5aa9f58f73df04d 时,会发生些什么?

  1. 找到 3020feea86d222d83218eb3eb5aa9f58f73df04d 这个 commit,显示出来
  2. 找到此 commit 关联的 tree object: adf4c9afac7afae3ff3e95e6c4eefe009d547f00,拉取相应的 blob 文件,并与当前工作区内的文件做 diff,然后显示出来

这就是 objects/ 文件夹作为 git数据库 被使用的真实例子。

pack文件

为什么会有 .pack 文件?

由于每次 commit 都会生成许多 hash文件,并且由于 blob 文件都是全量存储的,导致 git 效率下降,于是有了 pack-format,优势:

  1. 对于大仓库存储效率高
  2. 利于网络传输,便于备份
  3. 增量存储,优化磁盘空间

.git/objects 下的部分文件打包成 pack格式

$ tree .git/objects/ | wc -l
311

$ git gc
Enumerating objects: 288, done.
Counting objects: 100% (288/288), done.
Delta compression using up to 4 threads
Compressing objects: 100% (287/287), done.
Writing objects: 100% (288/288), done.
Total 288 (delta 131), reused 90 (delta 0)

$ tree .git/objects/ | wc -l
12

可以看到文件数量减小了不少,其中大部分文件被打到一个 .pack 包中,并且是增量存储,有部分变更的文件只存储 基础hash + 变更内容,磁盘空间优化很明显。

git gc 其实运行了两条命令:git repack 用来打包 和 git prune-packed 用来移除已打包的 hash文件

  • 如果你想打包所有文件,并不推荐,但可以用以下命令:
$ git repack -a -d -f --depth=250 --window=250

具体可见:此问题

  • 如果想看一下包里有啥,运行:
git verify-pack -v .git/objects/pack/pack-5963b552193021791c1a0ab9136c272f07124c98.pack

显示如下:

5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 commit 245 153 12
2305588a632214f266462260428c4395f936b5b0 commit 252 156 165
1fa9735670eb952b6468d17b418525717c8e3527 commit 248 156 321
3ffb7fb9830e232669c95b3b65f0f8f3fc7a6027 commit 248 155 477
86a5912f97d7dd8f90a28cab6bffc8ee78997e2c commit 244 151 632
94e1a0d952f577fe1348d828d145507d3709e11e commit 249 156 783
86903f8f5024485afa8480020a04cc00f228d23c commit 243 150 939
6efdffad4fb725aa8d0f4d7d29feb5aee7ea5dff commit 242 151 1089
04c87c65f142f33945f2f5951cf7801a32dfa240 commit 73 85 1240 1 6efdffad4fb725aa8d0f4d7d29feb5aee7ea5dff
2a810017bfc85d7db2627f4aabdaa1583212bda3 blob   19 27 1325
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 blob   0 9 1352
b5e810691433cf8a2960c27c1b33546fa96e2bef blob   16 26 1361
2f36e957afc2b3bcda988cb29a86e3a1490e8cc2 tree   153 106 1387
2ed6130bd33afa26817418308e29c4081ea056ec tree   5 15 1493 1 2f36e957afc2b3bcda988cb29a86e3a1490e8cc2
9df301ad27294a62ba1ae65aaed489072d778c79 tree   123 103 1508
7d48a14b9ca1dca2f6a593eef19633ce45f81bee blob   12 21 1611
a448b4d6450de854dcc6fe658bdb72e22c726cbb tree   123 102 1632
9e56fd51f52d8b9d242c50c24a4cae586d76ec7e blob   7 16 1734
bde15b851f135327ada02c9deac0fb1ee01cf343 tree   123 102 1750
58c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c blob   4 13 1852
3920a07c1d5694df6b8658592b0939241d70e9e5 tree   7 17 1865 1 bde15b851f135327ada02c9deac0fb1ee01cf343
16729e3b94f19bc95cb6f563f776bfb4694a6e5b tree   4 14 1882 2 3920a07c1d5694df6b8658592b0939241d70e9e5
b72c74792528892694c395b2c9a3d6af740f3fb2 tree   63 50 1896
098217953a6ca169bed33d2be8a07d584fcdaf30 tree   31 42 1946
non delta: 20 objects
chain length = 1: 3 objects
chain length = 2: 1 object
.git/objects/pack/pack-5963b552193021791c1a0ab9136c272f07124c98.pack: ok

后面那串数字说明文档里很详细:

When specifying the -v option the format used is:

        SHA-1 type size size-in-packfile offset-in-packfile

for objects that are not deltified in the pack, and

        SHA-1 type size size-in-packfile offset-in-packfile depth base-SHA-1

for objects that are deltified.

以上最后有 hash 的条目,说明是增量存储的 基础hash,其前是增量深度。

11. 文件夹 refs/

refs 可以理解成文件系统中的 symbol link,看下结构:

$ tree .git/refs/

.git/refs
|-- heads
|   `-- master
`-- tags
    `-- v1.0

$ cat .git/refs/heads/master 
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5

$ cat .git/refs/tags/v1.0    
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5

$ git cat-file -t 5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5
commit

可以看到 masterv1.0 都指向 5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 这个 commit

refs/heads/ 文件夹内的 ref 一般通过 git branch 生成。git show-ref --heads 可以查看。

refs/tags/ 文件夹内的 ref 一般通过 git tag 生成。git show-ref --tags 可以查看。

如下:

$ git branch abc

$ tree .git/refs/

.git/refs/
|-- heads
|   |-- abc
|   `-- master
`-- tags
    `-- v1.0

$ cat .git/refs/heads/abc 
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5

说明新建分支其实就是生成了一个指向某个 commitsymbol link,当然在这里叫做 ref

git tag 命令本质与 git branch 相同,只生成一个 ref 放在 tags 目录下,所以被称为 lightweight tag

git tag -a xx 命令会首先生成一个类型为 taghash文件 放到 objects/ 目录,然后生成 ref 放到 tags 目录下指向那个文件。这就叫做 annotated tag,好处是可包含一些元信息如 taggermessage,被 git 的 hash-object 算法管理,可被 GPG 签名等,所以更稳定,更安全

使用以下命令来拿到 refs 文件夹存储的信息:

$ git show-ref --head --dereference
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 HEAD
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/heads/abc
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/heads/master
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/tags/v1.0
5e84371048faa20412f5492e6af264a7e1edfec1 refs/tags/xx
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/tags/xx^{}

我们来看这些信息如何变化的:

$ touch new_file && git add . && git commit -m 'add new_file'
[master 44b0d05] add new_file
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 new_file

$ git show-ref --head --dereference
44b0d05ddadaaa8d2cc40d6647cc474b26f5d8d3 HEAD
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/heads/abc
44b0d05ddadaaa8d2cc40d6647cc474b26f5d8d3 refs/heads/master
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/tags/v1.0
5e84371048faa20412f5492e6af264a7e1edfec1 refs/tags/xx
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/tags/xx^{}

diff 一下可以看到:

5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 HEAD
5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 refs/heads/master

这两行发生了变化。也就是每次 commit 时,HEAD 与 heads 都会自动更新。

12. 文件 index

细心的读者发现,没有讲 index 文件?

原因在于,index 文件是整个 git 除 hash-object 体系最核心的部分,值得用单独一篇来讲。

可以先参考以下文章:

阮一峰老师写的 Git 原理入门暂存区 的部分

Git index format

Use of index and Racy Git problem

简要说一下,index 是一个微型的 linux文件系统,用最经济的方式实现了 inode,这并不是偶然,因为创造这个想法的人同时也是 linux 的创造者 Linus Torvalds

这个文件也叫做 git 的暂存区(Staging Area),git add 就是把工作区内的某些文件取部分 stat 抓取的内容并写入 .git/index 文件并存为相应的一条 index entry,多条 index entry 形成一个 tree

git commit 是把上一步形成的 tree 结构及相应的 blob 存储到 objects/ 文件夹下并同时生成一条 commit 记录。

git reset 是将刚写入 index 文件的 tree 丢弃,并从 HEAD 中恢复一个 tree

git status 是拿 index 文件中存储的 tree 与工作区内的文件在 stat 层面做对比,并输出变更。

以上,这几个文件夹咱们用一张图做总结:

image.png

参考资料:

https://git-scm.com/docs/githooks

https://git-scm.com/book/en/v2/Git-Internals-The-Refspec

https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain

http://www-cs-students.stanford.edu/~blynn/gitmagic/ch08.html#_the_object_database

https://git-scm.com/docs/git-show-ref

https://stackoverflow.com/questions/28720151/git-gc-aggressive-vs-git-repack

https://git-scm.com/docs/git-repack

版权声明:如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:developerteam@list.alibaba-inc.com 进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

集结各类场景实战经验,助你开发运维畅行无忧

其他文章