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 # 具体的引用,主要存储分支和标签的引用
当然除了上面说到的只是列举了一部分,执行一些特定的命令时,还会有别的文件出现。例如:
- ORIG_HEAD(此文件会在你进行危险操作时备份 HEAD,如git reset、git merge、git rebase、git pull
- FETCH_HEAD(这个文件作用在于追踪远程分支的拉取与合并,与其相关的命令有 git pull/fetch/merge)
… 如果你觉得有什么不对或疑惑的地方,欢迎纠正或提出问题,感谢!!
参考: