深入探究 Git 的内部机制, 学习并精通 Git
简介
Git无疑是现代软件开发的主要基石之一 . 它是协调开发人员工作的必备工具箱, 多年来已成为开源运动的基本引擎. 一个简单的例子是, 截至2021年11月, 根据Git的主要仓库管理器GitHub报告可知, 已有超过7300万名开发人员和2亿多个仓库.
一些程序员每天都要与Git打交道, 并普遍应用其中的关键概念. 在本讲座中, 我们将通过深入Git内部, 探索Git的基本基础, 继续向前迈步. 什么是分支? 什么是分支头? 合并分支意味着什么? 今天, 我们将回答这些问题以及其他问题.
打地基
Blob, 树和提交 是Git数据结构的主要组成部分. 就像房子由砖块砌成, 图由边和节点组成一样, 这些元素构成了Git的地基.
要理解这一切, 让我们从一个例子开始. 假设我们创建了一个空仓库. 当我们启动git init
命令时, git会自动创建一个名为.git
的隐藏文件夹, 用来存储内部信息.
Blob
现在, 假设我们创建了一个名为myfile.txt
的文件, 并使用git add myfile.txt.
命令将其添加到我们的版本库中.
当我们执行这个操作时, Git会创建一个blob , 这是个文件, 位于.git/objects
子文件夹中, 存储了 myfile.txt
的内容, 但**不包括任何相关元数据(如创建时间戳、作者等). 因此, 创建 blob 就像存储图片文件的内容一样.
Blob 的名称与其内容的哈希值**相关. 内容散列后, 前两个字符用于在*.git/objects*中创建子文件夹, 散列的其余字符则构成 blob 的名称.
总之, 向 Git 添加文件的步骤如下:
- Git 获取文件内容并对其散列
- Git 在
.git/objects
文件夹下创建一个blob
. 哈希值的前两个字符用于在该路径下创建一个子文件夹. Git 会在其中创建一个blob
, 其名称由哈希值的其余字符组成. - Git 会将原始文件(压缩版)的内容保存在
blob
中.
Git 创建blob
的过程描述
请注意, 如果我们有一个名为myfile.txt
的文件和另一个名为ourfile.txt
的文件, 而这两个文件**共享相同的内容, 它们的哈希值也相同, **因此它们被存储在同一个blob
中.
如果我们对myfile.txt
稍作修改, 并将其重新添加到版本库中, Git 也会执行同样的过程.
树
假设我们在版本库中创建了一个名为subfolder
的子文件夹, 也让我们在这个子文件夹中创建一个名为yourfile.txt
的文件, 并将其添加到版本库中. 在此过程中, Git 会根据我们在上一段中定义的流程为yourfile.txt
创建一个新的 blob.
Git会对第二个名为yourfile.txt
的文件进行散列, 该文件保存在.git/objects
文件夹中
此时, 我们使用git commit
命令提交myfile.txt
和yourfile.txt
:
- 创建仓库的根树
- 创建提交
让我们集中讨论第一步. 那么, 什么是根树呢? 根树存储了整个版本库的文件和文件夹结构 . 它是一个文件, 包含对版本库中每个 blob 或子文件夹的引用, 以递归方式建立.
根树的每一行都引用一个 blob 或其他子树, 这些子树又以同样的方式引用其他 blob 或其他子树 . 因此, 树相当于目录 : 就像我们可以从目录访问文件和子文件夹一样 , 我们也可以从树访问 blob 和子树.
根树和与mysubfolder
相关的子树的内容
一旦 Git 创建了根树和所有相关的子树, 它就会执行上文所述的散列和存储操作. 更准确地说, 它会对每棵树进行散列 , 并使用前两个字符在.git/objects
中创建一个子文件夹, 而其余的散列字符则构成保存文件的名称. 因此, 在这个过程中, 我们得到的新文件数量与数据结构中树的数量相同.
Git 会对根树和与mysubfolder
相关的子树进行散列, 两者都存储在.git/objects
文件夹中
提交
运行git commit
命令后, 第二步是创建提交. 提交内容存储在一个文件中, 其中包含与根树, 父提交(如果有)相关的信息, 以及一些元数据, 如提交者的姓名, 电子邮件地址和提交信息.
提交文件包含对根树的哈希值, 作者和提交者, 提交时间戳(本例中为 163267988), 父提交(本例中为空, 因为这是第一次提交)和提交消息的引用.
提交文件创建后, Git 会对其内容进行散列, 并使用散列名将内容存储到一个新文件中, 与上述操作完全相同(前两个字符构成.git/objects
中的子文件夹名称, 而散列名的剩余部分构成实际名称).
到目前为止所有树, 提交和 Blob 的结构
就是这样! 恭喜你, 你刚刚了解了 Git 的结构. 现在, 有了这些概念, 要定义分支, 标记, 头部和合并等概念就非常简单了!
垒砖墙
分支
分支是对提交的命名引用 . 例如, 当创建一个名为mybranch
的新分支时(使用git checkout -b mybranch
命令), Git 会在.git/refs/heads
路径下生成一个名为mybranch
的新文件, 该文件的内容是创建分支的提交的哈希值.
最初, master 和mybranch
都指向同一个提交
然后, 当我们在mybranch
上提交时, Git 会执行之前定义的操作(创建根树和提交文件), 然后用新的提交哈希值更新分支的文件.
执行新提交后, mybranch
文件的内容将被更新. 现在, mybranch
文件指向新的提交
因此, 分支是文件, 用于跟踪提交, 我们每次提交都会更新这些文件的内容.
标签
标签是对特定提交的永久引用 . 例如, 当我们创建一个名为mytag
的新标签时(使用git tag mytag
命令), Git会在.git/refs/tags
路径下生成一个名为mytag
的新文件.
不过, 当我们继续工作并在同一(或其他)分支上提交时, 标签文件不会更新, 而是继续指向其创建时的特定提交 . 与分支文件不同, 标签在执行新提交时不会移动.
执行了新提交, 但文件mytag
并未更新
HEAD
HEAD
在 Git 中执行一些任务:
- 因此, 当我们执行
git branch
时, Git 会通过HEAD
来了解我们所在的分支. - 它引用下一次提交的父提交, 因此
HEAD
指向的提交将是下一次提交的父提交. 回想一下, 当我们执行提交时, 的父提交会保存在提交文件中.
如果我们在分支 master 上, HEAD
就会引用该分支. 如果我们打开HEAD
文件, 就会看到"ref: refs/heads/master". 相反, 如果我们切换到mybranch
分支, 并打开.git
文件夹中的HEAD
文件, 我们会看到"ref: refs/heads/mybranch". 因此, HEAD
并不直接指向某个提交, 而是指向某个分支, 而该分支又指向该分支上的最新提交. 通过这种方式, Git 可以追踪当前已签出的提交.
我们在分支mybranch
上. HEAD
指向文件mybranch
, 而文件mybranch
又指向一个特定的提交. 与分支 master 相关的文件master
指向另一个提交
当我们在分支上执行提交时, Git 会读取HEAD
文件的内容, 并写入被引用为父提交的提交 . 从这个意义上说, HEAD
(间接)提供了下一次提交的父提交.
提交文件的内容. HEAD
(间接)提供了父提交
现在, 在 Git 中, 我们可以签出到前一个提交, 然后从那里开始修改. 这种模式被称为分离模式
. 在这种情况下, HEAD直接指向一个提交, 而不是分支 . 请注意, 这样做可能会有危险, 因为我们有可能丢失新的提交. 事实上, 在执行一次提交后, 如果我们签出到一个分支, 我们就无法再返回到这个新提交, 因为它没有被任何分支引用 ! 这就是为什么我们在分离模式下提交任何改动之前, 总是要创建一个新分支的原因!
Merge
Merge允许连接两个或多个提交. Merge有两种类型:
- 第一种是当两个分支发生分歧时. Git 会创建一个有两个父分支的新子分支 . 第一个父分支是我们所在的分支, 第二个父分支是将要Merge的分支. 提交文件将有两个父节点,
HEAD
被移动到新的子节点上. - 第二种情况是, 两个分支没有分叉, 但其中一个分支是另一个分支的延续. 在这种情况下, 合并被称为
快进合并
(fast-forward merge), 它不是真正意义上的合并, 因为没有冲突 . 在这种情况下, Git 只是把HEAD
和当前分支移到要合并分支的同一个提交点上.
就到这了. 恭喜你看到了这里! 希望你喜欢这篇文章! 现在, 你应该对 Git 的工作原理有所了解了. 如有任何疑问, 欢迎随时发表评论!
后会有期了, 要持续发光哦! :)