8.合并¶
分支版本库只是支持平行和并发开发的前半部分;最终,你必须把所有这些分支的部分重新组合起来。而且,是的,这个操作可以像你想象的那样复杂
Merging
是Git
将你的工作与他人的工作相结合的机制。因为Git
支持成百上千的贡献者分别工作的工作流程,所以Git
尽可能多地为你完成繁重的工作。偶尔,你也要插手帮助Git
,但在大多数情况下,合并对你来说可以而且应该是一个相当轻松的操作。
要开始这一章,请导航到你在本书中一直使用的ideas
目录。
看一下你的分支机构¶
首先,用以下命令切换到该仓库的clickbait
分支:
git checkout clickbait
如果你把当前ideas
仓库的分支历史可视化,你坐在clickbait
分支上,它看起来会是这样的:
在上面的图片中,你可以看到以下内容:
- 这是你的本地
main
分支。图中的底部代表了就版本库而言的时间起点,而最近的提交在图的顶部。 - 这是
origin
上的main
分支,也就是远程版本库。你可以看到你克隆版本库的时间点,以及从那以后你做的一些本地提交。 - 这是
clickbait
分支,由于这是你刚刚切换到的分支,你可以看到在clickbait
分支的顶端贴着HEAD
标签。你可以看到这个分支是在你克隆版本库之前的一段时间从master
创建的。 - 这是一个旧的分支,是在过去的某个时候从
master
上创建的,并在几个提交之后被合并到master
。这个分支后来被删除了,因为它已经达到了它的目的,不再需要了。
这是一个相当普遍的开发工作流程;在一个小团队中,main
可以有效地作为主要的开发线,开发人员在main
上建立分支,进行功能或错误修复,而不影响主要开发线中的内容。许多团队认为main
代表"部署到生产中的东西",因为他们把main
看作开发环境中的"真理之源"。
在讨论合并之前,你应该花点时间弄清楚一些"占有"的术语。
当Git
准备将两个文件合并在一起时,它需要先了解一下哪个分支是哪个。同样,main
没有什么特别之处,所以你不能总是假设你的分支是这样合并的。在实践中,你会发现你经常在非main
的分支之间进行合并。
因此,Git
以ours
和theirs
来看待分支。"我们的"指的是你要合并回的分支,而"他们的"指的是你想拉到"我们的"的分支。
假设你想把clickbait
分支合并到main
。在这种情况下,如下图所示,main
是ours
,clickbait
分支是theirs
。保持这种区别将对你的合并工作有不可估量的帮助。
三向合并¶
你可能会认为,合并实际上只是在两个分支上各取一个修订版,并以合乎逻辑的方式把它们混在一起。这就是two-way
合并,这也是我们大多数人思考世界的方式:由两个现有元素形成的新元素只是每个元素的独特和共同部分的结合。然而,Git
的合并实际上使用了三个修订来执行所谓的three-way merge
。
要知道为什么会这样,请看下面的双向合并方案。你有一个简单的文本文件;你正在处理该文件的一个副本,而你的朋友正在处理同一文件的另一个独立副本。
你从文件的顶部删除一行,你的朋友在文件的底部添加一行。
现在想象一下,你和你的朋友把你们的工作交给一个公正的第三方来合并这个文本文件。现在,这个第三方完全不知道这个文件的原始状态是什么,所以她必须猜测她应该从每个文件中获取什么。
最终的结果与你的初衷不尽相同,是吗?你最终得到了所有的四行;公正的第三方审查者可能认为Sam
在Chris
的作品的顶部和底部都加了一行。
为了对这两个文件进行有根据的合并,你的公正的第三方必须知道这两个文件的common ancestor
。这个共同的祖先就是在三方合并中起作用的第三个修订版。
现在,设想你和你的朋友*也向你的公正的第三方提供了你们开始时的原始文件--共同祖先。她可以将每个新文件的修改与原始文件进行比较,找出你的修改的差异,找出你朋友的修改的差异,并根据每个文件的差异创建正确的合并文件。
那就更好了。而这,本质上,就是Git
以自动化的方式所做的。通过对你的内容进行三方合并,Git
大多数时候都能做到这一点。偶尔,Git
也不能自己解决一些问题,你就得进去帮它一把。但在本书稍后的merge conflicts
工作中,你会接触到这些情况,这比听起来要简单得多。
现在是时候让你自己尝试一下合并了。打开终端,导航到存放你的版本库的文件夹,准备看看合并的实际效果。
合并一个分支¶
在这个场景中,你要查看别人在ideas
仓库的clickbait
分支所做的工作,并将这些修改合并到main
中。
确保你在clickbait
分支上,执行以下命令(如果你还没有这样做的话):
git checkout clickbait
执行下面的命令,看看这个分支上有哪些你想合并回main
的提交:
git log clickbait --not main
这个小工具很好用,因为它能告诉你"哪些提交只是在clickbait
分支中,而不是在main
中?"只要执行git log
,就会显示这个分支的所有历史,直到最初创建main
分支,这对你的目的来说信息太多。
你会看到下面的输出:
commit e69a76a6febf996a44a5de4dda6bde8569ef02bc (HEAD -> clickbait, origin/clickbait)
Author: Chris Belanger <chris@razeware.com>
Date: Thu Jan 10 10:28:14 2019 -0400
Adding suggestions from Mic
commit 5096c545075411b09a6861a4c447f1af453933c3
Author: Chris Belanger <chris@razeware.com>
Date: Thu Jan 10 10:27:10 2019 -0400
Adding first batch of clickbait ideas
好吧,有两个改动需要合并回来;我想你最好在你的网站失去更多流量之前,赶紧把这些点击率高的想法合并起来。
要看这个分支中的新文件的内容,执行以下命令:
cat articles/clickbait_ideas.md
这里面有一些伟大的想法,是肯定的。
回想一下,合并是pulling in changes
另一个分支上的修改的行为。在这个例子中,你想把clickbait
上的修改拉到main
分支。要做到这一点,你必须先在main
分支上。
执行下面的命令,移动到main
分支:
git checkout main
现在,你在另一个分支中看到的articles/clickbait_ideas.md
里有什么?再次执行同样的命令:
cat articles/clickbait_ideas.md
里面什么都没有。没关系--你很快就会用从clickbait
分支合并来的想法填满这个文件。
现在你回到了main
分支,准备从clickbait
分支拉入修改。执行下面的命令,将clickbait
的修改合并到main
:
git merge clickbait
哦,糟糕,你又回到了Vim
中。好吧,至少Git
为你创建了一个漂亮的默认信息:Merge branch 'clickbait
。对于这次合并来说,这些细节已经足够了,所以只要接受这个提交信息并退出即可。
- 按
:
(冒号)进入命令模式。 - 输入
wq
并按回车键,写下这个文件并退出Vim
编辑器。
一旦你退出Vim
,Git
就会为你启动合并操作并提交该合并,而且很可能在你知道之前就已经完成了。
现在,你可以用git log --oneline --graph --all
来看看Git
在这时对仓库的图形表示:
* 55fb2dc (HEAD -> main) Merge branch 'clickbait'
|\
| * e69a76a (origin/clickbait, clickbait) Adding suggestions from Mic
| * 5096c54 Adding first batch of clickbait ideas
* | 477e542 Adding .gitignore files and HTML
* | ffcedc2 Adds all the good ideas about management
* | 8409427 Removes terrible live streaming ideas
* | 67fd0aa Moves platform ideas to website directory
* | 0ddfac2 Updates book ideas for Symbian and MOS 6510
* | 6c88142 Adding some tutorial ideas
* | ce6971f Adding empty tutorials directory
* | 57f31b3 Added new book entry and marked Git book complete
* | f65a790 (origin/main, origin/HEAD) Updated README.md to reflect current working book title.
* | c470849 (origin/master, origin/HEAD) Going to try this livestreaming thing
* | 629cc4d Some scratch ideas for the iOS team
|/
* fbc46d3 Adding files for article ideas
* 5fcdc0e Merge branch 'video_team'
|\
| * cfbbca3 Removing brain download as per ethics committee
| * c596774 Adding some video platform ideas
| * 06f468e Adding content ideas for videos
* | 39c26dd I should write a book on git someday
* | 43b4998 Adding book ideas file
|/
* becd762 Creating the directory structure
* 7393822 Initial commit
你可以在图的顶部看到,Git
已经将你的clickbait
分支合并到了main
,而且HEAD
现在已经升到了最新的修订版本,也就是你的合并提交。
如果你想证明该文件现在已经被带入main
分支,请执行以下命令:
cat articles/clickbait_ideas.md
你会看到文件的内容被吐出到控制台。
快进式合并¶
在Git
中还有一种合并方式,被称为fast-forward
合并。为了说明这一点,回想一下上面的例子,你和你的朋友正在处理一个文件。你的朋友走了(可能是被谷歌或苹果公司雇佣了,幸运的家伙),你现在一个人在做这个文件。
一旦你完成了你的修改,你就把你的更新文件和原始文件(又是共同的祖先)一起交给你的公正的第三方进行合并。她会看共同的祖先文件,以及你的新文件,但她不会看到第三个文件的合并。
在这种情况下,她会在旧文件的基础上提交你的文件,因为没有什么可合并的。
如果从你拿起原始文件并开始工作以来,没有其他人碰过它,那么在这里做任何花哨的事情都没有实际意义。虽然Git
并不懒惰,但它非常高效,只做绝对需要的工作来完成工作。实际上,这正是快进合并的作用。
为了看看这个动作,你将从main
上创建一个分支,做一个提交,然后将分支合并到main
上,看看快速合并是如何工作的。
首先,执行以下命令,确保你在main
分支上:
git checkout main
现在,创建一个名为readme-updates
的分支,以保存对README.md
文件的一些修改:
git checkout -b readme-updates
Git
会创建该分支并自动将你切换到该分支。现在,用你喜欢的文本编辑器打开README.md
,在文件的末尾添加以下文字:
This repository is a collection of ideas for articles, content and features at raywenderlich.com.
Feel free to add ideas and mark taken ideas as "done".
保存你的修改,并返回到终端。用下面的命令将你的改变进行到底:
git add README.md
现在,用适当的信息提交这个阶段性变化:
git commit -m "Adding more detail to the README file"
现在,把这个改动合并到main
。记住 - 你需要在你想把修改拉到的分支上,所以你必须先切换到main
:
git checkout main
现在,在你合并这个改动之前,看看Git
的仓库图,使用--all
标志查看所有分支,而不仅仅是main
:
git log --oneline --graph --all
看一下结果的前两行:
* 78eefc6 (readme-updates) Adding more detail to the README file
* 55fb2dc (HEAD -> main) Merge branch 'clickbait'
Git
并没有将其作为分支中的一个分叉--因为它不需要这样做。就像你在上面的例子中看到的单个文件,这里没有必要合并任何东西。这就引出了一个问题。如果这里没有什么可合并的,那么提交的结果会是什么样子呢?
是时候去看看了! 执行下面的命令,将readme-updates
合并到main
:
git merge readme-updates
Git
会在输出中告诉你它已经完成了快进式合并:
~/GitApprentice/ideas $ git merge readme-updates
Updating 55fb2dc..78eefc6
Fast-forward
README.md | 4 ++++
1 file changed, 4 insertions(+)
你会注意到,Git
并没有调出Vim
编辑器,提示你添加提交信息。稍后你就会知道为什么会这样了。首先,用下面的命令看一下仓库的结果图:
git log --oneline --graph --all
仔细看一下结果的前两行。看起来没有什么变化,但看看现在HEAD
指向哪里:
* 78eefc6 (HEAD -> main, readme-updates) Adding more detail to the README file
* 55fb2dc Merge branch 'clickbait' into main
在这里,Git
所做的只是把HEAD
标签移到你的最新提交。这是有道理的;如果没有必要,Git
不会创建一个新的提交。因为在这种情况下,没有什么需要合并的,所以只需要把HEAD
标签移过去就好了。这就是为什么Git
没有提示你在Vim中输入提交信息来进行快进合并。
强制合并提交¶
如果你不希望Git
这样做的话,你可以强迫它不把这次合并当作一次快进合并。例如,你可能遵循一个特定的工作流程,在构建前检查某些分支是否已经合并到了main
。
但如果这些分支是快速合并的结果,就所有的意图和目的而言,它将看起来像是直接在main
上做的修改,但事实并非如此。
要强迫Git
在不需要的时候创建一个合并提交,你只需要在merge
命令的末尾加上--no-ff
选项。本章的挑战将让你创造一个快进的情况,并看看合并提交和快进合并的区别。
Note
为什么你不总是想要一个合并提交,尤其是当分支和合并是Git
中如此廉价的操作时?把HEAD
往前移有什么意义?总是有一个合并提交不是更清楚吗?
这个问题的政治含义就像古老的PC
与Mac
的争论、Android
与iOS
的争论,或者猫与狗的争论(如果你想知道的话,答案是"狗")。
这在有多个贡献者的大型软件项目中变得尤为重要,因为在这些项目中,你的提交历史可能有成千上万的提交。合并提交可以被看作是保留了一个功能或错误修复分支的历史背景;很明显,你做了分支、修复,然后再合并回来。相反,有很多分支和合并提交--尤其是隐式合并提交,你在本书后面会遇到--会使版本库的历史更难读懂。
这里没有真正的"正确"答案;但不要相信互联网上那些声称"合并提交是邪恶的"的人,因为它们不是。Git
的工作是尽力记录你的仓库发生了什么,你的工作流程不一定要为了确保你的提交历史是线性的、干净的而改变。然而,你无疑会和两边的团队合作,所以只要你了解Git
的合并提交,你就会做得很好,无论你的团队拥护哪种工作流程。
挑战¶
挑战:创建一个非快进式的合并¶
在这个挑战中,你要创建一个新的分支,再次修改README.md
文件,提交到你的分支,然后以非快进合并的方式将该分支合并到main
。
这个挑战需要以下步骤:
- 确保你在
main
分支上。 - 创建一个名为
contact-details
的分支。 - 切换到该分支。
- 编辑
README.md
文件,在文件末尾添加以下文字:"联系。support@razeware.com"。 - 保存你对该文件的编辑。
- 将你的修改分阶段进行。
- 提交你的修改,并附上适当的提交信息,如"添加
README
联系信息"。 - 切换回
main
分支。 - 调出版本库的图表,别忘了使用
--all
选项来查看所有分支的历史。注意main
和contact-details
在图中的样子。 - 使用
--no-ff
选项,合并contact-details
中的修改。 - 在
Vim
的提示下,在合并信息中输入一些适当的内容。如果有必要,使用上面的小抄来帮助你在Vim
中导航。 - 再次调出版本库的图表。你怎么知道这是一个合并提交,而不是一个快进提交?
如果你被卡住了,或者想检查你的解决方案,你可以在本章的challenge
文件夹下找到这个挑战的答案。
关键点¶
Merging
将一个分支上的工作与另一个分支上的工作相结合。Git
执行three-way merges
来合并内容。ours
指的是你想拉入的分支;theirs
指的是有你想拉入的变化的那个分支。git log <theirs> --not <ours>
显示你要合并的分支上有哪些提交,而这些提交还没有在你的分支中。git merge <theirs>
将"他们"分支上的提交合并到"我们"分支。Git
会自动为您创建一个合并提交信息,并让您在继续合并前编辑它。- 当你从"他们的"分支中分离出来后,"我们的"分支没有任何变化时,就会发生
fast-forward
合并,并导致没有合并提交。 - 要防止快进合并,并创建一个合并提交,请在
git merge
中使用--no-ff
选项。
接下来去哪?¶
如果说分支是Git
的"阴",那么将分支合并到一起就是"阳"。虽然概念很简单--把你的修改和他们的修改结合起来--但在实践中,人们很容易在Git
中被绊倒,因为合并并不总是像你所想的那样。
下一章,与远程同步,将带你超越本地环境,告诉你如何将本地修改与服务器上的内容同步。