Git 使咱们天天使用得最多的工具之一,它是 linux 内核的最先做者 Linus Torvalds 建立的全新版本控制工具,和 linux 同样的简单好用,这篇文章就简单地讲一下 git 是如何工做的linux
https://git-scm.com/下载安装便可git
我在个人桌面上建立了一个名为 git_demo 的文件夹github
我这里是直接拆分窗口,便于显示,并导航到一样的目录算法
在右侧窗口中,输入如下命令,用来监控目录变化缓存
watch -d -n 1 tree --charset=ascii -aF
该命令综合了两个工具,一个是watch
一个是tree
,经过 watch 来定时(上面指定每隔 1 秒)执行一次后面的命令,咱们就可以看到实时的目录变化了bash
补上一个动图版本编辑器
在左侧命令行中执行熟悉的git init
,咱们将在当前目录建立一个本地的 git 仓库,执行完后是这样的工具
在右侧咱们能够看到,本地多出了一个名为.git
的隐藏目录,里面有一些文件夹,咱们先看第一级测试
`-- .git/ |-- HEAD |-- config |-- description |-- hooks/ |-- info/ |-- objects/ `-- refs/
其中有这么几个文件/夹this
HEAD 他的内容是
ref: refs/heads/master
说明如今的引用指向的是refs/heads/master
这个目录,其实也是咱们当前工做的位置
config 其实就是本地的 git config
[core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true
是一种类toml的数据格式
description 就是一个描述,平时用不到
Unnamed repository; edit this file 'description' to name the repository.
他的做用就是在你执行一些特定 git 命令(好比 git commit)的时候,顺带执行你指定的 bash 命令,很强大的功能。好比咱们想在提交前执行代码测试,咱们就能够去编辑 hooks 里面的pre-commit.sample
这个文件,而且去掉文件名末尾的.sample
,那在你下次执行git commit
的时候,就会先运行这个 bash,只有当整个 bash 执行成功后(退出码为 0),才会执行你刚才的git commit
.
默认的已经存放了一个名为exclude
的文件,其实就是一个.gitignore
文件
这个文件夹内,最重要的就是HEAD
文件,objects
文件夹,refs
文件夹,git 经过先访问HEAD
文件,找到咱们当前工做的分支,而后再去refs
文件夹中找到对应的分支描述,这个描述文件正是指向了objects
文件夹内的某一个文件,再经过这个文件,咱们就能获得当前分支当前版本的全部源码了!
上面看了,HEAD
文件其实就只有一行,指向了refs
目录的一个文件
咱们就来探秘一下refs
文件夹和objects
文件夹究竟是在作什么
为了不过多的文件影响,咱们先将 hooks 文件夹里的文件都删掉,只保留 pre-commit.sample
如今咱们在git_demo
目录下新建一个文件名为foo.txt
,而且在里面输入如下数据并保存(注意下面的文本结尾没有空格,没有换行)
this is foo
再查看咱们的目录结构
|-- .git/ | |-- HEAD | |-- config | |-- description | |-- hooks/ | | `-- pre-commit.sample* | |-- info/ | | `-- exclude | |-- objects/ | | |-- info/ | | `-- pack/ | `-- refs/ | |-- heads/ | `-- tags/ `-- foo.txt
如期地多出了一个foo.txt
文件
其实就是咱们无比熟悉的git add
命令了,咱们在左侧命令行执行git add foo.txt
,能够看到目录树是这样
|-- .git/ | |-- HEAD | |-- config | |-- description | |-- hooks/ | | `-- pre-commit.sample* | |-- index | |-- info/ | | `-- exclude | |-- objects/ | | |-- 9b/ | | | `-- 3a97dafadb12faf10cf1a1f3a32f63eaa7220a | | |-- info/ | | `-- pack/ | `-- refs/ | |-- heads/ | `-- tags/ `-- foo.txt
在objects
文件夹中,多出了一个名为9b
的文件夹,而且里面有一个名为3a97dafadb12faf10cf1a1f3a32f63eaa7220a
的文件,咱们看看这个文件是什么呢
看起来是个二进制文件,其实这个就是咱们的源码了😂,只是被压缩处理了
git 对每次添加进缓存区的文件,都会执行一次 defete 压缩而后存储
咱们执行一下这个命令,这个命令的功能是将blob 12\0this is foo
这个字符串,用 openssl 中的 sha1 算法计算摘要
echo "blob 12\0this is foo" | openssl sha1
发现了什么,生成的摘要结果(9b3a97dafadb12faf10cf1a1f3a32f63eaa7220a)和咱们目录(9b)以及文件名(3a97dafadb12faf10cf1a1f3a32f63eaa7220a)加起来的字符串同样
git 会将文件以blob 文件长度(单位B)\0文件内容
进行计算 sha1,将获得的结果,前两位做为文件夹名称,剩下的部分做为文件名,将咱们的文件存储在objects
目录下
好了,咱们基本知道了,git 是如何存储咱们的文件了,那咱们提交一次代码,看会有什么变化
咱们也可使用 git 内置的一个查看命令,来查看对应的提交文件
# 查看 object 的类型 > git cat-file -t 9b3a97dafadb12faf10cf1a1f3a32f63eaa7220a blob # 查看 object 的类型,后面的提交 hash 其实能够简写前面一部分,只要保证只能检索出一个文件便可 > git cat-file -t 9b3a blob # 查看 object 的内容 > git cat-file -p 9b3a this is foo
很常规,咱们就执行git commit -m "add foo.txt"
这个命令进行提交就行了,结果以下
|-- .git/ | |-- COMMIT_EDITMSG | |-- HEAD | |-- config | |-- description | |-- hooks/ | | `-- pre-commit.sample* | |-- index | |-- info/ | | `-- exclude | |-- logs/ | | |-- HEAD | | `-- refs/ | | `-- heads/ | | `-- master | |-- objects/ | | |-- 82/ | | | `-- be13e5bf9fafe4db5ad38c76a5c0116e156953 | | |-- 9b/ | | | `-- 3a97dafadb12faf10cf1a1f3a32f63eaa7220a | | |-- f8/ | | | `-- aa85021bfe8c10d9517e22feda4fc67d0a4095 | | |-- info/ | | `-- pack/ | `-- refs/ | |-- heads/ | | `-- master | `-- tags/ `-- foo.txt
文件夹瞬间多了几个文件,objects 目录中多出了两个文件夹82
、f8
以及相应的文件,refs
文件夹中则是多出了一个名为master
的文件,他就是指向咱们当前的分支啦
查看一下refs/headers/master
文件里面内容是什么
> cat .git/refs/heads/master 82be13e5bf9fafe4db5ad38c76a5c0116e156953
是一个 hash,根据这个hash,咱们可以在objects
中找到对应的文件了,使用git cat-file
查看一下
> git cat-file -t 82be13e5bf9fafe4db5ad38c76a5c0116e156953 commit > git cat-file -p 82be13e5bf9fafe4db5ad38c76a5c0116e156953 tree f8aa85021bfe8c10d9517e22feda4fc67d0a4095 author Aiello <aiello.chan@gmail.com> 1577369635 +0800 committer Aiello <aiello.chan@gmail.com> 1577369635 +0800 add foo.txt
说明这个文件是一个commit
类型的文件,文件内容则是指向了一个类型为tree
的文件,咱们继续顺藤摸瓜,去找这个tree
类型的文件包含了什么信息
> git cat-file -t f8aa85021bfe8c10d9517e22feda4fc67d0a4095 tree > git cat-file -p f8aa85021bfe8c10d9517e22feda4fc67d0a4095 100644 blob 9b3a97dafadb12faf10cf1a1f3a32f63eaa7220a foo.txt
能够看到,这个tree
类型的文件,指向了咱们的源文件,咱们经过以前相同的方法进行一次 hash 计算试试
# 获取提交的类型 > git cat-file -t f8aa85021bfe8c10d9517e22feda4fc67d0a4095 tree # 获取文件内容 > git cat-file tree f8aa85021bfe8c10d9517e22feda4fc67d0a4095 100644 foo.txt�:������ ��/c� # 计算内容的字节数 > git cat-file tree f8aa85021bfe8c10d9517e22feda4fc67d0a4095 | wc -c 35 # 合起来计算 # 按照 类型 字节数\0内容 的公式计算 > (printf "tree %s\0" $(git cat-file tree f8aa85021bfe8c10d9517e22feda4fc67d0a4095 | wc -c); git cat-file tree f8aa85021bfe8c10d9517e22feda4fc67d0a4095) | openssl sha1 f8aa85021bfe8c10d9517e22feda4fc67d0a4095
会发现,和咱们输入的 hash 值同样! 那咱们用一样的方法手动计算一下 commit 的 hash 值呢:
> (printf "commit %s\0" $(git cat-file commit 82be13e5bf9fafe4db5ad38c76a5c0116e156953 | wc -c); git cat-file commit 82be13e5bf9fafe4db5ad38c76a5c0116e156953) | openssl sha1 82be13e5bf9fafe4db5ad38c76a5c0116e156953
和预期同样,输出了咱们在目录树中看到的 hash 值,因此,在 git 中,始终使用了这套计算公式来计算 hash:
类型 字节数\0内容
咱们修改一下当前的文件,并提交
> cat foo.txt this is foo and new line
执行git add
后,咱们的目录树变成了这样
|-- .git/ | |-- COMMIT_EDITMSG | |-- HEAD | |-- config | |-- description | |-- hooks/ | | `-- pre-commit.sample* | |-- index | |-- info/ | | `-- exclude | |-- logs/ | | |-- HEAD | | `-- refs/ | | `-- heads/ | | `-- master | |-- objects/ | | |-- 82/ | | | `-- be13e5bf9fafe4db5ad38c76a5c0116e156953 | | |-- 9b/ | | | `-- 3a97dafadb12faf10cf1a1f3a32f63eaa7220a | | |-- c9/ | | | `-- da32f4e76824497d02312e46ac0a40e28bef91 | | |-- f8/ | | | `-- aa85021bfe8c10d9517e22feda4fc67d0a4095 | | |-- info/ | | `-- pack/ | `-- refs/ | |-- heads/ | | `-- master | `-- tags/ `-- foo.txt
多出了一个名为c9
的文件夹,以及相关文件,其实咱们用git cat-file
就能够看到,这个就是咱们当前的文件
> git cat-file -p c9da32f4e768 this is foo and new line
这说明,git 根据咱们的最新文件,又建立了一个新的压缩记录。这个压缩文件中是全量的 foo.txt,就算删掉以前的9b3a97dafadb12faf10cf1a1f3a32f63eaa7220a
文件,对当前的文件也是没有任何影响的,咱们依然可以得到最新版的全量文件!只是删除之前的记录后,回滚就会出现问题。
接着咱们提交这个改动
> git commit -m "update foo.txt" [master 432a5d9] update foo.txt 1 file changed, 1 insertion(+)
而后看咱们的目录树
|-- .git/ | |-- COMMIT_EDITMSG | |-- HEAD | |-- config | |-- description | |-- hooks/ | | `-- pre-commit.sample* | |-- index | |-- info/ | | `-- exclude | |-- logs/ | | |-- HEAD | | `-- refs/ | | `-- heads/ | | `-- master | |-- objects/ | | |-- 03/ | | | `-- 7b10984589cbfe8c6a9c5b84d61592f84fd97a | | |-- 43/ | | | `-- 2a5d918414362c78da50274a1fa0c57b5dc380 | | |-- 82/ | | | `-- be13e5bf9fafe4db5ad38c76a5c0116e156953 | | |-- 9b/ | | | `-- 3a97dafadb12faf10cf1a1f3a32f63eaa7220a | | |-- c9/ | | | `-- da32f4e76824497d02312e46ac0a40e28bef91 | | |-- f8/ | | | `-- aa85021bfe8c10d9517e22feda4fc67d0a4095 | | |-- info/ | | `-- pack/ | `-- refs/ | |-- heads/ | | `-- master | `-- tags/ `-- foo.txt
和预期的同样,生成了43
和03
文件夹,分别存放了此次的commit
记录和tree
记录
> git cat-file -t 432a commit > git cat-file -t 037b tree
根据上面的一些操做,咱们对 git 的整套存储机制已经有了很是清晰的了解,咱们能够大胆的画出下面这个模型图
注意:图中我并无写出每一个文件的hash,可是要知道,每一个层中的每一个文件都有惟一的一个 hash 做为文件名,因此永远不会存在两个内容相同的文件(就算你的commit信息同样,可是其所对应的 tree 和 blob 的 hash 都会是不一样的)!
git 中每一级都是很清晰的分层,各司其职,而后经过 hash 把他们连接起来!
从图中咱们能够看到,当前咱们所在的 HEAD 是指向的master
分支,而master
分支又指向某一个指定的提交记录add bar.txt
,该提交记录又指向提交时的一个文件目录树,这个树则继续指向了当时的文件!
这种存储方式清晰,可靠,扩展性强,在真实文件层,一样内容的文件,只会存储一份(由于 blob hash 的计算只和文件内容相关,因此一个文件修改后提交,会增长一个新的 blob,若是将其修改回来,再提交,并不会建立新的 blob,而是复用以前的,由于他们的内容都是如出一辙的,就算文件名不一样,但内容彻底相同的文件,也会只存储一份!)
其实经过图中能够看到,新建或者切换分支,咱们要作的仅仅是新建一个 branch 文件,而后将该文件指向咱们指定的 commit 便可,就是这么简单可靠。
至于 git 的回滚、rebase、merge 等等操做,如今看起来就清晰不少了吧
文章开头写的,hooks 能够帮咱们作不少事情,咱们以前还留有一个叫pre-commit.sample
hooks 没有删除,咱们就将其拿来用一下
(由于该文件中有一些示例,若是你不想清空,那就复制一份该文件,并将其末尾的 .sample 去掉)
> copy .git/hooks/pre-commit.sample .git/hooks/pre-commit
编辑.git/hooks/pre-commit
文件,将其清空,并输入下面的代码
#!/bin/sh echo "this is pre-commit hooks"
则你在下一次提交的时候,控制台会打印出上面的文字,在这里面,能够写上一些 lint 的代码,若是 lint 经过,则容许提交,而后在pre-push
这个 hooks 中运行本地测试,经过才容许提交到服务端,等等(以前在作组件库的时候,咱们还用 hooks 自增小版本,哈哈哈,很是方便,也不会有小版本失控的状况,由于更新中版本的时候,会手动重置小版本
如今已经有不少库可以帮助你执行 hooks 了,如husky
🎉