8. 如何进行提交之间的合并? --"git merge"和"git rebase"

作者:jicanmeng

时间:2014年08月30日


下图是从<<Pro Git>>中截取的图,进行了一些修改。有两个分支:master和bugFix。分别使用git mergegit rebase命令后,看看是什么样子。

git-merge-before-1
图6-1 Two branches

1. git merge

git merge表示合并,格式如下:

$ git merge [选项……] <commit>

合并操作的大多数情况,只需要提供一个<commit>(提交ID或对应的引用:分支、里程碑等)作为参数。合并操作将<commit>对应的目录树和当前工作分支(HEAD指向的分支)的目录树的内容进行合并。合并后的操作以当前分支的提交作为第一个父提交,以<commit>作为第二个父提交。

执行git merge命令:

$ git checkout master
            Already on 'master'
            $ git merge bugFix
            $ 
git-merge-after-1
图6-2 After git merge

<<Pro Git>>上是这么描述git merge的过程的:

Git 会用两个分支的末端(C4 和 C6)以及它们的共同祖先(C2)进行一次简单的三方合并计算,生成一个新的提交。

现在执行git log master命令,

$ git log master --graph --oneline
                *   C7 Merge branch 'bugFix'
                |\
                | * C6
                | * C5
                * | C4
                * | C3
                |/
                * C2
                * C1
                * C0
            $ 

可以看到,现在C5和C6已经成为master分支的一部分。而且,如前面所说,C4是C7的第一个父提交。

<<Pro Git>>上面提到一种特殊情况:

如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。

还是以图6-1为例,我们执行命令验证一下上面的说法:

$ git checkout -b hotfix C1
            Switched to a new branch 'hotfix'
            $ git merge master
            $ 

执行第一条命令后,创建了hotfix分支,如下图:

git-merge-before-2
图6-3 create hotfix branch

执行了第二条命令后,git就会执行所谓的“fast-forwarding”,如下图:

git-merge-after-2
图6-4 hotfix merge master

我们考虑另外一种情况:当HEAD直接指向一个commit对象时,执行git merge命令又会怎么样?还是以图6-1为例,我们来验证一下:

$ git checkout C1
                Note: checking out '2ce2bd3'.

                You are in 'detached HEAD' state. You can look around, make experimental
                changes and commit them, and you can discard any commits you make in this
                state without impacting any branches by performing another checkout.

                If you want to create a new branch to retain commits you create, you may
                do so (now or later) by using -b with the checkout command again. Example:

                  git checkout -b new_branch_name

                HEAD is now at 2ce2bd3... after initial
                $ git merge master
                $ 

执行第一条命令后,HEAD直接指向了C1这个commit对象,如下图:

git-merge-before-3
图6-5 HEAD points to commit

执行第二条命令后,根据“fast-forward”原则,HEAD直接指向了C4这个commit对象,如下图:

git-merge-after-3
图6-6 HEAD points to another commit

根据前面的操作,我们能够得出,git merge其实合并的是commit对象,即使命令后的参数是一个分支名称,那么合并的也是这个分支指向的commit对象。

合并操作并非总会成功,因为合并的不同提交可能同时修改了同一个文件相同区域的内容,导致冲突。冲突会造成合并操作的中断,冲突的文件被标识,用户可以对标识为冲突的文件进行冲突解决操作,然后更新暂存区,再提交,最终完成合并操作。

下面看一个例子:

$ git merge bugFix
                Auto-merging a.c
                CONFLICT (content): Merge conflict in a.c
                Auto-merging temp/b.c
                CONFLICT (content): Merge conflict in temp/b.c
                Automatic merge failed; fix conflicts and then commit the result.
            $ cat .git/MERGE_MSG
                Merge branch 'bugFix'

                Conflicts:
                    a.c
                    temp/b.c
            $ cat .git/MERGE_HEAD
                7070f506fe45df4e0ebef333a1b20b5f9ec05d23
            $ 

当合并失败后,我们可以直接从失败的信息后看到是哪个文件冲突导致了失败,或者从.git/MERGE_MSG文件中查看详细的失败信息。从上面的命令输出可以看到,是a.c文件和temp/b.c文件发生了冲突。

工作区的版本会同时包含成功的合并和冲突的合并,其中冲突的合并会用特殊的标记(<<<<<<<=======>>>>>>>)进行标识。查看当前工作区中冲突的文件:

$ cat a.c
            #include 

            int main()
            {
            <<<<<<< HEAD
                    printf("hello,master\n");
            =======
                    printf("hello,bugFix\n");
            >>>>>>> bugFix
            }
            $ 

