乐趣区

关于git:GIT-浅析原理篇

此篇文章次要是讲讲 一些 git 操作产生的时候 , .git 文件如何变动,git 背地产生了什么。磨刀不误砍柴工嘛!算是一篇视频观后笔记(文末取视频地址)

根底概念

Git 是一个代码版本管控的工具,是一个内容寻址文件系统,即简略的键值对数据库。

Git 的一些根底基础知识

  • 版本库:git 在本地开拓的一个存储空间,个别在 .git 文件里。
  • 工作区(workspace):就是编辑器外面的代码,平时开发间接操作的就是工作区。
  • 索引区 / 暂存区(index/stage):临时寄存文件的中央,git add 后也就将工作区代码放到了暂存区(缓冲区)。
  • 本地仓库(Repository)git commit 后就是将暂存区的代码放到本地仓库。
  • 近程仓库(Remote):线上代码寄存的中央,如 github/gitee。

他们之间的关系是这样子的:

本文伊始,须要通晓一些 git 相干 根底命令

如果对根底 linux 的命令不相熟请查看 这里

git cat-file -t <sha1-hash> # 查看文件类型 commit 取前四位即可
git cat-file -p <sha1-hash> # 查看文件内容
cat .git/refs/heads/master # 查看 master 指向的文件
git ls-files # 查看索引区文件,也就是 index 的内容
git ls-files -s #查看 索引 / 暂存 区的文件内容

初始化

本文环境 后面是win10 => cmder, 前面是mac => iterm2+zsh

先在 porn…. 呸,github 先创立一个清新的近程仓库

举荐三种形式 往空仓库“填充”代码。

咱们先别顺着 Git 这小子,先把它克隆下来,看看 .git 文件

