Git创建分支处理合并冲突

Git新建分支及处理合并冲突,需要对git分支及合并过程有一个准确的了解。

GIT新建分支

假设git仓库中已经有了一些提交。现在,系统中有一个问题需要解决,把这个问题命名为#53。在远程仓库上为#53创建一个新分支iss53,然后,在本地使用带有-b参数的git checkout命令检出新分支。

GIT新建分支

$ git checkout -b iss53
Switched to a new branch "iss53"

或者

$ git branch iss53
$ git checkout iss53

git新建分支

GIT提交分支

现在为#53问题在新分支上做了一些修改并进行了提交。在处理#53问题的过程中,iss53分支在不断的向前推进。也就是说,HEAD指针目前指向了iss53分支。

$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'

git新建分支

创建hotfix分支

假如,现在有一个紧急bug需要修复,所以,现在需要进行分支切换。但在切换分支之前,留意工作目录和暂存区里未被提交的文件。因为,它可能会与即将检出hotfix分支产生冲突,从而阻止Git切换到hotfix分支。

最佳做法是在切换分支之前,保证当前分支处于一个干净的状态,可以通过提交做到这一点,还可以通过git stash命令对修改进行暂存。具体可以参见Stashing and Cleaning

现在,假设已经把修改全部提交了,可以切换分支了。为了说明问题先切回master分支。

$ git checkout master
Switched to branch 'master'

现在工作目录和iss53分支最初创建时一模一样。

请牢记:当切换分支的时候,Git会重置工作目录,使其看起来像回到了那个分支最后一次提交的样子。Git会自动添加、删除、修改文件,以确保此时的工作目录,与这个分支最后一次提交时的样子一模一样。

接下来,在本地检出hotfix分支,在hotfix分支上修复这个紧急bug。

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

$ vim index.html

$ git commit -a -m 'fixed the broken email address'
[hotfix 1fb7853] fixed the broken email address
 1 file changed, 2 insertions(+)

新建git分支

基于master分支新开的hotfix分支 ,在修复了bug后,将修复后的代码合并回master分支。使用git merge进行分支合并:

$ git checkout master

$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

git fast-forward

在合并分支时,出现到"快进(fast-forward)"这个词。由于当前master分支所指向的提交是hotfix提交的直接上游,所以Git只是简单的将指针向前移动。

换句话说,当试图合并两个分支时,如果顺着一个分支走下去能到达另一个分支,那么Git在合并两者的时候,只会简单的将指针向前推进(指针右移)。因为这种情况下的合并操作,没有分歧需要解决——这就是“快进(fast-forward)”。

现在,最新的修改已经在master分支所指向的提交快照中。

git创建分支

Git所存储的不是一系列的更改集(changeset),而是一系列快照。执行一次commit, Git会存储一个commit对象,它包含一个指针,指向提交内容的快照。Git中master分支的功能和其他分支一样,master分支在git中频繁出现,是因为运行git init命令时,会默认创建一个分支并命名为master。

GIT合并分支

紧急bug的修改代码发布之后(hotfix),准备回到被打断的工作中(iss53),可以先删除 hotfix 分支,已经不再需要它了。因为master分支已经指向了同一个位置。可以在git branch命令加上 -d参数来删除分支:

$ git branch -d hotfix
Deleted branch hotfix (3a0874c).
$ git checkout iss53
Switched to branch "iss53"

$ vim index.html

$ git commit -a -m 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
1 file changed, 1 insertion(+)

切换git分支

在hotfix分支上所做的修改并没有包含到iss53分支中,如果要拉取hotfix所做的修改,使用git merge master命令,将 master分支合并到iss53分支,或者也可以等到iss53 分支完成后,将其合并回master分支。

假设已经修复了#53问题,并且打算将iss53分支并入master分支(即:合并iss53分支到master分支上),这和之前合并hotfix分支所做的工作差不多,只需要切换到想并入的分支上,然后运行git merge命令。

