

Git终了级总结之三
source link: https://zhuanlan.zhihu.com/p/136267922
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Git终了级总结之三
本文主要是关于Git
的原理部分。
准备
计算SHA1
一种方式是使用管道
$ echo "Hello World" | git hash-object --stdin
557db03de997c86a4a028e1ebd3a1ceb225be238
另一种方式是针对于文件,文件内容也是Hello World
$ git hash-object -w readme.md
#等同于 cat readme.md | git hash-object --stdin
557db03de997c86a4a028e1ebd3a1ceb225be238
四大对象类型
blob
tree
commit
tag
不是增量保存
一定要理解Git
不是增量保存,它存储的不是增量的变化,而是文件的全部内容。
初始化
看看git init
之后在.git
里的内容是什么
|__config #配置
│__description
│__HEAD #指向当前分支最新commit
│__inedx #保存暂存区的信息
├─hooks #钩子
│
├─info
│__exclude
│
├─objects #对象文件
│ ├─info
│ └─pack
└─refs # 分支和标签的指针
├─heads
└─tags
在工作区里变化文件并不会直接引起.git
目录下的变化。
加入暂存区
$ git add readme.md
$ ls .git/objects/
55/ info/ pack/
$ ls .git/objects/55
7db03de997c86a4a028e1ebd3a1ceb225be238
看到以55
开头,并以SHA1
余下的字符串作为文件名,在.git/objects
里新建了一个文件。直接用cat
查看文件是看不出来的,这个文件内容已经被压缩,使用如下的命令查看类型
$ git cat-file -t 557db03de997c86a4a028e1ebd3a1ceb225be238
blob
看到这个文件的类型是Blob
对象。Blob
对象在Git
里保存一个文件的数据,但不包含文件的元数据,就是说,只有内容信息,不包含文件名等。
接着查看文件内容
$ git cat-file -p 557db03de997c86a4a028e1ebd3a1ceb225be238
Hello World
注意只有建出文件来才能被Git
所识别,仅仅新建一个空的目录,Git
是不会鸟你的。空的目录或者说目录在Git
里并不是一个Blob
对象。
假如现在又把config/package.json
加入到了暂存区。
$ git add config/package.json
能看到多了一个09/67ef424bce6791893e9a57bb952f80fd536e93
文件,同之前一个文件是相似的。
另外,可以用下面的命令显示所有文件和object
的关系列表
$ ls .git/objects/8a
32107db0566c57b0fd6e127c0a6456ad752140
$ git ls-files -s # 即--stage 列出暂存区的文件
100644 8a32107db0566c57b0fd6e127c0a6456ad752140 0 index
提交
接下来执行一下git commit
,再次观察,发现.git/object
多了好几个文件
├─09
│__67ef424bce6791893e9a57bb952f80fd536e93
│
├─1d
│__e921e93d60201b5d8531e215bf1976170cbc2e
│
├─51
│__b6ad67508f872084d3f99c7f3b17f639ea1eaa
│
├─55
│__7db03de997c86a4a028e1ebd3a1ceb225be238
│
├─c7
│__aed370443cb58b7638e72af2f4d430f1ae97d7
│
├─info
└─pack
先看两个tree
的
$ git cat-file -t 51b6ad67508f872084d3f99c7f3b17f639ea1eaa
tree
$ git cat-file -p 51b6ad67508f872084d3f99c7f3b17f639ea1eaa
100644 blob 0967ef424bce6791893e9a57bb952f80fd536e93 package.json
$ git cat-file -t c7aed370443cb58b7638e72af2f4d430f1ae97d7
tree
$ git cat-file -p c7aed370443cb58b7638e72af2f4d430f1ae97d7
040000 tree 51b6ad67508f872084d3f99c7f3b17f639ea1eaa config
100644 blob 557db03de997c86a4a028e1ebd3a1ceb225be238 readme.md
其中查看tree
的命令等效于
$ git ls-tree c7aed370443cb58b7638e72af2f4d430f1ae97d7
040000 tree 51b6ad67508f872084d3f99c7f3b17f639ea1eaa config
100644 blob 557db03de997c86a4a028e1ebd3a1ceb225be238 readme.md
再看一个commit
的
$ git cat-file -t 1de921e93d60201b5d8531e215bf1976170cbc2e
commit
$ git cat-file -p 1de921e93d60201b5d8531e215bf1976170cbc2e
tree c7aed370443cb58b7638e72af2f4d430f1ae97d7
author lianli <[email protected]> 1586957938 +0800
committer lianli <[email protected]> 1586957938 +0800
init commit
至此,可以画出一张图来把整个文件、目录、commit
都串联起来。
可以看到从一个commit
进入,可以顺着各条线,把这个commit
对应的所有的内容都恢复出来。
修改一下readme
,再次提交,不出意外的话会多出若干个新对象,其中一个是新的commit
$ git cat-file -p 1123d3d5df84367ee25821663e6ec76173f974d2
tree b2f397b7dccd0c1bd5d209f063bfaaf60aa61de0
parent 1de921e93d60201b5d8531e215bf1976170cbc2e
author lianli <[email protected]> 1586958866 +0800
committer lianli <[email protected]> 1586958866 +0800
change readme
看到这个commit
有一个parent
,这样可以顺藤摸瓜找到之前的所有的提交记录。
如果想要列出所有的tree
对象,可以
$ git cat-file -p master^{tree} #在master分支查看所有树
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 index
040000 tree c15516c5cb08087b79481e16699a14320a0b9e30 jjj
040000 tree 94addd1e230cdb76cd9af507d496fc98a75f5365 sss
要强调的是只有在执行了git commit
之后,tree
对象的文件才生成,而如果你想手动生成tree
对象文件的话,可以使用git write-tree
底层命令,执行完后会看到多出了tree
对象的文件。
$ git write-tree
50c8353444afbef3172c999ef6cff8d31309ac3e
而对应的手动生成commit
对象的底层命令是git commit-tree
,但仅仅是生成commit
对象文件而已,完成后并不能在历史日志里看到
$ echo -n "first commit" | git commit-tree 50c8353444afbef3172c999ef6cff8d31309ac3e
37fd9f1dd830cae9055315367b22d3e4db40f7ec
$ git cat-file -t 37fd9f1dd830cae9055315367b22d3e4db40f7ec
commit
如果想在git log
里看到,只要把SHA1
写入当前HEAD
的内容即可,相当于人工移动了指针
$ git update-ref refs/heads/master 37fd9f1dd830cae9055315367b22d3e4db40f7ec
# 或者
$ echo 37fd9f1dd830cae9055315367b22d3e4db40f7ec > .git/refs/heads/master
上面的git update-ref
还能用来人工创建分支,比如
$git update-ref refs/heads/feature-zhangsan cac0ca
分支
现在创建一个foo
分支
$ git checkout -b foo
在.git/refs/heads/
目录下就多了个foo
文件
$ ls .git/refs/heads/
foo master
同时HEAD
文件也指向了foo
$ cat .git/HEAD
ref: refs/heads/foo
查看foo
的内容,正是指向了最新的那个commit
$ cat .git/refs/heads/foo
1123d3d5df84367ee25821663e6ec76173f974d2
假如在foo
分支进行了修改,然后master
来合并这个修改,并且使用-no-ff
参数
$ git lg
* 9ddc1ce - (HEAD -> master) Merge branch 'foo' (7 seconds ago) <lianli>
|\
| * 2de9b88 - (foo) change readme in foo branch (73 seconds ago) <lianli>
* | 8e5b274 - add new file (49 seconds ago) <lianli>
|/
* 1123d3d - change readme (10 minutes ago) <lianli>
* 1de921e - init commit (26 minutes ago) <lianli>
做这个试验是因为好奇9ddc1ce
是否会有两个parent
,果然事实验证了我的想法
$ git cat-file -p 9ddc1ce
tree cda104a9b5d12f6b5d48d9189738878258f12910
parent 8e5b2744eb222c909a802a0c6f6c0c0bcdc29caa
parent 2de9b88e9c524734ccebb20c8a3ff948d5043abe
author lianli <[email protected]> 1586959484 +0800
committer lianli <[email protected]> 1586959484 +0800
Merge branch 'foo'
远程仓库分支信息对应在.git/refs/remotes/
下,比如.git/refs/remotes/origin/master
,可以用git ls-remote
查看
$ git ls-remote
From https://github.com/vuejs/vuex.git
e52756cab002c35f67bb6e13236fa9058830adce HEAD
18286b06444dd4778c42d19f2880cd714478a3a7 refs/heads/0.3.0
eb4d8249f5855a49f272d39e467d9b22cb8d5902 refs/heads/0.4.0
36c2951d33be1133b5101b5bdec3172a97113e54 refs/heads/1.0
b9b43f8faaff90c30ce526d9bf075cd3f57ee3e7 refs/heads/4.0
1d64911a42b545fdd0af59d1486d2dc96803853b refs/heads/4.0-new-build-system
ff229e69ef3e72638b618e536110dafd0da8e19e refs/heads/4.0-types
fb073928f5ededce9b6ee7f6c0e5ae84f3e9f247 refs/heads/adapt-new-typings
d037c26bebb7cc2a2d6af9671285d3a60291277f refs/heads/conventional-changelog
...
远程引用是只读的(你所能改的只能是checkout
远程分支所得的本地分支),就不要考虑直接修改它们了。
HEAD
总结一下在Git
里若干的HEAD
HEAD
指向当前分支的最新commit
,切换分支时,把HEAD
指向新分支的最新commit
ORIG_HEAD
在执行一些有风险的操作时,Git
自动把之前的commit
存到ORIG_HEAD
里面,以便失败的时候做回滚FETCH_HEAD
使用git fetch
会把远程分支的信息记录到.git/FETCH_HEAD
中MERGE_HEAD
合并操作在进行的时候,其他分支的头暂时存在MERGE_HEAD
中,即是正在合并进HEAD
的提交
HEAD
本质就是一个commit
$ git cat-file -p HEAD
tree a965fbed2bfac2920528351dd478ac790acbb232
parent 4ebfaf98d081a4f698dacceb4cc797470e3ca7b9
parent 28284a5e3fd6cd0fbdf111a896e0457c2eae4450
author Kia King Ishii <[email protected]> 1582812454 +0900
committer GitHub <[email protected]> 1582812454 +0900
gpgsig -----BEGIN PGP SIGNATURE-----
wsBcBAABCAAQBQJeV80nCRBK7hj4Ov3rIwAAdHIIAEi+kkyPuFItkSoIreFHcwHw
++6tElsNGYcVGufxhzlHeUQHmSCaYg5F9nYYDDSfvBwXWb7UX6FP6p2/6jOxrfrQ
l5E6NPxhUs0Vd2XmJY0meojrOgWPjW8bPergSz4uBaqdwJx81K0yr7iSASI5doYp
h8+A9lwcCrYlXy3KBUjg7eOcLdtmXbcIoI5VHiyr6MFgzQoxbvSVJgyqc1oFwvqE
EWYcuiIsZbR1tz5RfDs1xcNikVDwkQ25yudAv84jWbv5dhPuuSbQIcQeqQC4Kw20
jOEJsguajxK0P/AhFsVHJi2m5BmrxU2DO9unAc9ToSNNz9b3cfHNGoIoRHXU4eU=
=VZOx
-----END PGP SIGNATURE-----
Merge pull request #1679 from PeterChen1997/patch-1
fix docs description
计算对象数量
$ git count-objects
16 objects, 1 kilobytes
内容相同的文件
如果有内容相同的文件,岂不是会生成相同的SHA1
,这时会怎么样?还是来做试验
在新的工程里新建两个空文件,保存后提交
$ git cat-file -p ea41dba10b54a794284e0be009a11f0ff3716a28
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 bar
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 foo
两个文件含有相同的SHA1
,并且这个文件只会有一份,相当于两个文件的指针指向了同一个地方。
标签
打一个标签瞅瞅
$ git tag hello
$ ls .git/refs/tags
hello
$ cat .git/refs/tags/hello
9ddc1ce413afe1722e82ae16026afc29b91a36aa
如果是一个带信息的标签,在.git/objects
里能找到对应的对象
$ git tag bar -m "bar"
$ git cat-file -t 4ce0d2
tag
$ git cat-file -p 4ce0d2
object 9ddc1ce413afe1722e82ae16026afc29b91a36aa
type commit
tag bar
tagger lianli <[email protected]> 1586961037 +0800
bar
小结
- 文件在
Git
中以blob
对象的形式存放 - 目录以
tree
对象的形式存放,tree
能包含其它tree
和blob
commit
对象包含了一个tree
信息,提交时间,作者,提交文本信息,并且它可能有若干个parent
,指向其它commit
- 当前分支用
HEAD
文件来指向,分支的内容其实是一个commit
,在提交后发生变动 - 标签的内容其实也是一个
commit
,只不过不随着提交而变动,如果是带信息的标签,以tag
对象的形式存放 - 在执行
git add
把文件放入暂存区后,blob
对象生成,git commit
提交之后,tree
和commit
对象才生成 - 在推送到远程仓库服务器后,
.git/refs
下多出一个remote
目录,里面放了远程仓库的分支,比如ref: refs/remotes/origin/master
git gc
理解了Git
原理之后可以看垃圾回收了。在回滚或者别的操作,很有可能有些存储后的文件版本再也不会被用到了,这时候就可以执行在编程语言中常见的垃圾收集操作。
在执行垃圾回收之前,可以先用git fsck --unreachable
查看一下失效的对象。比如我在redux
工程里执行一下,就能发现好多的“叻色”啊
$ git fsck --unreachable
Checking object directories: 100% (256/256), done.
Checking objects: 100% (17011/17011), done.
unreachable commit 02029f59e34fc0e6c32d4aaa9c6ae2ad46117f1e
unreachable tree 5602822b2b389774ba13aed9e38e6b42427cf65b
unreachable commit 3e04cb440b7891f1abd8c70104624ce8cd1c8983
unreachable tree dd0e61e20732cbbb2826fdea4feea8f741e75101
unreachable tree ee0f44e9aac273da2ea3813927142f0c679c2087
...
正式的垃圾回收现在开始
$ git gc
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 12 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (6/6), done.
Total 6 (delta 0), reused 0 (delta 0)
Computing commit graph generation numbers: 100% (2/2), done.
这条命令会把.git/objects
目录下的对象打包到.git/objects/pack
目录下
$ find .git/objects -type f
.git/objects/info/commit-graph
.git/objects/info/packs
.git/objects/pack/pack-336b99230abd78a9b6efc62eb027f1ec59b115fa.idx
.git/objects/pack/pack-336b99230abd78a9b6efc62eb027f1ec59b115fa.pack
检查打包情况
$ git verify-pack -v .git/objects/pack/pack-336b99230abd78a9b6efc62eb027f1ec59b115fa.idx
c55ae4d701a27da60bc89317df658719840d00e5 commit 225 146 12
cd4bef0bdbe98cc9117cc155dc8460f4dd9036aa commit 179 120 158
d00491fd7e5bb6fa28c517a0bb32b8b506539d4d blob 2 11 278
15cef2257ae1986a491df9833f69267c1ff5cddb tree 33 44 289
b0123ccf260c4db368eb43dc20f2ab4b251fedca tree 33 43 333
3c124c9cdc0641713dfbc478105d863773e4bf87 blob 5 14 376
non delta: 6 objects
.git/objects/pack/pack-336b99230abd78a9b6efc62eb027f1ec59b115fa.pack: ok
在Git
觉得对象过多时它会自动触发垃圾回收,跟JVM
一样,另外在git push
的时候也会执行,估计是为了节约磁盘、带宽资源,能压缩就尽量压缩点。
在pack file
里,就是我们常见的差异存储了。Git
会先定位内容相似的文件,然后为它们之一存储整个内容,而其他相似的文件就只存储了差异,就这样实现了压缩的目标。
如果不带参数,当前仓库里的垃圾并没有被直接删除,如果想要立刻删除
$ git gc --prune=now
# 相当于
$ git gc
$ git prune --expire=now
一些git config
参数也决定了垃圾回收的行为
gc.auto
: 版本库允许存在的不可达对象的数量,默认为6700gc.autopacklimit
: 在重新打包成一个更大更高效的打包文件之前,版本库允许存在的打包文件数量,默认为50gc.pruneexpire
: 不可达对象在库里的持续时间,默认为两周gc.reflogexpire
:git reflog expire
命令会删除比这个时间旧的reflog
条目,默认90天gc.reflogexpireunreachable
: 仅当reflog
条目在当前分支不可达时,大于一定时间限制的条目才会被git reflog expire
命令删除,默认30天
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK