Git 如何存储数据的

Git 如何存储数据的.md

Git 使咱们天天使用得最多的工具之一,它是 linux 内核的最先做者 Linus Torvalds 建立的全新版本控制工具,和 linux 同样的简单好用,这篇文章就简单地讲一下 git 是如何工做的linux

要求

  • 知道 Git
  • 摘要算法的做用(MD五、sha-1
  • 一些简单的 Linux 命令

你将了解

  • .git 目录结构
  • git 如何存储文件

Demo 中用到的工具

  • VS Code:一款通用编辑器
  • iTerm:macOS 上的命令行软件
  • watch:类 Unix 上的命令行工具,能够定时重复执行指定命令并输出
  • tree:类 Unix 上的命令行工具,能够输出指定目录的目录树结构

本地配置

1. 在本地安装 Git

https://git-scm.com/下载安装便可git

2. 先在任意位置建立一个空文件夹

我在个人桌面上建立了一个名为 git_demo 的文件夹github

3. 在命令行中切换到该目录下

4. 监控目录变化

我这里是直接拆分窗口,便于显示,并导航到一样的目录算法

在右侧窗口中,输入如下命令,用来监控目录变化缓存

watch -d -n 1 tree --charset=ascii -aF

该命令综合了两个工具,一个是watch一个是tree,经过 watch 来定时(上面指定每隔 1 秒)执行一次后面的命令,咱们就可以看到实时的目录变化了bash

补上一个动图版本编辑器

开始 Demo

1. Git init

在左侧命令行中执行熟悉的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.
  • hooks 这里存放本地的 git hooks

    他的做用就是在你执行一些特定 git 命令(好比 git commit)的时候,顺带执行你指定的 bash 命令,很强大的功能。好比咱们想在提交前执行代码测试,咱们就能够去编辑 hooks 里面的pre-commit.sample这个文件,而且去掉文件名末尾的.sample,那在你下次执行git commit的时候,就会先运行这个 bash,只有当整个 bash 执行成功后(退出码为 0),才会执行你刚才的git commit.

  • info 存放一些默认的配置文件

    默认的已经存放了一个名为exclude的文件,其实就是一个.gitignore文件

  • objects 这里真实地存放着咱们代码的备份,一下子会详细说明的
  • refs 这个也是很是关键的一个文件夹,存放着咱们全部的分支信息

这个文件夹内,最重要的就是HEAD文件,objects文件夹,refs文件夹,git 经过先访问HEAD文件,找到咱们当前工做的分支,而后再去refs文件夹中找到对应的分支描述,这个描述文件正是指向了objects文件夹内的某一个文件,再经过这个文件,咱们就能获得当前分支当前版本的全部源码了!

上面看了,HEAD文件其实就只有一行,指向了refs目录的一个文件

咱们就来探秘一下refs文件夹和objects文件夹究竟是在作什么

2. 新建一个文件

为了不过多的文件影响,咱们先将 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文件

3. 将文件添加进缓存区

其实就是咱们无比熟悉的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 是如何存储咱们的文件了,那咱们提交一次代码,看会有什么变化

4. 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

5. 进行一次 commit

很常规,咱们就执行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 目录中多出了两个文件夹82f8以及相应的文件,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内容

6. 修改当前文件并提交

咱们修改一下当前的文件,并提交

> 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

和预期的同样,生成了4303文件夹,分别存放了此次的commit记录和tree记录

> git cat-file -t 432a
commit
> git cat-file -t 037b
tree

7. 提早总结

根据上面的一些操做,咱们对 git 的整套存储机制已经有了很是清晰的了解,咱们能够大胆的画出下面这个模型图

注意:图中我并无写出每一个文件的hash,可是要知道,每一个层中的每一个文件都有惟一的一个 hash 做为文件名,因此永远不会存在两个内容相同的文件(就算你的commit信息同样,可是其所对应的 tree 和 blob 的 hash 都会是不一样的)!

git 中每一级都是很清晰的分层,各司其职,而后经过 hash 把他们连接起来!

从图中咱们能够看到,当前咱们所在的 HEAD 是指向的master分支,而master分支又指向某一个指定的提交记录add bar.txt,该提交记录又指向提交时的一个文件目录树,这个树则继续指向了当时的文件!

这种存储方式清晰,可靠,扩展性强,在真实文件层,一样内容的文件,只会存储一份(由于 blob hash 的计算只和文件内容相关,因此一个文件修改后提交,会增长一个新的 blob,若是将其修改回来,再提交,并不会建立新的 blob,而是复用以前的,由于他们的内容都是如出一辙的,就算文件名不一样,但内容彻底相同的文件,也会只存储一份!)

8. 分支切换

其实经过图中能够看到,新建或者切换分支,咱们要作的仅仅是新建一个 branch 文件,而后将该文件指向咱们指定的 commit 便可,就是这么简单可靠。

至于 git 的回滚、rebase、merge 等等操做,如今看起来就清晰不少了吧

9. 打包送的 hooks

文章开头写的,hooks 能够帮咱们作不少事情,咱们以前还留有一个叫pre-commit.samplehooks 没有删除,咱们就将其拿来用一下

(由于该文件中有一些示例,若是你不想清空,那就复制一份该文件,并将其末尾的 .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

🎉

相关文章
相关标签/搜索