特殊标识<<<<<<<(七个小于号)和=======(七个等于号)之间的内容是当前分支所更改的内容。特殊标识=======(七个等于号)和>>>>>>>(七个大于号)之间的内容是所合并的版本更改的内容。

解决合并冲突的方法:
1. 编辑工作区的冲突文件,确定修改成为什么样子。
2. 提交到暂存区。
3. 提交到本地版本库。

放弃合并操作非常简单,只需要执行git reset命令将暂存区和工作区重置即可。

2. git rebase

git rebase表示变基操作,命令行格式如下:
$ git rebase [--onto <newbase>] <since> [<till>]
            $ git rebase --continue
            $ git rebase --abort

第一条命令是最重要的,也最复杂。[]中的内容表示可以省略。有两条原则:

  1. 当省略--onto <newbase>时,默认<newbase>为<since>这个commit对象。
  2. 当省略<till>这个commit对象时,默认<till>这个commit对象为HEAD指向的分支所指向的commit对象.

对图6-1执行git rebase命令:

$ git branch
              bugFix
            * master
            $ git checkout master
            Already on 'master'
            $ git rebase master bugFix
            $ 
git-rebase-after-1
图6-7 After git rebase

上面的命令中,git reabse master bugFix就等价于git checkout bugFix; git rebase master两条命令。因为执行了git checkout bugFix,HEAD就指向了bugFix分支。

下面说一说变基操作的过程:

  1. 首先会指向git checkout切换到<till>.
    • 因为会切换到<till>,因此如果<till>指向的不是一个分支(如master),则变基操作是在 detached HEAD (分离头指针)的状态下进行的.
  2. 将<since>..<till>所标识的提交范围写到一个临时文件中.
    • 对于图6-1来说,"C0..C6"所标识的提交范围是C1、C2、C5、C6。那么,"C4..C6"所标识的提交范围又是多少呢?因为C4不是C6的直接上游,需要找到C4和C6的共同祖先C2,"C2..C6"所标识的提交范围即"C4..C6"所标识的提交范围,即C5、C6.这一点需要注意。
    • 写到一个临时文件中,其实就是生成一个个的patch。对于图6-1来说,就会生成两个patch,从C2到C5有一个patch,从C5到C6有一个patch.
  3. 将当前分支强制重置(git reset --hard)到<newrebase>.
  4. 从保存在临时文件的提交列表中,将提交逐一按顺序重新提交到重置之后的分支上.
    • 其实就是把前面生成的patch一个个地打到重置后的分支上面.
  5. 如果遇到提交已经在分支中包含,则跳过该提交.
  6. 如果在提交过程中遇到冲突,则变基过程暂停。用户解决冲突后,执行git rebase --continue继续变基操作。或者执行git rebase --skip跳过此提交。或者执行git rebase --abort就此终止变基操作,切换回变基前的分支上.

和前面的合并操作一样,变基操作也会产生冲突。假设在打第一个patch时产生了冲突,产生冲突,变基操作会停止。此时我们可以使用git status命令查看那些文件产生了冲突,手工编辑它们,修改为我们想要的最终结果,提交到暂存区。然后执行git rebase --continue继续打第二个patch。当所有的patch都打完之后,变基操作就成功了。

可以通过一个例子看一下:

