1. git 冲突的产生
上图中列出了使用git中的常规操作,但是却没有git merge,工作中经常直接使用pull中隐含的merge,有merge就有冲突(conflict),有时发生了冲突也不知道原因,解决起来手忙脚乱,本文将介绍git冲突发生的场景,git冲突的判断原理,以及应对方法,以便在工作中更愉快地使用git。
1.1 产生时机
在两个分支进行merge操作时(pull中隐含了merge操作),可能会出现冲突。
在聊出现冲突之前,我们来聊聊不会发生冲突的情况:
在master分支上,执行git merge dev。
- 若dev的提交分支图在master分支图之内,那就没什么需要合并的,因为代码已经在历史上了。
- 如果master的提交分支图在dev分支图之内,也就是没有“横生枝节”,那master会直接到达dev的状态,智能合并。
tips:zsh中查看git log快捷命令glol
,快速查看两分支的commit历史。
此上两种情况都不会发生冲突。哪怕这两种情况下,两个分支当前的文件天差地别,因为git会认为这两个分支在历史上已经合并过了。
- 还有一种情况,两个分支上的文件未发生冲突(比如,一模一样或是增加了不同的行(不相邻),git能够自动解决的冲突便认为是未发生冲突),但是两个分支的提交记录不同。典型情况是,在master上和dev1上对文件做了相同的改进并进行了commit,这个时候git会提示:
Merge branch 'dev1'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
本质就是git要合并两条分支的话,会自动生成并commit一个新节点,但是需要用户提供这个节点的commit message,一般直接输入:wq
再enter就行。
1.1.1 文件冲突
典型场景是,用户在两个分支上都提交了新的内容,这是冲突产生的基础,这种情况下两分支的历史就不会相互包含。此时若用户编辑了同一文件,git会进行冲突判断,我们这里认为git解决不了的需要用户手动解决的才是真正的冲突,若git可以自动解决,则对应前文不发生冲突情况的第三点。
不严谨地说,大多数情况下,用户修改了(删除)同文件同一行的内容才会发生冲突。
至于具体如何判定两文件是否冲突,那些内容冲突,详见下一小节。
1.1.2 文件删除
当我们在一条分支上删除了某个文件,而在另一条分支上修改了这个文件,merge会因为冲突而失败。
同时git status查看状态会显示如下信息让用户知晓并解决这个冲突。用户可以在此基础上删除或者保留文件然后add&commit,冲突就解决了。
1.1.3 修改文件名
在不同分支上同时修改某一个文件的名字,merge时会自动解决冲突,出现两个不同名字相同内容的文件,git处理时应该是认为原文件被删除,两个分支各增加了一个文件(虽然git也知道这个行为是rename)。
不过这个时候git的提交策略不同,git会将新的文件放到暂存区,老的文件删除,用户需要自己去add并commit当时的情况。
1.2 判断标准
网络上对于git冲突判定的文章较少,总结起来其主要有两点:三路归并算法和diff算法。
1.2.1 三路归并算法
简化情况,我们面对的问题是如何判断两个文件是否产生冲突(不可自动解决的冲突)。实际上git做这个对比还用到了第三个文件,也就是这两个分支的最近公共祖先节点上的文件状态,并称之为Base。另两个文件分别称为MIne(当前分支),Theirs(需要被合进来的分支)。
三路归并算法分别对比(使用下文提到的git diff算法)Mine和Base,Theirs和Base。得到当前两文件相对于Base的不同之处(可以认为是两个分支的修改)
- 若这些不同之处发生在不同的行,git则会自动进行合并进入到前文提到的不会发生冲突的第三种情况。
- 若修改之处发生在同一行(不知是这个算法的设计还是缺陷,发生在相邻行也会触发冲突)。
发生冲突后则会产生新的未提交文件,由用户处理解决冲突。
更深入的内容详见从原理出发搞定Git冲突!& git merge是怎样判定冲突的?
1.2.2 git diff算法
git diff更为单纯,就是比较两文件的不同之处,平时看到的红红绿绿的加减号commit记录,就是diff算法的结果。git diff采用的是Myers差分算法,简单理解就是一种类似于最长公共子序列的动态规划算法,这个算法将两个不同文件的比较转化为一个最短路径的图搜索问题。算法详情见git生成diff原理:Myers差分算法 。
总之该算法的输入是两个文件,输出是从一个文件到另一个文件的“路径”(通过增删行)。
2. git冲突解决办法
2.1 merge时的冲突
前文提到,当发生冲突后,会产生新的文件内容,这个时候查看status会提示用户有新的unmerged内容,解决冲突的方法就是将新的文件内容重新add&commit。这个时候我们会发现出现了新的merge节点,冲突也就没了。
不过这样只是表面的解决,实际上代码里还有一堆<<<<<<<========>>>>>>>>这样的符号等着我们去修改,并加以取舍,确定最终内容,然后提交,这才是解决冲突的真正步骤。
解决完冲突后,被合并分支的历史commit就会在当前分支的历史commit之中,二者就不会发生冲突了。
2.2 git拉取远端代码时的冲突
平常更多面临的情况是pull代码的时候发生的冲突,pull = fetch + merge。其实发生的冲突就是merge时发生的,解决完冲突再commit,最后push就可以成功了。
3. git pull push fetch merge的一些细节
3.1 git pull 、git push 和git fetch 的默认参数
git fetch origin <place>
: 从远程分支上拉取对应分支到本地的远程分支指针(origin/master);
git fetch未指定参数时,将远端所有的提交记录同步到本地的远程分支指针上。
git pull
未指定参数时,先执行无参数的git fetch,然后将在当前分支上合并同名(没做其他骚操作的话)的远程分支(已更新)。
git push
未指定参数时,初次会报错
fatal: The current branch new has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin develop
需要执行下面这句话将远程分支与本地分支关联起来,然后执行git push就默认推送到origin develop了。
3.2 git fetch 的效果与git pull的异同
简单来说git pull = git fetch + git merge。
git fetch
好像在工作中用到的不多,一般都直接pull代码。但是git fetch作为一种对当前代码无更改的数据更新方式,也是不可或缺的。
git fetch会拉取远程数据到本地的远程分支指针(如origin/master),但不会对代码进行改变,如果checkout到指定分支就可以查看这个分支上更新的代码,并且方便地在本地进行合并操作。
e.g.
git pull origin foo
相当于:
git fetch origin foo; git merge origin/foo
3.3 git(master): git merge dev --squash
相比于默认的fast-forward, --squash可以让master分支上的提交历史更干净,并且在merge之后还需要在主分支上执行一下commit,将新代码提交上去。
4.总结
- 修改公共代码尽量不在同一行,不同文件就更好了
- 开发时push前常同步master上的代码,免得最后提交的时候冲突太多