$ git checkout master
Switched to branch 'master'

$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

git三方合并

这次看起来与之前合并hotfix分支有所不同,因为,这个开发历史是从一个更早的位置开始分叉出来的(diverged)。现在,master分支所在的提交,并不是iss53分支所在提交的直接上游(祖先),Git不得不做一些额外的工作。

出现这种情况时,Git将使用两个分支末端所指向的快照(C4 和 C5),以及这两个分支的工作上游/祖先(C2),做一个简单的三方合并。

git分支合并

和之前将分支指针向前推进(fast-forward)有所不同,此次Git将三方合并的结果生成一个新的快照,并且自动创建一个新的提交指向它。 这被看作一次合并提交,它的特别之处在于它不止一个父提交。

git三方快照

需要指出的是,Git会自行决定选取哪一个提交,作为最优的共同上游/祖先,并以此作为合并的基础,这和CVS、Subversion (1.5 版本之前)不同,在这些古老的版本管理系统中,用户需要自己选择最佳的合并(基础)版本。Git的这个优势使其在合并操作上,比其他版本管理系统要简单很多。

既然修改已经合并进来了,已不再需要iss53分支了,现在可能删除这个分支了。

$ git branch -d iss53

git合并冲突

有时候合并操作不会如此顺利,如果在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git就没法对它们进行自动合并。 如果对#53问题的修改和hotfix的修改涉及到同一个文件的同一处,在合并的时候就会产生合并冲突:

$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

此时Git做了合并,但没有自动地创建一个新的合并提交,Git 会暂停下来,等待管理员去解决合并产生的冲突。

可以在合并冲突出现后使用git status命令进行查看,找出那些因合并冲突而处于未合并(unmerged)状态的文件:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

任何因合并冲突而有待解决的文件,都会以未合并状态给标识出来。Git会在有冲突的文件中加入标准的冲突标记,这样就可以打开这些包含冲突的文件,然后手动解决冲突。出现冲突的文件会包含一些特殊区间片段,看起来像下面这个样子:

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

这表示HEAD所指向的版本(也就是当前master分支所在的位置,因为在运行merge命令时已检出到这个分支上),在这个片段的上半部分(======= 的上半部分),而iss53分支所指示的版本在=======的下半部分。

解决冲突时,需要选择由 =======分割的两部分中的一个,或者自行合并这些内容。 例如,可以把这段内容换成下面的样子来解决冲突。

<div id="footer">
please contact us at email.support@github.com
</div>

上述冲突解决方案仅保留了其中一个分支的修改,并且 <<<<<<< , ======= , 和 >>>>>>> 这些行被完全删除了。 在解决了所有的冲突文件之后,对这些文件使用 git add命令将其标记为冲突已解决。一旦暂存了这些原本有冲突的文件后,Git就会将它们标记为冲突已解决。

如果想使用图形化工具来解决冲突,可以运行git mergetool,该命令会启动一个合适的可视化合并工具,引导一步一步的解决冲突:

$ git mergetool

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html

Normal merge conflict for 'index.html':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

如果不想使用默认的工具,可以在 “one of the following tools” 这句后面看到目前所支持的合并工具。 选择一款喜欢的工具,输入名字就可以打开了。

等退出合并工具之后,Git会询问刚才的合并是否成功。 如果回答[是],Git会暂存那些文件以表明冲突已解决。通过运行git status来确认所有的合并冲突都已被解决。

$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    modified:   index.html

如果对结果感到满意,并且确定之前有冲突的文件都已经暂存了,输入git commit完成合并提交。 默认情况下提交信息看起来像下面这个样子:

Merge branch 'iss53'

Conflicts:
    index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#    .git/MERGE_HEAD
# and try again.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#    modified:   index.html

如果觉得上述信息不够充分,不能完全体现分支合并的过程,可以添加修改信息加入更多说明。