[jicanmeng@andy git-rebase3]$ ls
                a.c
            [jicanmeng@andy git-rebase3]$ git branch
                  bugFix
				* master
            [jicanmeng@andy git-rebase3]$ git log master --oneline
				d543173 line-7-master2
				4aee472 line-6-master
				8ee450b add line number
				97795cc initial commit
			[jicanmeng@andy git-rebase3]$ git log bugFix --oneline
				e4330fc line-7-bugFix2
				3e323f9 line-6-bugFix
				8ee450b add line number
				97795cc initial commit
			[jicanmeng@andy git-rebase3]$ git rebase master bugFix
				First, rewinding head to replay your work on top of it...
				Applying: line-6-bugFix
				Using index info to reconstruct a base tree...
				Falling back to patching base and 3-way merge...
				Auto-merging a.c
				CONFLICT (content): Merge conflict in a.c
				Failed to merge in the changes.
				Patch failed at 0001 line-6-bugFix

				When you have resolved this problem run "git rebase --continue".
				If you would prefer to skip this patch, instead run "git rebase --skip".
				To restore the original branch and stop rebasing run "git rebase --abort".

			[jicanmeng@andy git-rebase3]$ git status
				# Not currently on any branch.
				# Unmerged paths:
				#    (use "git reset HEAD ..." to unstage)
				#    (use "git add/rm ..." as appropriate to mark resolution)
				#
				#    both modified:      a.c
				#
				no changes added to commit (use "git add" and/or "git commit -a")
			[jicanmeng@andy git-rebase3]$ cat .git/HEAD
				d5431739857c2d23d392ccbd22ea0f80a8b5bb5a
			[jicanmeng@andy git-rebase3]$ cat .git/refs/heads/master
				d5431739857c2d23d392ccbd22ea0f80a8b5bb5a
			[jicanmeng@andy git-rebase3]$ cat .git/refs/heads/bugFix
				e4330fcc7f68343b54081ec9572a48a0b5c1c867
			[jicanmeng@andy git-rebase3]$ vim a.c
			[jicanmeng@andy git-rebase3]$ git rebase --continue
				You must edit all merge conflicts and then
				mark them as resolved using git add
			[jicanmeng@andy git-rebase3]$ git add a.c
			[jicanmeng@andy git-rebase3]$ git rebase --continue
				Applying: line-6-bugFix
				Applying: line-7-bugFix2
				Using index info to reconstruct a base tree...
				Falling back to patching base and 3-way merge...
				Auto-merging a.c
				CONFLICT (content): Merge conflict in a.c
				Failed to merge in the changes.
				Patch failed at 0002 line-7-bugFix2

				When you have resolved this problem run "git rebase --continue".
				If you would prefer to skip this patch, instead run "git rebase --skip".
				To restore the original branch and stop rebasing run "git rebase --abort".

			[jicanmeng@andy git-rebase3]$ git status
				# Not currently on any branch.
				# Unmerged paths:
				#    (use "git reset HEAD ..." to unstage)
				#    (use "git add/rm ..." as appropriate to mark resolution)
				#
				#    both modified:      a.c
				#
				no changes added to commit (use "git add" and/or "git commit -a")
			[jicanmeng@andy git-rebase3]$ cat .git/HEAD
				1b2d21ef016afb32fdbd949e53bec3927c5bd157
			[jicanmeng@andy git-rebase3]$ cat .git/refs/heads/master
				d5431739857c2d23d392ccbd22ea0f80a8b5bb5a
			[jicanmeng@andy git-rebase3]$ cat .git/refs/heads/bugFix
				e4330fcc7f68343b54081ec9572a48a0b5c1c867
			[jicanmeng@andy git-rebase3]$ git log HEAD --oneline
				1b2d21e line-6-bugFix
				d543173 line-7-master2
				4aee472 line-6-master
				8ee450b add line number
				97795cc initial commit
			[jicanmeng@andy git-rebase3]$ git log master --oneline
				d543173 line-7-master2
				4aee472 line-6-master
				8ee450b add line number
				97795cc initial commit
			[jicanmeng@andy git-rebase3]$ git log bugFix --oneline
				e4330fc line-7-bugFix2
				3e323f9 line-6-bugFix
				8ee450b add line number
				97795cc initial commit
			[jicanmeng@andy git-rebase3]$ vim a.c
			[jicanmeng@andy git-rebase3]$ git add a.c
			[jicanmeng@andy git-rebase3]$ git rebase --continue
				Applying: line-7-bugFix2
			[jicanmeng@andy git-rebase3]$ cat .git/HEAD
				ref: refs/heads/bugFix
			[jicanmeng@andy git-rebase3]$ cat .git/refs/heads/master
				d5431739857c2d23d392ccbd22ea0f80a8b5bb5a
			[jicanmeng@andy git-rebase3]$ cat .git/refs/heads/bugFix
				5f96023e8ddacea3d41e49da8710a16190904bcf
			[jicanmeng@andy git-rebase3]$ git branch
			* bugFix
			   master
			[jicanmeng@andy git-rebase3]$ git log master --oneline
				d543173 line-7-master2
				4aee472 line-6-master
				8ee450b add line number
				97795cc initial commit
			[jicanmeng@andy git-rebase3]$ git log bugFix --oneline
				5f96023 line-7-bugFix2
				1b2d21e line-6-bugFix
				d543173 line-7-master2
				4aee472 line-6-master
				8ee450b add line number
				97795cc initial commit
            [jicanmeng@andy git-rebase3]$ 

需要注意的地方有:

  1. rebase后,当前分支变为bugFix了;
  2. rebase的过程中,HEAD指向具体的commit对象,rebase成功一个commit,HEAD移动到最新的commit对象上面,直到rebase结束,HEAD才指向了bugFix分支。这一点和上面提到的rebase的过程略有差异。

参考资料

  1. Pro Git 3.2 分支的新建与合并
  2. git权威指南 第十二章 改变历史
  3. Learn Git Branching