问题假设
让我们通过一个简单的分支和合并的例子,演示在实际工作中可能会使用的工作流程。将按照以下步骤进行:
- 在网站上进行一些工作。
- 为正在开发的新用户故事创建一个分支。
- 在该分支上进行一些工作。
在这个阶段,我们可能会接到一个电话,说另一个问题非常严重,需要一个热修复。将执行以下操作:
- 切换到生产分支。
- 创建一个分支来添加热修复。
- 在测试通过后,合并热修复分支,并推送到生产环境。
- 切换回原始的用户分支,并继续工作。
基础分支
首先,假设正在项目上工作,并且在主分支上已经有了几个提交。
要创建一个新的分支并立即切换到它,可以使用git checkout命令并加上-b选项。
$ git checkout -b iss53
Switched to a new branch "iss53"
这条指令相当于
$ git branch iss53
$ git checkout iss53
在网站上进行一些工作并提交了一些更改。这样做会推进iss53分支,因为我们已经检出了它(也就是说,我们的HEAD指向它)。
$ vim index.html
$ git commit -a -m 'Create new footer [issue 53]'
现在我们接到通知说网站出了问题,需要立即修复。使用git,不必将所做的iss53更改与修复一起部署,也不必在开始应用修复到生产环境之前花费大量精力来撤销那些更改。只需切换回主分支。
然而,在这样做之前,请注意,如果工作目录或暂存区有未提交的更改与要检出的分支冲突,Git 将不会让我们切换分支。最好在切换分支时保持干净的工作状态。有办法绕过这个问题(即存储和提交修订),我们后续会介绍。现在,让我们假设已经提交了所有的更改,所以可以切换回到主分支。
$ git checkout master
Switched to branch 'master'
在这一点上,当前项目工作目录恰好与开始处理问题#53之前的状态一样,我们可以专注于我们的热修复。这是一个重要的要点要记住:当切换分支时,git 会将工作目录重置为看起来像最后一次在该分支上提交时的样子。它会自动添加、删除和修改文件,以确保工作副本与分支在你最后一次提交时的样子一样。
接下来,我们有一个热修复要做。让我们创建一个热修复分支,在上面完成工作,直到它完成。
$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'Fix broken email address'
[hotfix 1fb7853] Fix broken email address
1 file changed, 2 insertions(+)
可以运行测试,确保热修复是我们想要的,最后将热修复分支合并回主分支,以部署到生产环境。可以使用git merge命令来完成这个操作。
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)
在本次合并中,会注意到“fast-forward”这个短语。这是因为合并进来的分支 hotfix 所指向的提交 C4 直接在当前所在的提交 C2 之前。Git 简单地将指针向前移动。换句话说,当尝试将一个提交与可以通过跟随第一个提交的历史到达的提交合并时,Git 通过向前移动指针来简化操作,因为没有需要合并的不同工作 —— 这被称为“fast-forward”。
我们的更改现在包含在主分支指向的提交快照中,现在我们可以部署这个修复。
在我们的超级重要的修复部署完成后,我们准备回到之前被打断的工作。首先我们最好删除 hotfix 分支,因为不再需要它 —— 主分支指向相同的位置。可以使用 `git branch` 的 `-d` 选项来删除它。
$ git branch -d hotfix
Deleted branch hotfix (3a0874c).
现在可以切换回到正在处理的关于问题 #53 的工作分支,并继续工作。
$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'Finish the new footer [issue 53]'
[iss53 ad82d7a] Finish the new footer [issue 53]
1 file changed, 1 insertion(+)
这里值得注意的是,在hotfix 分支中所做的工作并没有包含在你iss53 分支的文件中。如果需要将它拉取进来,可以通过运行 `git merge master` 将你的主分支合并到你的 iss53 分支中,或者可以等待在稍后决定将 iss53 分支合并回主分支时再集成这些更改。
基本合并
假设 #53 的工作已经完成,并准备好合并到主分支中。为了做到这一点,只需切换到想要合并到的分支,然后运行 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(+)
这看起来与之前执行的 hotfix 合并有些不同。在这种情况下,开发历史与某个较早的点分叉了。因为当前所在的分支上的提交不是要合并的分支的直接祖先,Git 需要做一些工作。在这种情况下,Git 执行一个简单的三路合并,使用两个分支端指向的快照和这两个分支的共同祖先。
与仅仅向前移动分支指针不同,Git 创建了一个新的快照,它是这个三路合并的结果,并自动创建了一个指向它的新提交。这被称为合并提交,它是特殊的,因为它有多个父节点。
现在工作已经合并了,不再需要 iss53 分支。可以在问题跟踪系统中关闭这个问题,并删除这个分支:
$ git branch -d iss53
基本冲突合并
偶尔,这个过程不会顺利进行。如果在要合并的两个分支中的同一个文件的同一部分进行了不同的更改,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 status`:
$ 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 分支,因为在运行合并命令时已经检出了它)是该块的顶部部分(在 ======= 之上的所有内容),而 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):
如果想使用除默认工具之外的合并工具(在这种情况下 git 选择了 opendiff,因为该命令在 macOS 上运行),可以在“以下工具之一”后面看到列出的所有支持的工具。只需输入想使用的工具的名称即可。
当退出合并工具后,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
#
如果认为修改这个提交消息,对于未来查看此合并的其他人会很有帮助的话,可以加入有关如何解决合并以及解释为什么做出这些更改的详细信息。