Git 的原理
Posted zhuo木鸟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Git 的原理相关的知识,希望对你有一定的参考价值。
文章目录
什么是 Git?
Git 是一个分布式的版本控制系统。他的工作原理类似于区块链,严格来说,他是一个对象数据库。
Git 通过版本库来管理数据(代码)。这里的版本库,指的是开发者、或者服务器中托管的项目文件夹。在后文中,亦用版本库指由一系列历史项目文件、当前项目文件组成的文件夹。
对于一个开发者来说,他电脑上的项目文件,就叫本地版本库。而其他开发者的项目文件(有可能一样),就叫远程版本库。一般的,虽然分布式版本控制系统中,各个版本库都是同等地位的。但是为了开发方便,一般都会有一个公认的服务器,维护一个“官方”版本库。当然,假若这个官方版本库丢失了,那么可以用任何一个开发者的版本库顶上。这个官方版本库,也叫项目版本库。
不过,一个开发者,为了调试的方便,都有多个本地版本库。其中一个作为主版本库。另外的几个,一般作为调试之用。这些调试的版本库,一般称为克隆版本库。
版本库是由什么构成的
我们知道,一个项目文件,肯定包含源代码、说明文档、日志等等。由 Git 维护的版本控制系统中,一个项目文件也叫版本库。并且,区分与一般的文件管理系统,版本库除了有当前项目文件外,还包含了历史的所有项目文件,也即项目文件的历史版本(以下简称历史版本)和当前版本(以下简称当前版本)。
当然,当前版本、历史版本,并不是通过直接存储里面的项目文件,而是将它们经过处理后,转换成哈希数值存储在一个对象数据库中。这个数据库,相当于文件管理系统管理项目时,所用到的日志。只不过,可以通过这个“日志”,将项目文件恢复到历史状态。我们后面也会叫这个**“日志”为对象数据库**。
这究竟是怎么做到的呢?我们将在下一节说明。目前的主要任务,还是了解一下什么是版本库吧。
不过,在此之前,必须说明的一点是,在诸多介绍和书籍中,也将这个日志数据库,称为版本库
。晕了?没关系,大家只要记住,版本库有时候是指项目文件夹的“日志数据库”(对象数据库)、也可以指项目文件夹。
注意,这里的术语,除了“日志数据库” 是笔者为了方便提出的之外,其余术语都来自官方资料
版本库(项目文件夹)的构成
版本库由:版本库(对象数据库)、工作区、缓存区构成。
其中工作区存储了实质的文件、以及文件目录。对象数据库,则存储了版本库的历史信息(历史版本等)。当然,他们都有相同的根目录,就是我们的版本库(项目文件夹)啦。
这样可能有点抽象,让我们看一个实际的例子:
其中, .git 就是我们的版本库(对象数据库)。而 folder1、folder2、ReadMe.md 就是我们的工作区了。
咦?缓存区呢?怎么没画出来。
缓存区又叫 index、暂存区。工作区中所做的每一次修改,并不会实时地、同步地更新给版本库。而是需要开发者手动提交,将修改弄成一个版本,存到缓存区中。然后,缓存区再正式地,将该版本加上适当的注释,添加到版本库中。
为什么要多此一举?我们先在此打住。在后续章节,我们将会仔细涉及。
版本库(对象数据库)的结构
前面提到:
工作区中所做的每一次修改,并不会实时地、同步地更新给版本库。而是需要开发者手动提交,将修改弄成一个版本,存到缓存区中。然后,缓存区再正式地,将该版本加上适当的注释,添加到版本库中。
这里有一个重要的概念,提交。但我们创建一个项目、并首次添加一些项目文件时,就会形成工作区。然后,我们需要用 git init 命令,生成一个空的版本库。
如果我们要将工作区所做的修改,包括文件的增、删、改、差等记录添加到版本库中,就要用到 git add xx [ xx 可以是所有文件、或指定的某些文件、文件夹],将 xx 作为一个版本,上传到缓冲区。然后,再用 git commit -m '注释'
将版本以及相应的注释,提交到版本库中。
一般的,我们修改了工作区,都必须以版本的形式,提交给数据库管理。
当然,版本不是工作区的简单复制。而是将工作区的文件、文件目录进行处理后,以哈希数值的形式,追加给版本库。
至此,大家应该能够理解版本库的构成了。版本库是由一系列版本构成的、以哈希值为内容的对象数据库:
图中,每一个节点就是一个版本。每一个节点,是由工作区(历史工作区,若版本不是最新版本),处理成哈希散列构成的。当然,每一个版本,都是一次提交。因此,有时候也称版本为提交。
版本库就是由提交构成的。这些提交,可能是基于上一次提交的提交,因此具有某些序列关系。
如何查看版本库的所有提交呢?可以用 git log [--oneline] \\[--graph] [-n number]
实现。
提交的组成
我们重复了三遍,提交由工作区(历史工作区,若版本不是最新版本),处理成哈希散列构成的。具体怎么做呢?
对于当前提交对应的工作区的所有文件、目录(文件夹),我们给它们取个别名叫 blob、tree:
- 对每一个 blob,我们将其内容根据 SHA1 算法,计算出一个 40 位的哈希值,用来代替其原始内容,存放在版本中。
- 对每一个 tree,我们将其内容(下属的 blob、tree 的文件名),通过 SHA1 算法,计算一个 40 位的哈希值,存储在版本中。
然后,将版本中的哈希值,以及提交时的元数据通过 SHA1 算法,计算出一个 Hash 值,作为该版本的标题。我们称为版本号。有时,也会称为提交哈希。其中,提交时的元数据,是指提交作者、提交时间、修改内容的作者、以及注释。
如何查看版本中的内容呢?
git cat-file -p 版本号
,会输出:
$ git cat-file -p HEAD
tree dcdef0feae480a65a74e82bfc84faf0dbdbe0bc0
parent b510c6998b678742b7e872edff4b7dc0dd638b55
author xxxxxxx <xxxxxx@qq.com> 1597471230 +0800
committer xxxxxxx <xxxxxxxx@qq.com> 1597471230 +0800
change to master branch
HEAD 的当前版本的版本号的快捷名字。
- 包括根目录:tree dcdef0feae480a65a74e82bfc84faf0dbdbe0bc0
- 上一个版本的根目录:parent b510c6998b678742b7e872edff4b7dc0dd638b55
- 修改者信息:author xxxxxxx xxxxxx@qq.com 1597471230 +0800
- 提交者信息(有可能不是同一个人):committer xxxxxxx xxxxxxxx@qq.com 1597471230 +0800
- 注释:change to master branch
为了查看某版本中的某个文件,存储了什么内容。我们可以用如下命令:
git cat-file -p 根目录的版本号
:
$ git cat-file -pdcdef0feae480a65a74e82bfc84faf0dbdbe0bc0
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 ReadMe.md
040000 tree 49b4dea4392ce90ecd5a1bc9e7b44362aac69d0e folder1
040000 tree a9f3cdba16734bb778a8203a9cb8c4754f3b0310 folder2
其中 100644 代表文件的权限。
我们也可以继续用 git cat-file -p 子目录哈希
来查看子目录的内容。(所需查询的文件的版本号可以逐级查询到)
当然,在逻辑实现上,虽然每一个版本看似存储了当前工作区的所有文件以及目录。但是,在物理上,版本库存储的版本,并非是相互独立、毫不相干的。也就是说,假设文件 f 在提交 A 中存储了,并在之后的提交中, f 并没有被更改(这点可以通过检测哈希值有无变化来判断),则之后的所有提交中,并不会实际存储 f,而单单存储一个引用。
SHA1 算法
SHA1 算法是一个加密算法。他可以将任意长度的输入,输出成固定长度的随机数(哈希值)。不过,同样的输入,可以输出同样的哈希值。这个算法的输出,总共有 2160 种。因此,虽然可能不同的输入,有相同的哈希值。但是这个概率微乎其微,因此认为不同的输入,都有一个独一无二的哈希值。
实际上,区块链也是一样。区块链用的是一个叫 SHA256 的算法。
现在,请思考,用哈希值来替代实际文件,有什么好处呢?为什么要这样做呢?
- 由于哈希值可以大致认为:不同输入,得到的哈希值绝对是独一无二的。因此,我们不用当心哈希值重复。这样有什么好处呢?此时,我们在本地版本库中,修改文件时,就不用去对照远端的项目版本库,有无重复的哈希。也就是说,我们可以离线工作
- Git 本质上讲是数据库。谈到数据库,就必须知道搜索速度的问题。如果采用哈希值,就可以用哈希表来存储数据。从而,将数据的访问时间降为常数时间。
- 若修改了文件内容,那么相应的哈希值也会变化。也就是说,我们可以每隔一段时间,就算一下文件的哈希值,并与原先的哈希值对比,从而判断文件是否被恶意篡改。
- 用哈希值代替文件进行存储,很明显,能够降低大小,节省存储空间。
- 避免冗余存储。假设有一个文件,文件名不同,但内容相同。那么其算出的哈希值,肯定也是相同的。而系统只会去存储一个哈希值。这样做,可以大大降低冗余导致的存储浪费。
- 重命名检测算法,实现文件的移动、重命名等操作。
这里具体解释一下重命名检测算法:首先 Git 所存储的文件名,不是单个文件的名字,而是以版本库为根目录,描述的整个相对路径。如果一个文件进行了移动,并将操作提交到版本库后。Git 首先会认为原文件被删除了,然后在检查提交中有没有与其内容相同(可以通过判断哈希值来鉴别)的文件。若有,则直接认为是重命名操作。当然,这里的重命名,不是传统意义上的更改文件名,而是更改带有文件名的那个相对路径。
总结
版本库(项目文件夹)的构成如下:
版本库的节点(即一个提交)的构成如下:
在逻辑上,一个节点(版本)存储了相应工作区的所有文件、文件目录、以及元数据的哈希值。在物理上,有些从未被修改过的文件,只保存一次。
每一个版本,都有一个哈希值,称为版本号。这个版本号用作为版本的引用。
每一个版本中的 blob、tree 都有独一无二的哈希。该哈希值与其内容有关。可以实现快速访问、压缩内存、避免重复存储、以及自动检错等优点。
版本库的最新的节点,一般都落后或者同步于工作区中的文件,且版本的建立,需要手动进行提交。提交的过程是,先提交给缓存区、再提交给版本库。
版本库相当于存储了项目文件的“历史”。
暂存区
缓存区也叫暂存区、索引(Index),后文统一称之为暂存区。建立一个提交,首先要将工作区中的文件,上传到暂存区中。再用 git commit 命令,将文件提交,形成一个新版本追加到版本库中。
什么样的文件,不应该上传到版本库中?
- 调试用的实验修改
- 意外修改
- 尚未准备好的修改
- 自动生成的文件
因此,在提交文件是,不能直接用 git add .
或 git add -all
将工作区中的文件都上传到暂存区中。而是要有选择的挑选文件上传,即 git add 所要上传的文件或文件夹
。
查看暂存区
我们可以用 git status [--short]
查看工作区中文件的工作状态,以查看那些文件已经被添加到存储区中。例如:
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: my_code.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: test_text.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
his_code.txt
可以看到,工作区的文件有如下三种状态:
- 已经在暂存区中的(Changes to be commited)
- 被修改了的,但没在暂存区中的(Changeds not staged for commit)
- 没有被追踪的。这类文件是指,在工作区中,但不受 Git 管理的文件。
除此之外,我们也可以用 git diff --staged
来查看假若暂存区被提交,那么新版本与当前版本的差别。以此来判断暂存区的内容。
清空暂存区
如题,方法如下:
git rm --cached 文件名
git stash
;这条命令比较危险。- 删除 .git 文件中 index 文件夹的所有内容
让我们重点看一下第二条命令,这个命令是将工作区、暂存区中,与当前版本库中的版本相比,增加的操作全部存到一个栈中保存起来。可以通过 git stash list
和 git stash pop
分别查看栈内容,和还原栈内容。
选择性提交
什么是选择性提交?由于在 add 的过程中,我们可以指定特定的文件,上传到暂存区中。于是,当我们在开发时,假若一次性修改了文件 A 和 B,那么可以单独将文件 A 的修改上传到暂存区、提交到版本库。
但是,由于这些修改是一次性的。也就是说,单独提交文件 A 的修改,作为一个新的版本,则该新版本实际上在硬件是从未存在过的。那么,就有可能造成一些错误。例如,假设修改文件 B 是文件 A 得以运行的前提,那么只提交文件 A 的修改,会导致新版本实际上是不可运行的。
选择性提交的好处在于,能够使得版本泾渭分明。比如我们在文件 B 改了一个 bug,但在文件 A 中开发了新的内容。那么,分开来提交,就可以让人们清晰地认出该 Bug 的那个版本。
版本库管理
这里的版本库,是指对象数据库。
版本的回滚、复原
前面提到,版本库存储了项目文件的历史。因此,人们可以根据版本库,将文件还原到历史状态。
首先,需要用 git log
命令来查看版本库中所有的版本、版本号以及提交时补充的注释。
然后,根据版本号,既可以还原到所需要的版本,具体代码为:git reset [--hard] 版本号
。但要注意,还原后,工作区的内容保持不变。如果需要重新提交,则需要再次 add 后 commit。
此时,输入 git log
不会出现当前版本号之后的版本号。
如果要还原版本,则可以用 git reflog
查询所有的提交,包括之前回滚的那部分提交。查询所需要还原的版本的版本号,再次使用 reset 命令即可。
当然,如果不愿意删除 reset 后版本之后的所有“新”版本,也可以使用 revert 命令。这个命令在回滚提交的同时,也创建了一个新的提交,用来保存“新”版本。然后,修改当前版本并提交后,可以和“新”版本合并,从而避免丢失。
分支
在同一版本库中,可以有不同的分支。也就是说,版本库中的版本,可以不是依次提交的。如下图所示:
此时,由于分支的存在,就构成了两条并行的“泳道”。
为什么有分支的存在?
1、并行开发
2、修复旧版本中的 Bug
分支管理:创建、合并、变基
创建与删除分支
创建一个分支,可以使用 git branch 分支名
的方式,以当前版本为起点,创建一个新的分支。
也可以用 git branch 分支名 版本号
,来以任意版本为起点,创建一个新的分支:
分支是相互独立的,并且一个开发者在同一时间内,只能操作一个分支。该处于操作状态的分支叫做活跃分支。在未创建任何分支的情况下,节点库只有一条叫 master 的分支。这条分支也是默认的活跃分子。
可以通过 git checkout 已存在的分支名
来设置当前的活跃分子。(其原理是将 HEAD 指针指向该分支的最后一个版本)在切换活跃分子后,工作区的内容也随之变化。
在分支上的提交,都会在版本库中创建一个版本。并且,分支中的版本,具体来说,分支的新版本,是相互独立的。
若要删除某个分支,则需要用 git branch -d 分支名
。
合并分支
若要使得所有的版本都合为一处,就要进行版本的合并(分支的合并)。以便将两个节点修改的内容,全部并在一起。当然,这个过程中,可能存在合并冲突(conflict)。此时,就需要手工合并了。这里不再介绍。
合并可以用如下命令实现:git merge 分支1
。运行后,即可将分支 1 合并到当前的活跃分支中。
变基
假设开发者 1 已经走了好长一段路了,但开发者 2 还没有完成 Bug 的修改,如下图:
开发者 2 为了保持新版本,避免落后太多,导致修改的 Bug “过时”,就需要进行变基。
如下版本库:
为了整合历史,我们可以将版本 E 变基,移动到 F 中。具体做法和合并差不多。其做法是保持版本 E 中的修改不变,然后以 F 为基版本,再将修改还原出来:
实现为——首先以分支 2 为活跃分支,然后输入: git rebase master
即可。
当然,变基和合并类似,也有可能出现冲突。此时,就需要手动变基。
标签(tags)
大多数项目中,都会采用 1.2.3.4 后者 “gingerbread” 等数字来表示软件的版本。这就是标签!当然,标签应是永久性的。修然可以强制修改,但大多数情况下,不建议这样做。因为,这很可能会导致版本的混乱。
创建标签
git tag 1.2.3.4 master -m 'My New Tags‘’
这句话将在 master 分支中的当前版本,创建一个标签。这里的标签也带有注释,并作为元数据,保存在版本库中。
标签的管理与作用
- 可用
git tag -l
显示版本库中所有的标签。 - 可用
git log --decorate
在日志中显示标签。 - 可用
git tag --contains 哈希值
可以查询包含哈希值的所有标签。
版本库的交互
创建克隆版本库
对于一个开发者来说,可不能直接在本地版本库中嘿嘿嘿。而是想要在一个克隆版本库进行调试后,再用本地版本库拉回修改。
创建克隆版本库可以用:git clone 克隆源
;不过,在克隆版本库之前,需要创建一个空文件夹,之后在用 cd 进入到文件夹中国,再输入上述命令。
当然,这里的克隆源可以是文件路径,也可以是 URL(如 Github 地址)
裸版本库
裸版本库是指不带有工作区的版本库。也就是说,裸版本库中的项目文件=版本库。这也是为什么,很多时候,我们用版本库代指项目文件夹的原因。
裸版本库一般作为项目版本库使用。用于维护一个“官方”版本库或者作为备用的版本库。其可用 git clone --bare 克隆源
产生。
不过,裸版本库的文件夹一般命名为 xxx.git 。尽管这个后缀并不改变文件夹的性质…
版本库之间的版本分享
有了多个版本库,就需要进行版本库之间的分享。分享的命令有 pull、fetch、push。在此之前,由于使用 URL 或者文件路径来代指其他版本库有点麻烦,因此经常给版本库取一个“昵称”。命令如下:
git remote add 昵称 URL/文件路径
如果昵称太多,记不住,可以用 git remote --verbose
显示所有命令了的昵称;用 git remote rm 昵称
来删除昵称。
抓取 Fetch 操作
Fetch 用于跟踪目标版本库的所有分支。代码为:git fetch 远程版本库的昵称或URL
:
结果如下:
可见,创建的只是分支。我们可以用 git branch -r
命令来显示远程分支。并用 git diff 分支d clone\\分支e
来查看本地分支、以及远程分支的区别。
也可以用 git log 分支d .. clone\\分支e
来查看远端分支新增加的提交(版本)。
Pull = Fetch + Merge
若用 git pull 命令,则可以直接将远端分支追加后,在用 Merge 合并分支。当然,如果里面有冲突,还需要手工合并。
也可以用 git pull --rebase
在 Merge 之前变基。
在 Pull 和 Fetch 时,标签都会自动追加。如果不需要,可用 --no-tags 参数。
Push
将本地版本库推送到远程版本库。在进行 Push 时,值得注意的几点是:
- 需要有远端版本库的访问权限
- 是一种 fast-foreword updated,也就是说,需要远程版本库,属于本地版本库的一个子集,才能进行 Push
- push 并不会让远端版本库跟踪我们的分支
- push 不会将标签也塞给远程版本库。若需要,要加上 --tags 参数。
Github
Github 是一个为 Git 版本控制系统提供托管服务的网络供应商,我们可以使用 SSH 和 HTTPS 协议访问 Github 上的版本库(URL 啦)。
在 Github 中,我们可以直接在服务器上 clone 版本库,这种在服务器端的版本库也叫 fork 版本库。之后,通过 push 命令进修改提交到这个 fork 版本库中。
如果我们希望将自己的提交,传递给源版本库,可以先该 Github 上的所有者发送一个 pull 请求,拉取源版本库。之后,在用 push 命令将提交发给所有者。
有时在 Github 或 Gitee 上创建一个仓库后,因为带有 ReadMe.md,所以可能会出现下面的错误:
如何解决呢?
只需要:
git pull origin master --allow-unrelated-histories
思考题
- 在同一个本地版本库中,修改者、提交者可能是同一个人吗?为什么有可能是不同的人?如何在修改、提交的时候,表明自己的身份?
- 版本库的某一个版本信息中(用 git cat-file -p 版本号获取),上一个版本的根目录有变化吗?为什么它的哈希,与当前根目录的哈希不同呢?
- 选择性提交是什么?依次修改并提交属于选择性提交吗?选择性提交有什么坏处和好处?
- 为什么需要缓存区?git stash 有毛用?
- 如果我们需要的是一个可移动的标志,可以使用标签吗?为什么?
- 如何给 Github 中的某个版本库取一个昵称?
- 在 Github 中,将本地版本库推送到 Github 中的源版本库时,需要用到 git push origin master。请问,这里的 origin 是什么?master 又是什么?
- 解释下述操作:git stash、git log [–oneline] [–graph] [–decorate]、git diff [–staged]、git branch、git checkout、git branch [-r]、git merge、git cat-file -p、git pull、git push、git tag
以上是关于Git 的原理的主要内容,如果未能解决你的问题,请参考以下文章