对于很多试图说明git工作过程的文章都会出现一个图:
A---B---C topic / D---E---F---G---H master
但是并没有告诉你,这些字母代表的commit到底是什么。把topic分支merge到master的时候的conflict为什么不体现出来?这才是初学者迷茫的东西,文章画的图倒是简单明了,然后困惑还是困惑。很多问题其实如果把问题描述清楚了,问题也就解决了。那么git解决的问题是什么,都知道是版本控制,那么具体化之后是什么东西呢?
1、什么是commit?
首先git的区分文件变化的最细粒度是line行,不是单词更不是字母。行变化主要是增删改三种。commit可以看成是记录这些增删改操作的数据。
commit是一个add,del,update的集合,update分解为一个del配合一个add,update n,可以表示为(del n)+(add n.5)或者(del n)+(add (n-1).5) 其中n.5表示在n行后添加一行,所以commit只是一个del和add的集合,只要记录下del的行号,add的gap号的集合,配合待变化的base文件,就可以精确确定修改内容是什么。这些add和del是无序的,只要全部应用到base文件,就会得到相同的结果文件。
2、合并
合并两次commit就是,两个ops集合的合并,合并是可以递归的,一系列commit可以合并成一个commit,对应就是一些列ops集合合并为一个大ops集合。
3、什么是冲突?
冲突就是指两次commit的执行顺序会影响都最终结果文件内容的不同,这就是通常说的冲突。也就是不能简单机械地合并,否则不符合预期。那么我的预期是什么呢?预期就是颠倒两次commit得到的结果是一样的。这样的合并过程需要人为参与,告诉git合并的时候应该以什么样的结果为准。
那么什么样commit产生冲突呢?
两个commit,del同一行,不分操作先后只执行一次,就可以确定不同操作顺序不影响最终结果。
两个commit,add到同一个gap,那么不同的操作顺序最终结果不同,所以,这两个commit是conflict。
两个update同一行,翻译为一个del加上一个add,如果add可以是被del行的前或者后的gap,这个由于对同一个gap的add判定conflict。
甚至update相邻两行。
实验验证同时修改相邻两行为什么会冲突,思考了一下,原因可能如下。
如果是update一行,那么相当于在这一行前或者后增加一行,所以,这一行本身和这一行前后的gap都被锁定——可以认为是被污染的范围。
由于,add的gap仍然是有交叉的,因为一个update等价于del该行然后在after或before的gap增加新行,也判定为conflict。
所以如果用整数表示行号,用x.5表示x行后的gap,因为修改一行是del line,add before或者add after都行。
commitA,update line4——修改范围是4,3.5,4.5
commitB,update line3——修改范围是3,3.5,2.5
commitC,update line2——修改范围是2,2.5,1.5
所以A和B是冲突的,A和C不是冲突的。
4、解决merge conflict
对于git新手而言冲突很可怕,但是版本控制的日常就是发生冲突。
当两个ops集合合并的时候,产生一个新的commit,也就是两个commit的和,将所有ops操作应用到文件,git判断有冲突之后标记处冲突部分——哪个commit分别做的修改是什么,此时,文件可以编辑,用户修改后并确认,add,commit为最终的merge结果。
git的冲突的情况只会被夸大,而不会被忽略,宁枉勿纵,这是安全的策略。git检测文件的改动时,总是尽量精确描述修改的部分——例如在文件最前面增加一行,如果准确描述,就是add line to gap 0.5,如果简单起见也可以认为del all,add new all。没有精确描述缩小改动是问题不大,最多只是影响性能。
现在再理解ABC代表的commit,就可以想到背后只不过一个增删的操作集合,merge也只是集合的合并。
5、rebase
将分支rebase到一个分叉点之后的master提交,实际上导致分支上的commit做了相应的调整,是为了保证从分支merge到master的时候不会出现冲突。否则,需要先从master merge到分支,此时在分支解决冲突,然后将最新分支merge到master,两个分支多了一个历史交汇点。如图,先将master分支merge到topic的M,此时如果有冲突,则在topic分支解决提交到topic,然后从topic merge到master的H。
A---B---C--M topic / / D---E---F-----G-----H master
而使用rebase则可能在rebase的时候有冲突,此时解决冲突,这时topic分支的commit其实已是A2,B2,C2了,因为他们的base文件变化了,只有操作集合跟随变化,才能保证最终的结果文件不变。
A2---B2---C2 topic / D---E---F---G---H master
由于,topic是rebase到G的,所以此时merge到master,一定没有conflict,这样合并的master分支就更好看。
最后,相信一点,git说没有冲突就真的没有冲突,真的有冲突不会被git所遗漏。
==========================================
但是,其实没有冲突不代表代码不会有bug。举个极端的例子:
一个outputstream,忘记了写bye这个单词,还忘记了close。如果
{
……
OutputStream os= ...…
other code
……
}
一个人增加
{
……
OutputStream os= ...…
other code
os.write("bye")
……
}
另一个人增加
{
……
OutputStream os= ...…
……
os.close()
other code
……
}
merge是无冲突的,结果如下
{
……
OutputStream os= ...…
……
os.close()
other code
os.write("bye")
……
}
显然这段代码运行时就会出错。