搞懂 Git 工作原理,遇到问题不再瞎蒙

拟墨画扇 提交于 2020-12-28 19:32:36

上一篇文章我们了解了Git的常用命令,这一篇文章我们将来了解这些常用命令的工作原理,以便更好的掌握这些命令。

在开始本篇文章之前,读者可以先试着回答以下几个问题:

  • 是否了解工作区暂存区仓库之间的区别?

  • Git的常用命令在三大区域中是如何工作的?

  • 分支是如何合并的?原理是什么?

  • 分支合并中rebasemerge的区别?

如果有回答不出的,那么建议还是往下仔细看看文章吧~

三大分区

我们首先用一张图来理解工作区、暂存区和仓库的位置:

图片

我们先看由下而上的路径,首先工作区就是我们当前的文件目录,我们改完代码,用git add命令把当前文件加入暂存区,然后git commit暂存区生成的快照提交到本地仓库,最后再用git push命令把本地仓库的提交复制到远程仓库,也就是Github之类的在线仓库。

而由上到下的路径其实也很好理解,git pull用来将远程仓库的最新提交拉取到本地仓库git reset -- files 用来撤销最后一次git add files,也就是撤销commit,这是我们前面提到的回滚的一种办法;git checkout -- files则是把文件从暂存区复制到工作区,用来丢弃本地修改(也就是覆盖掉还未add到暂存区的改动)。

常用命令的工作原理

先来个开胃小菜:

diff

上一篇文章中我们讲了git diff可以直观的看到工作区暂存区的差异,这里我们画图演示下不同的diff是如何比较的:

图片

  • git diff,不加任何参数,将工作区(未add的内容)和暂存区进行比较;

  • git diff HEAD,将工作区与HEAD指针指向的commit进行比较,一般来说我们当前的改动就是在HEAD指向的commit的基础上进行改动;

  • git diff --cached,将暂存区与当前commit进行比较;

  • git diff dev,将工作区与目标分支的最新commit进行比较;

  • git diff [commitId_1] [commitId_2],将两个commit进行比较。

commit

前面我们说了,commit会在暂存区生成快照,然后推到本地仓库,这里我们考虑三种情况下的提交:

  • 当前HEAD指向末尾的commit

图片

  • 当前HEAD指向中间的commit,此时提交就会再分离出一条新的路线,因此后续的分支合并就不可避免地要派上用场。

图片

  • 希望用新提交覆盖前一个提交:git commit --amend

这个使用场景也非常广泛,比如我们git commit后才发现漏改了点东西,这个时候如果再改再提交,就会导致对一个错误的修改用了两个commit,在git log上看将会非常丑,对于我们自己做小demo时可能无所谓,对于一些大项目或者开源项目,本来commit就很多,这样胡乱地增加commit必然是不能接受的。

如上图所示,我们新增的commit会代替原来的commit的位置,而旧commit则被抛弃掉。

checkout

当我们使用git checkout [branch_name]切换分支时,如下图所示:

图片

dev分支会把其中的内容复制到暂存区和工作区中,覆盖掉master的版本,而只存在于master的文件则会被删除。

reset

下图展示了回滚的情况,具体的三种情况请仔细看下方的描述:

图片

  • git reset [commitId] --sort,这是最弱的回滚方式,只改变commit信息,不影响暂存区工作区

  • git reset [commitId],不携带参数时,默认只回滚暂存区,也就是把dks8v所在的信息复制到暂存区,但是不影响工作区

  • git reset [commitId] --hard,这种方式则能回滚工作区暂存区

merge

Git的合并有许多策略,默认情况下Git会帮助我们挑选合适的策略,当然如果我们需要手动指定,可以使用:git merge -s [策略名称],了解 Git 合并策略的原理可以使你对合并结果有一个准确的预期。

Fast-forward

Fast-forward是最简单的一种合并策略,如我们前面示例的图所示,dev分支是master分支的祖先节点,那么合并git merge dev的话,只会将dev指向master当前位置,Fast-forwardGit合并两个没有分叉的分支时的默认行为。

Recursive

RecursiveGit在合并两个有分叉的分支时的默认行为,简单的说,是递归的进行三路合并。

图片

这里出现了一个新名词——三路合并(three-way merge),也是我们接下来讲解的重点。我们先搞清楚合并的整体链路。

  • 首先dev分支的c5k8xHEAD指向的sf22x,再加上它们的最近公共祖先a23c4先进行一次三路合并;

  • 然后将合并后的结果拷贝到暂存区工作区

  • 再然后产生一次新的提交,该提交的祖先为dev原master

分支合并的原理

首先,我们来看看两个文件如何合并:

下图所示为test.py中某一行的代码,如果我们要将A/B两个版本合并,就需要确定是A修改了B,还是B修改了A,亦或者两者都修改了,显然这种情况下分辨不出来。

图片

因此,为了实现两个文件的合并,我们引入三路合并

如下图所示,很显然ABase版本相同,B版本的修改比A版本新,因此将A/B合并后,得到的就是B版本。

图片

聪明的读者看完上面的例子,就会想到,要是A/BBase都不一样怎么办?这就是接下来要讲的问题了。

冲突

当出现下图这种情况时,一般就需要我们手动解决冲突了。

图片

也就是我们在合并代码时往往会看到的一种情况:

<<<<<<< HEAD
print("hello")
=======
print("fxxk")
>>>>>>> B

对于新手而言,看到这个箭头可能有点摸不着头脑,到底哪个是哪个呢?其实分辨起来很简单,中间的=======是分隔符,到最上方的<<<<<<之间的内容,是HEAD版本,也就是当前的master分支,而到最下方>>>>>>之间的内容,则是分支B的,我们只需要删除箭头,保留所需要的版本即可:

print("hello")

最终合并结果:

图片

递归三路合并

在实际的生产环境中,Git的分支往往非常繁杂,会导致合并A/B时,能找到多个A/B的共同祖先,而所谓的递归三路合并就是,对它们的共同祖先继续找共同祖先,直到找到唯一一个共同祖先为止,这样可以减少冲突的概率。

图片

如上图所示,我们要合并56,就需要先找到5/6的共同祖先——23,然后再继续找共同祖先——1,当我们找到唯一祖先时,开始递归三路合并,先对1、2、3进行三路合并,得到临时节点2'/B

图片

接下来继续对2、5、6进行三路合并,得到7/C

图片

rebase

当我们处于dev分支,然后使用git rebase master时,可以理解为把dev分支上的部分在master分支后面重新提交了一遍(重演),具体看下图:

图片

首先找到dev分支和master分支的祖先a23c4,然后从a23c4dev所在路径上的节点,都通过回放的方式插入到master之后,注意,这里“复制”的过程中,commitId是会改变的。同时,dev旧分支上的节点因为没有了引用则会被丢弃。

总结

回顾开头的问题,相信仔细阅读完本篇文章的你已经可以解答了。本篇文章更多聚焦在Git的工作原理上,但对于底层原理还未展开叙述,下一篇我们会对Git底层到底是如何存储文件,如何实现进行讲解,敬请期待。

参考资料

图解Git:https://marklodato.github.io/visual-git-guide/index-zh-cn.html

Pro Git:https://bingohuang.gitbooks.io/progit2/content/

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!