15

Git终了级总结之三

 4 years ago
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.
neoserver,ios ssh client

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

小结

  1. 文件在Git中以blob对象的形式存放
  2. 目录以tree对象的形式存放,tree能包含其它treeblob
  3. commit对象包含了一个tree信息,提交时间,作者,提交文本信息,并且它可能有若干个parent,指向其它commit
  4. 当前分支用HEAD文件来指向,分支的内容其实是一个commit,在提交后发生变动
  5. 标签的内容其实也是一个commit,只不过不随着提交而变动,如果是带信息的标签,以tag对象的形式存放
  6. 在执行git add把文件放入暂存区后,blob对象生成,git commit提交之后,treecommit对象才生成
  7. 在推送到远程仓库服务器后,.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: 版本库允许存在的不可达对象的数量,默认为6700
  • gc.autopacklimit: 在重新打包成一个更大更高效的打包文件之前,版本库允许存在的打包文件数量,默认为50
  • gc.pruneexpire: 不可达对象在库里的持续时间,默认为两周
  • gc.reflogexpire: git reflog expire命令会删除比这个时间旧的reflog条目,默认90天
  • gc.reflogexpireunreachable: 仅当reflog条目在当前分支不可达时,大于一定时间限制的条目才会被git reflog expire命令删除,默认30天

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK