近期需要给 git 仓库制作一个 commit-msg
钩子,进入 .git/hooks
文件夹正准备干活,突然想知道其它 git hooks 都是干啥的?.git
文件夹里面那么多文件,又都是干什么的呢?
前言: 近期需要给 git 仓库制作一个
commit-msg
钩子,进入.git/hooks
文件夹正准备干活,突然想知道其它 git hooks 都是干啥的?.git
文件夹里面那么多文件,又都是干什么的呢?于是写了这篇文章。另外,想要
git
进阶,了解.git
文件夹也是最佳切入点,关于git
运作机制的线索都可以在这里找到。
1. .git
文件夹创建
任意文件夹中,用 git init
命令初始化仓库,即可在此文件夹下创建 .git
文件夹(.
打头为隐藏文件夹,所以平时可能看不到)。这个文件夹之外的部分叫做工作区(Working Directory),.git
文件夹我们称做 Git 仓库 (Git Repository)。
如果出于某种原因,想要重新来过,rm -rf .git && git init
,此仓库的 git 记录会归零!(提醒:慎用!!!)
2. .git
结构
随便初始化一个仓库,git init temp
,运行 cd temp && ls -F1 .git
,可以看到基本的 .git
目录结构:
1 | HEAD |
但这里面没有实质性内容,研究意义不大。我们找一个有过几次提交的仓库,运行 ls -F1 .git
可以看到更丰富的 .git
目录结构(通常会有 7 个文件 5 个目录):
1 | COMMIT_EDITMSG |
重要:动手之前,请做好整个仓库的备份!!!
重要:动手之前,请做好整个仓库的备份!!!
重要:动手之前,请做好整个仓库的备份!!!
2.1. 文件 COMMIT_EDITMSG
此文件是一个临时文件,存储最后一次提交的信息内容,git commit
命令之后打开的编辑器就是在编辑此文件,而你退出编辑器后,git
会把此文件内容写入 commit 记录。
⭐实际应用: git pull
远程仓库后,新增了很多提交,淹没了本地提交记录,直接 cat .git/COMMIT_EDITMSG
就可以弄清楚最后工作的位置了,是不是很实用?
2.2 文件 HEAD
此文件永远存储当前位置指针,就像 linux 中的 $PWD
变量和命令提示符的箭头一样,永远指向当前位置,表明当前的工作位置。在 git
中 HEAD
永远指向当前正在工作的那个 commit
。
分支 HEAD
HEAD 存储一个分支的 ref,运行:cat .git/HEAD
通常会显示:
1 | ref: refs/heads/master |
这说明你目前正在 master
分支工作。此时你的任何 commit,默认自动附加到 master
分支之上。
执行 git cat-file -p HEAD
, 显示详细的提交信息:
1 | git cat-file -p HEAD |
孤立 HEAD
HEAD 不关联任何分支,只指向某个 commit,运行 git checkout bfdec30d
,你会看到如下信息:
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
,看到:
1 | cat .git/HEAD |
看到区别了吗?HEAD 指向一个40字符的 SHA-1 提交记录,git 已经不知道你在哪个分支工作了,所以你如果生成新的 commit,git 不知道往哪里 push
,你只能做些实验性代码自嗨一把,无法影响到任何分支,也无法与人协同。这就是所谓的 **'detached HEAD' state
**。
(由分支HEAD 变为 孤立HEAD)
关于 HEAD
的用法示例:
1 | git push origin HEAD |
2.3 文件 ORIG_HEAD
正因为 HEAD
比较重要,此文件会在你进行危险操作时备份 HEAD
,如以下操作时会触发备份:
1 | git reset |
此文件的应用示例:
1 | 回滚到上一次的状态(慎用!!!) |
2.4 文件 FETCH_HEAD
这个文件作用在于追踪远程分支的拉取与合并,与其相关的命令有 git pull/fetch/merge
,
而 git pull
命令相当于执行以下两条命令 (pull
= fetch
& merge
):
1 | git fetch |
**并且,此时会默默备份 HEAD
到 ORIG_HEAD
**。
看看 FETCH_HEAD
里面有什么内容:
1 | cat .git/FETCH_HEAD |
最前面是 hash 值,最后面是需要 fetch
的分支信息。
此文件可能不止一行,比如:
1 | cat .git/FETCH_HEAD |
⭐其中会有关键字 **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.”
2.5 文件 config
此文件存储项目本地的 git 设置,典型内容如下:
1 | [core] |
这是典型的 INI
配置文件 ,每个 section
可包含多个 variable = value
,其中 [core]
字段包含各种 git 的参数设置,如 ignorecase = true
表示忽略文件名大小写。
⭐git config --global
影响的则是全局配置文件 ~/.gitconfig
;可执行以下命令对该文件进行修改:
1 | git config --global -e |
[core]
段的内容跟git config
命令对应
执行以下命令:
1 | git config user.name abc |
会在 config
文件中追加以下内容:
1 | ... ... |
[remote]
段表示远程仓库配置
详见 Git Internals - The Refspec ,注意这里的 +
与 *
的含义。
[branch]
段表示分支同步设置
假设当前在 master
分支,执行 git pull
若出现以下提示:
1 | There is no tracking information for the current branch. |
就说明 .git/config
文件缺少对应的 [branch "master"]
字段。
解决方案为:
1 | git branch -u origin/master master |
会出现提示:
Branch ‘master’ set up to track remote branch ‘master’ from ‘origin’.
其实就是生成以下内容在 .git/config
中:
1 | [branch "master"] |
你去手动编辑 .git/config
,效果一样。这就是 upstream
的真正含义,即生成 config
中的这段配置。
2.6 文件 description
看到文档 中有如下一段描述:
The description file is used only by the GitWeb program, so don’t worry about it.
说明这个文件主要用于 GitWeb
的描述,如果我们要启动 GitWeb
可用如下命令:
1 | 确保lighttpd已安装: brew install lighttpd |
默认会启动 lighttpd
服务并打开浏览器 http://127.0.0.1:1234
(试着改成对外IP并分享给别人?)
以下显示当前的 git 仓库名称以及描述,默认的描述如下:
Unnamed repository; edit this file ‘description’ to name the repository.
上面这段话就是默认的 description
文件的内容,编辑这个文件来让你 GitWeb
描述更友好。除此之外没发现其它用处。
2.7 文件夹 hooks/
存放 git hooks
,用于在 git 命令前后做检查或做些自定义动作。运行 ls -F1 .git/hooks
1 | prepare-commit-msg.sample # git commit 之前,编辑器启动之前触发,传入 COMMIT_FILE,COMMIT_SOURCE,SHA1 |
如果要启用某个 hook,只需把 .sample
删除即可,然后编辑其内容来实现相应的逻辑。
比如我们要校验每个 commit message 至少要包含两个单词,否则就提示并拒绝提交,将 commit-msg.sample
改为 commit-msg
后,编辑如下:
1 | !/bin/sh |
这样当提交一个 commit 时,会执行 bash 命令: .git/hooks/commit-msg .git/COMMIT_EDITMSG
,退出值不为 0
,就拒绝提交。
2.8 文件夹 info/
此文件夹基本就有两个文件:
- 文件
info/exclude
用于排除规则,与.gitignore
功能类似。 - 可能会包含文件
info/refs
,用于跟踪各分支的信息。此文件一般通过命令 git update-server-info 生成,里面的内容:
1 | 94e1a0d952f577fe1348d828d145507d3709e11e refs/heads/master |
这表示 master 分支所指向的文件对象 hash 值为:94e1a0d952f577fe1348d828d145507d3709e11e
,
运行 git cat-file -p 94e1a0d952f577fe1348d828d145507d3709e11e
,可以看到 master 分支最后提交的记录信息。
同时:cat .git/objects/94/e1a0d952f577fe1348d828d145507d3709e11e
可以看到最后提交文件的二进制内容表示。
文件 info/refs
对于 搭建 git 服务器 来说至关重要。
2.9 文件夹 logs/
记录了操作信息,git reflog
命令以及像 HEAD@{1}
形式的路径会用到。如果删除此文件夹(危险!),那么依赖于 reflog 的命令就会报错。
1 | mv .git/logs .git/logs_bak |
报错信息如下:
error: pathspec ‘HEAD@{1}’ did not match any file(s) known to git
2.10 文件夹 objects/
此文件夹简单说,就是 git的数据库
,运行 tree .git/objects
,可以看到目录结构:
1 | .git/objects/ |
这些文件分两种形式:**pack压缩包 形式放在 pack/
目录下,除此之外都是 hash文件
形式,被叫做 loost objects
**。
这个文件夹以及相应的算法,我没找到独立的名称,就叫它 hash-object
体系吧。因为确实有个 git hash-object
命令存在,是一个底层的负责生成这些 loost objects
文件,如果要看到这些文件各自的含义,执行以下命令:
1 | git cat-file --batch-check --batch-all-objects |
可以看到
1 | 04c87c65f142f33945f2f5951cf7801a32dfa240 commit 194 |
但你会发现这个列表里有些值在文件夹中并不存在,因为除了 loost objects
它还汇总了 pack
文件中的内容。
hash 文件
又称为 loose object
,文件名称共由 40 字符的 SHA-1 hash 值组成,其中前两个字符为文件夹分桶,后 38 个字符为文件名称。
按文件内容可分为四种类型:commit, tree, blob, tag,若执行以下命令会生成所有四种类型:
1 | echo -en 'xx\n' > xx # 共 3 个字符 |
经过以上操作后,对比一下文件树,发现多了四个 hash文件
:
1 | |-- 0c |
这四个 hash文件
分别是:
1 | cc/c9bd67dc5c467859102d53d54c5ce851273bdd # blob |
我们想看下里面到底存的什么?其实这些文件都经过了压缩,压缩形式为 zlib 。先安装一下解压工具 macOS 版 brew install pigz
或 windows 版 pigz ,后执行:
1 | pigz -d < .git/objects/cc/c9bd67dc5c467859102d53d54c5ce851273bdd |
会发现,显示结果都是 type size+内容
形式,这就是 object 文件的存储格式:
1 | [type] [size][NULL][content] |
type
可选值:commit, tree, blob, tag,NULL
就是C语言里的字符结束符:\0
,size
就是 NULL
后内容的字节长度。
type
的几种类型可以使用 git cat-file -t hash
看到,内容可以用 git cat-file -p hash
看到。
1 | git cat-file -t ccc9bd67dc5c467859102d53d54c5ce851273bdd |
所以 blob
文件就是对原文件内容的全量拷贝,同时前面加了 blob size\0
,而文件名称的 hash
值计算是计算整体字符的 SHA-1
值:
1 | echo -en 'blob 3\0xx\n' | shasum |
知道原理后,其它类型格式请自行参考 斯坦福 Ben Lynn 所著的 GitMagic 。
所以,当我们 git show 3020feea86d222d83218eb3eb5aa9f58f73df04d
时,会发生些什么?
- 找到
3020feea86d222d83218eb3eb5aa9f58f73df04d
这个commit
,显示出来 - 找到此
commit
关联的tree object
:adf4c9afac7afae3ff3e95e6c4eefe009d547f00
,拉取相应的blob
文件,并与当前工作区内的文件做diff
,然后显示出来
这就是 objects/
文件夹作为 git数据库
被使用的真实例子。
pack 文件
为什么会有 .pack
文件?
由于每次 commit
都会生成许多 hash文件
,并且由于 blob
文件都是全量存储的,导致 git 效率下降,于是有了 pack-format ,优势:
- 对于大仓库存储效率高
- 利于网络传输,便于备份
- 增量存储,优化磁盘空间
将 .git/objects
下的部分文件打包成 pack格式
1 | tree .git/objects/ | wc -l |
可以看到文件数量减小了不少,其中大部分文件被打到一个 .pack
包中,并且是增量存储,有部分变更的文件只存储 基础hash + 变更内容,磁盘空间优化很明显。
⭐**git gc
其实运行了两条命令:git repack
用来打包 和 git prune-packed
用来移除已打包的 hash文件
**
- 如果你想打包所有文件,并不推荐,但可以用以下命令:
1 | git repack -a -d -f --depth=250 --window=250 |
具体可见:此问题
- 如果想看一下包里有啥,运行:
1 | git verify-pack -v .git/objects/pack/pack-5963b552193021791c1a0ab9136c272f07124c98.pack |
显示如下:
1 | 5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 commit 245 153 12 |
后面那串数字说明文档里很详细:
1 | When specifying the -v option the format used is: |
以上最后有 hash 的条目,说明是增量存储的 基础hash,其前是增量深度。
2.11 文件夹 refs/
refs
可以理解成文件系统中的 symbol link
,看下结构:
1 | tree .git/refs/ |
可以看到 master
和 v1.0
都指向 5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5
这个 commit
。
refs/heads/
文件夹内的 ref
一般通过 git branch
生成。git show-ref --heads
可以查看。
refs/tags/
文件夹内的 ref
一般通过 git tag
生成。git show-ref --tags
可以查看。
如下:
1 | git branch abc |
说明新建分支其实就是生成了一个指向某个 commit
的 symbol link
,当然在这里叫做 ref
。
而 git tag
命令本质与 git branch
相同,只生成一个 ref
放在 tags
目录下,所以被称为 lightweight tag
。
而 git tag -a xx
命令会首先生成一个类型为 tag
的 hash文件
放到 objects/
目录,然后生成 ref
放到 tags
目录下指向那个文件。这就叫做 annotated tag
,好处是可包含一些元信息如 tagger
和 message
,被 git 的 hash-object
算法管理,可被 GPG 签名等,所以更稳定,更安全。
使用以下命令来拿到 refs
文件夹存储的信息:
1 | git show-ref --head --dereference |
我们来看这些信息如何变化的:
1 | touch new_file && git add . && git commit -m 'add new_file' |
diff 一下可以看到:
1 | 5978c2c79cd3a4711fb8edd3166c9f9f5c8c97f5 HEAD |
这两行发生了变化。也就是每次 commit 时,HEAD 与 heads
都会自动更新。
2.12 文件 index
细心的读者发现,没有讲 index
文件?原因在于:
⭐**index
文件是整个 git 除 hash-object
体系最核心的部分,值得用单独一篇来讲**。
可以先参考以下文章:
简要说一下,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
层面做对比,并输出变更。
以上,这几个文件夹咱们用一张图做总结: