前言
git
是每一个程序员必须熟练使用的一个工具,但是在当前这个浮躁的社会,特别是正在大发展的前端领域,大家似乎只是在乎怎么使用,而并不想去了解内部的实现原理,虽然对于大部分的第三方包来说我们确实不用去在意它的实现细节,但是对于git
这个陪伴我们无数日日夜夜的工具来说还是有必要揭开她的外衣一探究竟的。
本文将以一个空项目开始的方式一步一步去探索git的内部实现逻辑。
重点概念
SHA-1
SHA-1 是一种不可逆的加密算法,SHA-1可以将不固定大小的数据生成为一个被称为消息摘要的160位(20字节)散列值,散列值通常的呈现形式为40个十六进制数。
Git分区
Workspace:工作区
Index / Stage:暂存区
Repository:本地仓库
Remote:远程仓库
Git Objects对象类型
-
blob
:保存了提交文件的数据快照 -
tree
:保存了本次提交的所有文件的blob
对象信息 -
commit
:保存了本次提交的信息和tree
对象信息
以上三种是比较重要的对象类型
目录结构
我了解一个项目都是从目录结构开始的,所以学习git
我们也是从它的目录结构入手
#新建空项目并初始化
$ git init
#进入.git文件可以看到目录结构
│ config #存放一些仓库的配置信息
│ description #存放仓库的描述信息,主要给git托管系统使用
│ HEAD #本地映射到refs文件的引用,能够找到下一次commit的前一次哈希值(远程分支为ORIG_HEAD)
│
├─hooks #存放一些shell脚本
│
├─info #存放一些仓库信息
│ exclude
│
├─objects #存放git的各种类型object
│ ├─info
│ └─pack
└─refs #保存引用的相关信息
├─heads
└─tags
这里重点要关注的是 HEAD、objects、refs。
GIT储存原理
1. 文件快照储存
新建test.txt
并写入内容 hello git!,执行git add .
,可以发现objects
文件夹发生了变化:
objects:.
├─27
│ 706f8151e4c44bb7a129d64b35fff3422d5e3a #新增文件
│
├─info
└─pack
27 和 706f8151e4c44bb7a129d64b35fff3422d5e3a拼接在一起刚好是40位,这个就是根据你的文件由SHA1
算法生成的40位16进制哈希值,而这个文件就是用来保存你的文件数据快照,你的文件有多大,它就有多大。
这个文件是二进制文件,无法直接查看,但可以通过git
提供的命令进行查看:
$ git cat-file -t 27706f8151e4c44bb7a129d64b35fff3422d5e3a #查看object类型
blob
$ git cat-file -p 27706f8151e4c44bb7a129d64b35fff3422d5e3a #查看object数据
hello git!
这时我们还会发现根目录下也新增了一个index
文件,也是二进制文件,这个是管理暂存区的文件,同样我们可以使用git
提供的命令进行查看:
$ git ls-files --stage #查看index文件
100644 27706f8151e4c44bb7a129d64b35fff3422d5e3a 0 test.txt
这个文件保存了我们暂存文件的blob
对象的哈希值,也可以理解为保存了一个指向这个对象的指针。
$ git reset HEAD -- . #取消暂存区文件
$ git ls-files --stage #查看index文件发现没有任何输出值
2. 提交信息管理
$ git commit -m '首次提交'
[master (root-commit) e895c99] 首次提交
1 files changed, 1 insertions(+)
create mode 100644 test.txt
objects
文件夹发生了变化:
objects:.
├─27
│ 706f8151e4c44bb7a129d64b35fff3422d5e3a
│
├─98
│ b241e3ee5f307af72f5aaafb154dbfb54c3a30 #新增文件
│
├─e8
│ 95c99416c3adf72869db580b4c729891f27d0d #新增文件
│
├─info
└─pack
新增了两个哈希命名的文件,老规矩,我们先查看一下
$ git cat-file -t 98b241e3ee5f307af72f5aaafb154dbfb54c3a30
tree #tree类型object
$ git cat-file -p 98b241e3ee5f307af72f5aaafb154dbfb54c3a30
100644 blob 27706f8151e4c44bb7a129d64b35fff3422d5e3a test.txt
#tree对象保存了本次提交的相关文件快照对象的信息
$ git cat-file -t e895c99416c3adf72869db580b4c729891f27d0d
commit #commit类型object
$ git cat-file -p e895c99416c3adf72869db580b4c729891f27d0d
tree 98b241e3ee5f307af72f5aaafb154dbfb54c3a30
author username <xxx@qq.com> 1606826893 +0800
committer username <xxx@qq.com> 1606826893 +0800
首次提交
#commit对象保存了本次提交的作者、提交者、提交时间、提交附带信息以及最关键的tree对象的信息
这里我们已经发现了commit -> tree -> blob
的关系,那谁指向commit
呢?
这时我们可以发现refs
文件夹也发生了变化,heads
文件夹下多了个master
文件,内容为本次更新的哈希值:
e895c99416c3adf72869db580b4c729891f27d0d
而根目录下的HEAD
文件内容又为:
ref: refs/heads/master
这样我们就建立了从HEAD -> master -> commit -> tree -> blob
的完整关系链。
而根目录下新增的logs
文件夹,跟踪了各个分支上的所有操作,其实从里面的文件我们可以更加直观的看到我们进行的一些git
操作
logs:.
│ HEAD
│
└─refs
└─heads
master
#此时 HEAD 和 master 文件保存了同样的信息
0000000000000000000000000000000000000000 e895c99416c3adf72869db580b4c729891f27d0d username <xxx@qq.com> 1606826893 +0800 commit (initial): 首次提交
#前两个哈希值表示前一次提交和本次提交的commit对象哈希值,后面是用户名,用户邮箱,提交时间,提交附加内容
接下来我们对test.txt
进行二次修改并执行git add .
和git commit
操作
#logs下的HEAD文件很明显的记录了我们的操作
0000000000000000000000000000000000000000 e895c99416c3adf72869db580b4c729891f27d0d username <xxx@qq.com> 1606826893 +0800 commit (initial): 首次提交
e895c99416c3adf72869db580b4c729891f27d0d 8f15afaae122a0eeaabc04fb1dc3ab36e3ecbb90 username <xxx@qq.com> 1606828104 +0800 commit: 第二次提交
我们知道8f15afaae122a0eeaabc04fb1dc3ab36e3ecbb90
是本次的commit
类型对象
#查看commit对象内容
$ git cat-file -p 8f15afaae122a0eeaabc04fb1dc3ab36e3ecbb90
tree 910e967c436b0824e4ac0aebd4963c64bdd5f31b
parent e895c99416c3adf72869db580b4c729891f27d0d
author username <xxx@qq.com> 1606828104 +0800
committer username <xxx@qq.com> 1606828104 +0800
第二次提交
可以看到这次的commit
对象不仅仅记录了本次的tree
对象,也记录了上次的commit
对象,这样也就保持了与上次commit
的所有信息的联系,以此类推,多少次的提交我们也都可以像链表一样将它们联系起来。
#查看tree对象内容
$ git cat-file -p 910e967c436b0824e4ac0aebd4963c64bdd5f31b
100644 blob cb7a44021ad5013a4620857a2d67f4db9ca2bccb test.txt
这时我们就从tree
对象找到了本次提交的文件快照信息。
3. 分支管理
新建一个分支
$ git branch test_branch
#logs文件夹发生了变化
logs:.
│ HEAD
│
└─refs
└─heads
master
test_branch #新增文件
#查看test_branch
0000000000000000000000000000000000000000 8f15afaae122a0eeaabc04fb1dc3ab36e3ecbb90 username <xxx@qq.com> 1606828668 +0800 branch: Created from master
#记录了本次的新建分支操作和master分支的最后一次提交的commit对象
#同样的,refs文件夹的heads也新增了test_branch文件
8f15afaae122a0eeaabc04fb1dc3ab36e3ecbb90
#内容为最新一次提交的commit对象
执行分支切换
$ git checkout test_branch
#根目录下的HEAD文件
ref: refs/heads/test_branch
#指向了我们的当前分支
修改分支上的文件进行提交
#logs下的test_branch
8f15afaae122a0eeaabc04fb1dc3ab36e3ecbb90 67e35930423b61cbb09d90dd47e0e16bf0abbdb3 username <xxx@qq.com> 1606829497 +0800 commit: 分支提交
然后切换为主分支进行合并
$ git merge test_branch
Updating 8f15afa..67e3593
Fast-forward
hello.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
#logs下的master
8f15afaae122a0eeaabc04fb1dc3ab36e3ecbb90 67e35930423b61cbb09d90dd47e0e16bf0abbdb3 username <xxx@qq.com> 1606829717 +0800 merge test_branch: Fast-forward
#refs下的master
67e35930423b61cbb09d90dd47e0e16bf0abbdb3
#内容也更新为了test_branch分支上的最后一次提交信息
4. 小结
经过以上操作我们不难发现,git
其实是通过commit -> tree -> blob
这样的三类对象组成的关系链来储存每一次的提交信息的,而commit
对象与commit
对象也构成了一条关系链,这样就把无数个提交信息联系了起来,而HEAD
用来指示当前的工作分支,并在refs
文件夹中找到这个分支的指向的最后一个commit
对象。
我们平时的提交变更、切换分支等操作其实也就是改变了这其中的指向而已。
顺带一提这种哈希链的储存方式也可以很好的进行防篡改,如果你改了其中一个文件,那么文件的哈希值变了,tree
的哈希值也需要变,跟着commit
的哈希值也需要变,你必须改掉整个仓库的关系链。
GIT常用操作指令
1. 初始化仓库
#初始化代码仓库
$ git init
#从远程仓库拉取代码仓库,默认拉去master分支
$ git clone [url]
#从远程仓库拉取指定分支
$ git clone [url] -b [branch]
2. 工作区操作
#查看git当前文件状态,包含工作区和暂存区
$ git status
#还原指定变更文件
$ git restore [file1] [file2] ...
#还原工作区所有变更文件
$ git restore .
2. 暂存区操作
#添加指定更改文件到暂存区
$ git add [file1] [file2] ...
#添加所有更改文件到暂存区
$ git add .
#撤销暂存区指定文件(HEAD不区分大小写)
$ git restore --staged [file1] [file2] ...
#撤销暂存区所有文件
$ git restore --staged .
3. 仓库区操作
#提交指定暂存区文件到仓库区
$ git commit [file1] [file2] ... -m [message]
#提交所有暂存区文件到仓库区
$ git commit -m [message]
#查看最近提交信息,可得到commit哈希值
$ git log
#回退一个版本,提交的文件会回到暂存区
$ git reset --soft HEAD~1
#回滚到指定的commit版本
$ git reset --hard [commit_id]
#用新的commit回滚到指定的commit版本,此次回滚和之前的commit都会保留
$ git revert [commit_id]
4. 分支操作
#列出所有本地分支,-r列出所有远程分支,-a列出本地和远程所有分支
$ git branch
#新建一个分支
$ git branch [branch]
#切换到指定分支
$ git checkout [branch]
#从指定分支新建一个分支,并切换到该分支
$ git checkout -b [branch] [orgin_branch]
#合并指定分支到当前分支
$ git merge [branch]
5. 标签操作
#列出所有tag
$ git tag
#新建一个tag在当前commit
$ git tag [tag]
#查看tag信息
$ git show [tag]
#推送指定tag到远程
$ git push origin [tag]
#删除本地tag
$ git tag -d [tag]
#删除远程tag
$ git push origin :refs/tags/[tagName]
6. 远程仓库操作
#显示所有远程仓库
$ git remote -v
#拉取远程仓库指定分支,与本地分支进行合并
$ git pull orgin [branch]
#上传指定本地分支到远程仓库
$ git push orgin [branch]
总结
在这个IT行业快速的时代,大部分的需求都是有现成的工具和解决方案可以参考和使用,但是大部分人往往只是局限在于用上,并不会想着去了解实现原理,更不会有想法去优化它,这其实是一个很危险的信号,会使用这些工具是一方面,掌握这些工具的实现思想才是最重要的一点。