D:\assets
λ git clone https://github.com/OFreshman/gitTest.git
Cloning into 'gitTest'...
warning: You appear to have cloned an empty repository.
λ rm -rf .git/*.sample # 为了看起来洁净,删除了 sample 文件 
D:\assets\gitTest (main -> origin)
λ tree .git /f # 以树形构造递归查看所有文件夹即文件夹,D:\ASSETS\GITTEST\.GIT
│  config # 仓库本地配置文件
│  description # 形容 用于 GitWeb
│  HEAD # 工作区目录指向的分支
├─hooks # 寄存一些 shell 脚本,能够设置特定的 git 命令后触发相应的脚本
├─info # 附加信息 
│      exclude # git 文件排除规定。相似 .gitnore
├─objects # 所有文件存储对象
│  ├─info # 对象存储的附加信息,存储着打包后的文件名
│  └─pack # git 压缩对象
└─refs # 夹存储着分支和标签的援用
    ├─heads # 分支对应对象
    └─tags # 标签

各文件详情补充

.git/info

info/exclude,初始化时只有这个文件,用于排除提交规定,与 .gitignore 性能相似。区别在于.gitignore 这个文件自身会提交到版本库中去,用来保留的是公共须要排除的文件;而info/exclude 设置的则是你本人本地须要排除的文件,他不会影响到其他人,也不会提交到版本库中去。

info/refs,如果新建了分支后,会有 info/refs 文件,用于跟踪各分支的信息。对于刚克隆下来的仓库 能够通过命令去生成 info/refs 文件

FETCH_HEAD

指向上一次的 fetch 的 commit

➜  gitTest git:(main) cat .git/FETCH_HEAD
> d4166b7319f1c85b7a86718f6a0387e0e9b1fa2c        branch 'main' of github.com:OFreshman/gitTest

如果须要合并能够这样

git merge FETCH_HEAD

因为刚刚 fetch 的是 main 分支,下面命令相当于执行 git merge remote/main

.git/index

二进制暂存区(stage)

COMMIT-EDITMSG

COMMIT-EDITMSG 是一个临时文件,存储最初一次提交的 message。有提交才会有这个文件夹。

logs

存储提交、操作日志,git reflog 依赖此文件夹。

refs

refs/heads/ 文件夹内的 ref 个别通过 git branch 生成。git show-ref --heads 能够查看;
refs/tags/ 文件夹内的 ref 个别通过 git tag 生成。git show-ref --tags 能够查看。
refs/remotes/ 文件个别是寄存近程的分支。git fetch 就会更新外面的内容


下面大略阐明了.git 各子文件的作用。因为这是个空仓库(warning: You appear to have cloned an empty repository),所以没有分支,即便看起来像有(main -> origin), 此时曾经初始化了一个 git 仓库了(相当于执行了 git init),再次执行 git init 会提醒 反复初始化曾经存在的仓库了。

λ git init
> Reinitialized existing Git repository in D:/assets/gitTest/.git/ 

留神:线上的默认分支是 main,本地是 master。

另外此时本地 git branch 无奈查看分支,须要 git add && git commit 后能力查看。如果是 git init 初始化 会有 分支,且 存在 .git/branchs 文件夹。

refs/heads, refs/tags 目前都为空(因为克隆的就是空仓库), HEAD 指向工作区的分支

λ cat .git/HEAD
ref: refs/heads/main

git status 查看到未提交的代码时为啥不一样, 其实就是比照了暂存区和工作区的文件内容。index(索引区)始终保留着最新的待提交的文件

工作区到暂存区(add)

新建一个文件,并写入内容 hello txt

echo 'hello txt'>hello.txt

此时 该文件在工作区(workspace),未被追踪(untracked),将其增加至 索引区(index)

git add hello.txt

能够看到减少了两个文件 index, objects/32,objects 外面是一个个 hash 对象,hash 值前两位为一级文件夹名,残余值为相应二级文件夹名,前两位雷同的会被归到同一个文件夹,而 index 是寄存暂存区文件的文件夹。

这里就产生了一个疑难:为什么 Git 要这么设计目录构造,而不间接用 Git 对象的 40 位 hash 作为文件名?起因是有两点:

  1. 有些文件系统对目录下的文件数量有限度。例如,FAT32 限度单目录下的最大文件数量是 65535 个,如果应用 U 盘拷贝 Git 文件就可能呈现问题。
  2. 有些文件系统拜访文件是一个线性查找的过程,目录下的文件越多,拜访越慢。

objects 一级子目录数量就变成了 <= (10+26)^2。大大减少了数量,相应的也晋升了检索速度

objects 外面的 hash 对象类型有三种: blobcommittree

git 提供了相应的命令去查看类型, 查看的时候只须要带上文件夹名(32)+ hash 子文件前四位(4b9a)。四位也能够只有是惟一。

能够查看该文件内容

也能够查看文件长度

当反复增加雷同内容的不同文件时,objects 不会扭转,因为 32-4b9a6927b8f2217f751be4f8379e0d093856ab 存的是文件内容且 Git 有反复检测。

32-4b9a6927b8f2217f751be4f8379e0d093856ab 不止是文件内容的 hash,而是蕴含 类型 + 长度 + \0(linux 字符串结束符) + 文件内容 的 sha1 hash 值,可验证一下。(shasum 是 mac 装置的命令包,默认输入 sha1 hash)

还有个命令能够佐证 失去的 hash

how-is-the-git-hash-calculated

(printf "commit %s\0" $(git cat-file commit 324b9a | wc -c); git cat-file commit 324b9a)|shasum
> 324b9a6927b8f2217f751be4f8379e0d093856ab

shasum 是获取文件的各种 hash 值 的函数,默认 SHA1。参考 mac 下如何获取文件 MD5 校验值和 SHA1 校验值 – 掘金

此时索引区文件(-s 是 –stage 的缩写)

-s 失去的事索引区 文件的 权限 + blob 对象 + 0 + 文件名

为了比照看成果,此时 减少新文件 hello.txt 并去批改文件 hello.txt , git status 查看状态的时候 之前是 未跟踪(untracked), 当初是批改(modify)

将新文件提交,那么 .git/objects 中的 blob 对象就会扭转

索引区到本地仓库(commit)

接下来对下面的文件进行 commit

附带的信息:这是根提交(root-commit , 第一次),提交对象的 hash 值,文件扭转数量和行数,文件权限(之前提到过),文件名。

commit 之后 可应用 git rev-parse HEAD:<file> 查看以后版本对象的 sha1 值

git rev-parse HEAD:hello.txt
5b10c6a97b7b4132c2ad4d6d80ceddd2b8a4fdba

此次提交的对象类型为 commit

这个 commit 对象的内容含有 tree 类型对象,以及仓库作者和提交者信息,提交信息。

.git 文件 也有一些变动

logs 是 git 历史相干的文件。objects 里减少了后面提的的两个对象(commit,tree)以及 heads 外面减少了 main。也就在此时本地也有了分支 main (git branch 查看)。

HEAD 是一个指向当前工作分支的指针,目前指向 main, refs/heads/main 是啥?就是 main 分支指向的 最新提交对象。

tree 对象含有两个 blob 文件,仔细观察能够发现就是之前 add 时 增加的两个 blob 对象

当初 objects 的里对象的关系如下

为了探索其中的神秘,再次更改 hello.txt(vim hello.txt, git add, git commit), 具体细节就不展现了,给一个 objects 比照图

新增了 四个对象,git cat-file -p 一一查看新增的四个内容

对于这次 commit 对象 多了一个 父级对象(parent)。简略梳理一下就是

或者能从后面两张图总结出一些法则

  • 每次 add 都会生成一个 blob 对象
  • 每次 commit 都会生成 commit 和 tree 对象以及若干个 blob 对象 (blob 数量 = 新增 / 批改的文件数)
  • tree 对象 始终蕴含最新的所有文件

接着验证一下,更改 test.txthello.txt, 减少文件 demo.txt

git add . 时会减少三个 对象

git commit 之后减少了两个对象,一个是 commit 类型(),一个是 commit 类型蕴含的 tree 类型

总共减少了五个对象,它们的关系如下图

至此,后面的推断被证实,且在用户增加 / 提交文件的时候,objects 的外部变动也能很好的体现。objects 里保留在暂存区和本地仓库的文件对象。

main 分支指向最新的提交,暂存区蕴含最新的所有文件。

➜  gitTest git:(main) cat .git/refs/heads/main
0d8d0eb0446d7bf92a32512089fa927314675ac9
➜  gitTest git:(main) git ls-files -s
100644 598bc0d8552fb08de29c7fcd317cacf09c0f237b 0    demo.txt
100644 acdf79a141b2f07dca7b715d606b23307d669f94 0    hello.txt
100644 ba0ba9399c8a2336b1aab6a61fa499b012561588 0    test.txt

此时提交历史就是上图中 三个 commit 的提交

git log --oneline
* 0d8d0eb (HEAD -> main) 2nd commit
* de07e86 change hello.txt
* 779c005 added two files

仓库含有文件

这是常见的状况,然而后面为了简化变动,仅仅应用了文件。

应用文件夹有一些不一样,后面说过 每次提交都会产生一个 committree、若干个 blob(取决于文件数量)对象,当有文件夹时,committree(root) 依旧会存在,仓库的一级 文件 / 文件夹 会别离作 blob/tree(1-level) 被蕴含在 tree(root) 上面,如果一级文件夹外面蕴含文件夹,那么其子文件依旧会作为 tree(2-level), 蕴含在 其 父级 tree(1-level) 下, 该子文件夹的子文件 会被作为 blob 蕴含在子 tree(1-level)

比方仓库有 一个文件夹 FF里有个文件 f.txt,两个文件 a.txt, b.txt
add 会产生 三个 blob(f.txt, a.txt, b.txt)
commit 会产生 两个 tree(自身一个,F 文件夹一个),一个 commit

commit > tree > [tree(F) > blob(f.txt) , blob(a.txt), blob(b.txt)]

不一样的就是 文件 F 也作为一个 tree, 这个 tree 含有一个 blob, 即该文件夹下的子文件。

后面文字说的可能有点绕,补一张图(某仓库下增加了若干个文件夹,文件,在第一次提交之后 .git/objects 的状况)

当初再举个🌰,我在仓库建设一些文件

第一次 add 后,有五个 blob 对象,就是那五个文件

commit 后 会有 1 个 commit、6 个tree(1+5),总共 12 个对象

和 Objects 对象图

分支

分支大家都不生疏,在 Git 里,分支就是一个非凡(具备名字)的指针,指向某个 commit。

另外 git 外面还有个 HEAD 文件

这也是个指针,在沉闷的分支上指向最新的提交。简略了解就是 切到哪个分支,它就指向哪个分支。能够相干命令查看一下

➜  gitTest git:(main) cat .git/HEAD
ref: refs/heads/main
➜  gitTest git:(main) cat .git/refs/heads/main
0d8d0eb0446d7bf92a32512089fa927314675ac9

有这样的关系: HEAD => main => lastest commit

接着新建一个 dev 分支,此时 .git/refs, .git/logs 都扭转了

tree -I <ignore-file> <file> 树形开展 file 文件并疏忽 ignore-file

后面说过 logs 示意 git 的提交历史,此时查看提交历史

括号外面示意分支 main 和 dev 都指向 0d8d0ed 的 commit 对象,且 HEAD 指向 main(以后在 main 分支上);

简略画了个图

master 上产生一个 commitdev 也产生一个(停在此分支),就有如下关系

而后 再去合并 main 会产生什么?

  1. 产生一个新的 commit;
  2. dev 指向新的 commit;

那么删除分支会删除分支指向的 commit 吗?答案是不会的

至此,分支的 创立、切换、合并、删除都演示完了。

记住一句话:分支是一个有名字的指针,指向 某个 commit

变基 Rebase

有了下面的常识,变基就很好了解了。次要有两种状况

  1. git rebase <branch> 变基分支
  2. git rebase -i <commit-id> 变基提交

先说状况 2,了解了状况 2,状况 1 就很好了解了。

变基到某个提交

变基,变基,就是扭转基底,艰深一些就是基于哪个 commit。变基到某个提交也有两种状况,本分支或者其它的分支上的提交。其实原理差不多,这里演示变基其余分支的提交。

为了不便查看成果,删除原有仓库及文件,从新初始化,并且进行一个提交。

# 清空仓库
➜  gitTest git:(new) rm -rf .git
➜  gitTest rm hello.txt test.txt

# 初始化
➜  gitTest git init
已初始化空的 Git 仓库于 /Users/Public/Learn/git/gitTest/.git/
➜  gitTest git:(master) echo "master txt" > master.txt
➜  gitTest git:(master) ✗ git add . && git commit -m "1st commit"
[master(根提交)b505c69] add master.txt
 1 file changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 one.txt

接着切出 dev 分支,产生三个提交,master 上产生两个

能够看到 b505c6 是他们的独特提交, 也可成为基底。

而后 dev 上变基 到 master 上第一个提交 80b77b

git rabse -i 80b77b

我抉择 sword 更改三条提交正文,前面都加上了 rebase

变基胜利。

新产生了三个 commit 对应着 dev 上原来的三个 commit, 并且原来的三个 commit 隐没了,dev 的提交历史如下

master 分支不受影响,关系如下图。

变基分支

假如当初 main 分支有 1 个提交 9b789c,新建分支 dev,此时 main、dev 分支的基底就为9b789c2

接着 在 dev 上产生了一个提交,main 上产生了 2 个提交,

用图总结一下

此时 在 dev 分支上执行 git rebase main(不加 -i 就是啥都不扭转,就是间接产生若干新提交到 main 指向的提交后)。

dev 提交历史

main 分支不受影响

关系如图。

总结

变基就是 将以后分支 的所有 commits 都变为新的 commits,这些新 commits 的第一个的上一个提交为变基的指标 commit。后面说过,分支其实是个有名字的指针(相似 HEAD),指向以后分支的最新 commit,分支变基不过是变基指标 commit 不一样而已!

近程仓库

本地初始化仓库,并增加 近程(remote)配置,近程名为 origin

git init
git remote add origin https://github.com/OFreshman/gitTest.git

配置 文件 .git/config 会减少 origin 的配置

此时再建设本地与近程追踪,并推送至近程

objects 也有一些小变动

去查看这两个文件能够看到文件内容 含有的对象都是一个 29906142d145c9bcf6438088b7e4e5ac13853174

此时日志也表明 main 与 origin/main 都指向 299061 ,

总结起来就是近程仓库其实就相当于一个分支,只是 分支名有些非凡:origin/main, 相干的数据配置保留在 根目录和 logs 的 refs/remotes

对象压缩

git 作为代码库,有的代码仓库上 G,甚至更大,如果不对这些仓库 代码文件做优化策略那么只有波及文件的读取存改(在 git 这又是高频操作)会变得很慢,且存储下载占用大量带宽!所以 Git 采取压缩的策略

当初 gitTest 文件夹下有两个文件 init.txt、test.txt, 并且初始化仓库

接着提交,objects 里减少了一些对象,9d、d5 为相应的 blob 对象,能够看到大小都失去了压缩。

压缩应用的是 zlib 的 deflate 算法

之前是 2.3M,155k。

通过 命令 git gc 被动压缩

压缩之后,info 和 pack 减少了一些文件,次要关注 pack,外面含有 idx 和 pack 后缀的文件,压缩的所有文件对象都含在 pack 文件里,idx 相当于是个索引文件

git 提供了命令去查看压缩文件内容 git verify-pack -v [<file>]

显示的信息顺次为

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

git 也提供解压缩的 命令

git unpack-objects < <pack-file> #解压 .pack 文件

解压缩须要留神,须要将压缩文件挪动地位(比方到 .git 根目录),因为

Objects that already exist in the repository will not be unpacked from the packfile Therefore, nothing will be unpacked if you use this command on a packfile that exists within the target repository.

git push 也是一个压缩的过程。

git clone 也会产生压缩

刚克隆的仓库只有 objects/pack 含有相应的压缩文件

对于批改文件。.idx 内 只保留最新版本(因为用的最频繁),以前版本只保留 diff

垃圾对象

垃圾对象是指 .git/objects 外面那些无用的对象。常见的两种状况下会产生:

  1. git add n 次,执行 commit,n- 1 次的 add 就会产生 n-1 个垃圾对象。TODO diff 差量存储咋做的。
  2. 分支删除后该分支上产生的 objects。

针对状况 1 ,波及到对象压缩(git gc)的操作时会去革除对象,这里我被动触发

In most cases, users should run git gc, which calls git prune.

没被压缩的就被视为了垃圾对象(前两次批改时增加的),执行 git prune 取清理掉垃圾对象(96,b1), 加 -n 能够看到 此命令会革除哪些对象。

针对状况 2 ,就简单一些,分支被删除,咱们能够 通过 reflog 找到想要的提交再去拿进去。这也是 Git 的的想法:这些提交对象被视为未来可能会被用,所有就不会被当做垃圾对象,即便 执行 git prune。然而有些状况下我确定那分支上的提交永远不会用了,而且这个分支上的提交对象都还比拟大,不删除的话空间资源比拟节约(磁盘,云)。那该如何删除呢,网友提供了这样一条命令:

参考 #How to remove unused objects from a git repository?

git -c gc.reflogExpire=0 -c gc.reflogExpireUnreachable=0 \
  -c gc.rerereresolved=0 -c gc.rerereunresolved=0 \
  -c gc.pruneExpire=now gc "$@"

最初

全文次要介绍了 git 惯例的一些场景下 .git 文件的变动,次要是 .git/objects,根本讲清楚了 git 背地的那些事。

本文拖了很久,后期看到本人文章池(写了,但没有写完的文章)有 git 的一篇,去年开始写的,每天上班就补一补,总算是写完了。

参考

Git 基本原理介绍 -Blibli

一文讲透 Git 底层数据结构和原理

用 21 张图,把 Git 工作原理彻底说分明 - 腾讯云开发者社区 - 腾讯云

这才是真正的 Git——Git 外部原理 – 掘金

这个 git 命令你每天都在用,但你却不晓得 – 掘金

Git 不要只会 pull 和 push,试试这 5 条提高效率的命令 – 掘金

45 个 GIT 经典操作场景,专治不会合代码 – 掘金

前端架构师的 git 功力,你有几成火候?– 掘金

「一劳永逸」一张脑图带你把握 Git 命令 – 掘金

多年 Git 应用心得 & 常见问题整顿 – 掘金

Git 原理与高级应用(2) – 掘金

退出移动版