欢迎访问7788车友汇

ca3561(深入Git:4个关键步骤解锁版本控制机制)

频道:生活常识 日期: 浏览:657

本篇文章主要面向对git的使用有一定了解的同学,通过对Git底层命令的介绍来理解git内部的工作机制,从而更好的学习并理解如何使用Git与为何是如此运作的。


基础知识

Git目录结构

当我们需要使用Git来进行版本控制时,第一步就是执行 git init 进行版本库的创建,此时Git会创建一个 .git 的目录,这个目录包含的git存储的所有的信息。这个目录目录的部分目录结构如下:


info:包含了一些用于存储和管理版本库元数据的文件,如exclude文件会配置不希望被追踪的文件或目录(类似.gitignore)config:项目特有的配置信息,比如用户的姓名、邮件、远程地址等object:包含了git中所有的对象,是Git用来存储项目历史的核心数据,我们后续会进行介绍refs:存储着指向数据的提交对象的指针HEAD:当前被检出的分支 我们在目录下还可能会发现如 descriptionhooks 等文件或目录,我们这次不讨论这些内容;Git的完整的目录结构与描述,可以阅读官方文档进行学习

存储方式

首先,我们需要知道的是,Git的核心部分是一个键值对数据库,你可以通过向Git插入任意类型的内容获得一个唯一键,并且通过该唯一键来取回对应的内容。存储的数据将保存在上一段中我们提到的object的文件夹(即对象数据库)中。我们可以尝试新建一个版本库并执行 git add 来演示效果


$ git init test$ cd test$ ls .git/objectsinfo pack$ echo "hello world" >> a.txt && git add .$ ls -R .git/objects3b info pack.git/objects/3b:18e512dba79e4c8300dd08aeb37f8e728b8dad.git/objects/info:.git/objects/pack:

我们可以看出,objects多了一个在Hash的前两位为文件夹名称(3b),其余38位作为文件名的文件来存储刚才我们添加的文件,而文件的内容则是将内容转化为二进制并压缩后生成的。


GIt提供了 cat-File 的命令通过传递hash来读取对应二进制文件的内容,比如当前的文件我们可以执行 git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad,此时会即会答应出文件原本的内容:hello world


git add

上一段中,我们使用的 git add 来演示了将项目存储进版本库的效果,现在我们通过介绍一些底层命令的使用来拆解分析 git add 的工作本质


保存内容

我们重新初始化一个版本库,新建相同的文件后,可以执行 git hash-object 向数据库插入一条数据,此时可以看到,object文件夹中也有了一个相同的文件


$ git init dismantle$ cd dismantle$ echo "hello world" >> a.txt && git hash-object -w ./a.txt$ ls -R .git/objects3b info pack.git/objects/3b:18e512dba79e4c8300dd08aeb37f8e728b8dad.git/objects/info:.git/objects/pack:

hash-object 默认仅会计算出对应的hash,我们通过添加 -w 参数来指明该命令不要只返回hash,还需要将内容写入数据库中


暂存区

那么问题来了,我们执行了两条不同的命令,同样都向数据库中插入了数据;那么这两条命令的区别在哪里呢?答案就是暂存(staged或index) 我们可以执行 git status 命令来查看当前文件的状态;可以看到 test 目录中,文件已经添加到了暂存区,而 dismantle 中并没有 我们可以执行 update-index 将文件添加至暂存区中;当一个文件还不在暂存区时,需要添加 --add 参数,同时通过 --cacheinfo 来指定需要添加到暂存区的文件的类型[^2]、hash、文件名


```shell$ cd test && git status -sA a.txt$ cd dismantle && git status -s?? a.txt$ git update-index --add --cacheinfo 100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad a.txt$ git status -sA a.txt

当我们将文件添加至暂存区后,我们可以执行 statusdiff 等命令查看返回的结果,我们可以看到两个目录下,返回的结果是相同的


对于暂存区或Git提交操作不太了解的同学,可以查阅Git的官方的小册内容


git commit

存储对象

在我们聊commit的过程之前,我们需要先了解Git的存储对象;Git一共有四种类型的存储对象:数据(blob)、树(tree)、提交(commit)、标签(tag),我们本篇只讨论前三种。


我们先在 test工程中进行一次commit操作,此时,可以看到我们在objects下多了两个文件


$ git commit -m 'feat: 2.5'$ ls -R .git/objects3b eb f4 info pack.git/objects/3b:18e512dba79e4c8300dd08aeb37f8e728b8dad.git/objects/eb:aa691b5554f29ac9d4f37811a1da6f24d376a1.git/objects/f4:100ba8b3119f593a2b89c7284cf66d4be739b3.git/objects/info:.git/objects/pack:

如前半部分文章中我们通过 addhash-object 生成文件的内容就是一个数据对象,我们当时通过 git cat-file 可以查看对应的内容,可以看出其仅保存了文件的内容信息;这类型的对象我们称之为 数据对象。但是我们在开发一个项目的过程中,仅知道代码的内容肯定是不够的,我们还需要通过文件名来检索代码、管理依赖等,树对象就是来解决这个问题的。一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 hash,以及相应的模式、类型、文件名信息。例如,当前这次提交的生成树对象为


$ git cat-file -p ebaa691b5554f29ac9d4f37811a1da6f24d376a1100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad a.txt

此时我们通过树对象和数据对象可以还原出某个时刻下项目工程中的所有文件,那么如何将所有的提交串起来呢?显而易见的,提交对象就是来处理这个问题的。提交对象包含着一次提交的信息:当时树对象、父提交(如有)、作者信息、提交注释,一次提交的内容如下:


$ git cat-file -p f4100ba8b3119f593a2b89c7284cf66d4be739b3tree ebaa691b5554f29ac9d4f37811a1da6f24d376a1author gugu <gugu@gmail.com> 1697353561 0800committer gugu <gugu@gmail.com> 1697353561 0800feat: 2.5

我们可以通过提交对象来将所有的commit串起来,再通过对应的树对象和数据对象,检索出对应提交时所有的内容


我们在上文提到 cat-file 可以打印出对象的内容,其实此命令也可以打印对象的类型,只需要将 -p 替换为 -t 即可,大家可以自己尝试,本文不再赘述


生成树对象和提交对象

add 相同,我们也可以调用GIt的底层命令来自己完成commit这个操作,首先我们可以通过 write-tree 来生成一个树对象


$ cd dismantle && git write-treeebaa691b5554f29ac9d4f37811a1da6f24d376a1$ git cat-file -p ebaa691b5554f29ac9d4f37811a1da6f24d376a1100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad a.txt

可以看到,我们生成了一个与test项目中一模一样的树对象,然后我们再通过 commit-tree 来进行代码的提交,生成一个提交对象


$ git commit-tree ebaa691b -m 'feat: 2.5'f1ded58d3f850515daa3636efce0598bbe9a1180$ git cat-file -p f1ded58d3f850515daa3636efce0598bbe9a1180tree ebaa691b5554f29ac9d4f37811a1da6f24d376a1author gugu <gugu@gmail.com> 1697354831 0800committer gugu <gugu@gmail.com> 1697354831 0800feat: 2.5

提交对象的内容除了提交的时间,其余的内容都是与test项目中的相同(此时或许你会有疑问,为什么这次生成的hash都是不同的,没关系,我们最后再来说这个问题)


分支

那么我们的提交操作到此就结束了吗?给大家5秒钟的时间来思考这个问题 。。。。。。细心的小伙伴肯定发现了,不对,两者还有差异,我 git log 怎么报错呢?


$ git log masterfatal: ambiguous argument 'master': unknown revision or path not in the working tree.Use '--' to separate paths from revisions, like this:'git <command> [<revision>...] -- [<file>...]'

从报错中可以得处,我们竟然还没有master分支?这不科学!大家是否有思考过,上文中我们可以通过存储对象来得到整个项目的结构、内容等,但是在切换分支的时候又是怎么做到同样的事情呢?项目中分支的信息又是存储在哪的呢?这里就得说一下,分支的本质即为指向一系列提交之首的一个引用,其信息会保存与 refs/headers 下,以分支名为文件名,提交的 hash 为内容的文件 也就是说,我们距离生成一次提交,还剩下更新分支,将引用指向最新的提交


$ git update-ref refs/heads/master f1ded58d3f850515daa3636efce0598bbe9a1180$ ls .git/refs/headsmaster$ cat .git/refs/heads/masterf1ded58d3f850515daa3636efce0598bbe9a1180

至此,完整的一次提交便结束咯


补充

对象文件的生成规则

还记得我们上文中的那个问题嘛?为什么两个项目中只有那个提交对象的文件名是不同的呢?我们来看一下Git是如何生成对象文件的hash和二进制内容的吧 Git会先生成一个以对象类型开头,随后加一个空格和内容的字节数,最后是一个空字节的头部信息;将头部信息和文件的内容拼接后进行 SHA-1 校验和得出的hash值即为对象文件的名称,通过 zlib 压缩得到的信息作为文件的内容 下方的node代码模拟的hash和内容的生成过程,并通过 Git 的命令进行验证,逻辑无误


const { deflateSync } = require('zlib');const crypto = require('crypto');const fs = require('fs');const addFile = (content) => { const headers = `blob ${content.length}0`; const shasum = crypto.createHash('sha1') shasum.update(headers content) const hash = shasum.digest('hex') console.log('hash: %s', hash); // e0501eec17daa40898f8340ca52af1949852025e const deflatedContent = deflateSync(headers content); const dirname = hash.slice(0, 2); const fileName = hash.slice(2); if (!fs.existsSync(`.git/objects/${dirname}`)) { fs.mkdirSync(`.git/objects/${dirname}`, { recursive: true }); } fs.writeFileSync(`.git/objects/${dirname}/${fileName}`, deflatedContent, { encoding: 'hex' });}addFile("this is a demo");

$ echo -n "this is a demo" | git hash-object --stdine0501eec17daa40898f8340ca52af1949852025egit cat-file -p e0501eec17daa40898f8340ca52af1949852025ethis is a demo

总结

我们先借用git book的一张图,来总结我们的整个数据库的结构




作者:陆晨杰


来源:微信公众号:Goodme前端团队


出处:https://mp.weixin.qq.com/s/YQW5X8SbVosMRFigyMKniA


0 留言

评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。