本文介绍Git里强大的分支功能。您将学习如何从开发主线中分离出来独立地处理其他工作,如何比较分支以查看它们的差异,如何合并它们。我们将讨论不同的合并技术,如三向合并(three-way merging)、快进合并(fast-forward merging)、压缩合并(squash merging)和变基合并(rebase merging)。我们还将讨论解决合并冲突、撤销错误的合并以及使用Stashing和Cherry Picking等工具。
什么是分支
从概念上讲,分支可以创建独立工作区,不会弄乱工作主线。

比如我们有稳定的master主线,同时又需要开发一些新特性,如果直接在主线上开发,会使主线变得不稳定。于是创建分支独立进行开发,分支的开发不会影响主线。当分支的功能代码经过严格测试没问题后,我们可以将分支的功能合并到主线。

利用分支功能,我们可以使每一个新加入团队的人员从稳定的代码(主线)开始工作。
Git使用HEAD指针指向当前的工作分支,HEAD是很小的一个文件,它标识了当前分支的名称等相关信息。当我们要切换分支时,Git就通过修改HEAD的方式来实现。

Git的分支功能与其他版本控制系统是不同的,比如SubVersion,它的分支是对原分支的内容进行全部复制,当分支越来越多时,速度将变得很慢,同时也浪费磁盘空间。
Git则是通过HEAD指针的方式实现,它的分支切换速度快且成本极低,当需要切换到某个分支时,它将该分支的历史快照记录还原到工作目录,所以Git总是只有一个工作目录。
获取存储库
访问https://pan.baidu.com/s/1CKWdXi9dpz-6Vb0rEkHmAA,提取码是u4zc ,获取venus.zip文件,解压。venus目录作为本文的示例项目。
使用分支
假设我们的系统收到了一个Bug报告,需要进行修复。我们创建一个分支进行处理,分支命名为bugfix。
git branch bugfix
通过 git branch 命令查看分支的情况,也可以通过 git status查看当前的分支。
现在要切换到bugfix分支,有两种方法。一种方法是之前的git checkout 命令,但它兼有其他作用,容易造成混淆。另一种是git switch 命令。
git switch bugfix
分支也可以进行改名,比如我们将bugfix修改成更有意义的bugfix/signup-form,表示修复注册表单的Bug。
git branch -m bugfix bugfix/signup-form

当前的分支是bugfix/signup-form,我们尝试修改audience.txt文件。

将修改暂存并提交。
git add .
git commit -m “Fix the bug that prevented the users from signing up.”
通过 git log –oneline 可以查看到,HEAD指向bugfix,同时也能看到master分支,因为bugfix基于master分支发展的。
我们切换回master分支,用git log –oneline是看不到bugfix分支的,当然如果想看到的话,加上 –all 参数即可。

打开audience.txt文件,发现并未改变。说明创建的bugfix分支是独立隔离的并不影响主分支master的代码。

当Bug修复完成,我们要将该分支合并到主分支master,然后删除bugfix分支,使用 git branch 的 -d 参数。
git branch -d bugfix/signup-form
如果需要强制删除,可以使用大写的 -D 作为参数。
git branch -D bugfix/signup-form
这里我们暂不删除。
比较分支
在分支完成任务后,我们会将其合并到主分支master。在合并之前,我们需要了解该分支有哪些修改。使用git log 来比较差异。
git log master..bugfix/signup-form
如果我们想查看修改的内容,可以使用git diff 命令。
git diff master..bugfix/signup-form
当前我们处于master分支,可以省略掉master。
git diff bugfix/signup-form
如果我们仅想了解合并bugfix分支后有哪些文件受影响,可以加上–name-only 或 –name-status 参数。
git diff –name-status bugfix/signup-form

暂存
切换分支时,目标分支的最后一次提交的快照将被恢复到工作区。如果工作区正在修改尚未提交,切换分支是不被允许的。
我们来测试一下,修改audience.txt,然后使用git switch切换到bugfix/signup-form,提示不被允许。

我们可以将工作区的内容通过 git stash push 命令暂存,-m 参数可以添加说明信息。
git stash push -m “New tax rules.”
这里要注意,新建文件是不会暂存的。我们创建newfile.txt文件,再尝试暂存,发现是不成功的。我们需要添加 -a 参数,可以跟之前的 -m 合起来变成 -am。

如上图,我们看到,查看暂存列表的命令是 git stash list。现在可以切换分支了,git switch bugfix/signup-form。在目标分支处理完成后,切回master分支,需要将暂存的内容恢复。
使用 git stash show stash@{1} 或 git stash show 1 可以查看 ID为 1 的暂存。git stash apply 1 可以恢复暂存区的内容。

如果我们要删除暂存区的内容,可以具体删除,比如
git stash drop 1
也可以删除全部暂存区的内容
git stash clear
合并
合并是将某个分支的修改带到另一个分支。有两种合并方式:
快进合并(Fast-forward merge)、三方合并(Three-way merge)。
先说快进合并。当目标分支(例如 master
)没有任何新的提交,而源分支(例如 bugfix)上的提交是在目标分支的基础上进行的,Git 会将目标分支直接“快进”到源分支的最新提交。这种合并不会生成一个新的合并提交。


想像一个场景:我们要对某个文件夹(假设名称为master)里的多个文件进行升级修改。首先复制该文件夹,然后在复制的文件夹(名称为bugfix)里进行相关修改。修改完成后,我们可以选择将修改好的文件复制回master。如果文件很多,这个复制过程是需要一些时间的,由于master没有修改,我们可以选择更简单的方法,直接将bugfix改名为master。在Git中,快进合并就类似这个场景。
再说三方合并。当目标分支和源分支各自都有独立的提交时,Git 会基于这两个分支的共同祖先(即共同的基准提交)进行合并。Git 会自动合并两个分支的改动,三方合并通常会创建一个新的合并提交。


快进合并
看一个快进合并的例子。
首先查看提交的历史记录,通过 –all –graph可以看得更清楚。
git log –oneline –all –graph
要想将bugfix分支合并到master,首先得切换到master分支。
git branch master
然后通过git merge命令进行合并。
git merge bugfix/signup-form
即便master未作任何修改,也是可以禁用快进合并的,有些情况下,这是有好处的。我们来看一个例子。
先创建一个分支bugfix/login-form并切换到该分支,我们可以使用git switch并加 -C 参数将2个步骤合二为一。
git switch -C bugfix/login-form
修改 toc.txt,并进行暂存和提交,比如命名为”Update toc.txt”。通过git log –oneline –all –graph 能看到相关的分支情况。现在我们使用非快进合并将其合并到master。
首先,切换到master:git switch master。
然后,使用git merge 加 –no-ff 表示通过非快进合并进行合并。
git merge –no-ff bugfix/login-form
这样,即便快进合并是可行的,也创建一个新提交合并bugfix和master的修改。我们可以查看到这个合并的结果。
git log –oneline –all –graph

对于是否禁用快进合并,有两种截然不同的意见。反对禁用快进合并的一方认为,禁用快进合并会污染历史记录;而支持禁用快进合并的一方认为,禁用快进合并能真实地反应历史记录,在撤销合并操作时能体现出这个优势。
如果您的团队要求禁用快进合并,又怕在合并时忘输 –no-ff,可以进行对当前存储库进行设置。
git config merge.ff false
或者全局设置
git config –global merge.ff.false
三方合并
创建新分支 feature/change-password。
git switch -C feature/change-password
然后在新分支上添加一个文件change-password.txt文件并提交快照。
回到master分支,修改objectives.txt文件,也进行提交。
这样 feature/change-password分支与master就不在直线上。合并就使用了三方合并,它们与两个分支共同的祖先进行比较,然后进行合并。
git merge feature/change-password
具体操作过程如图。


查看已合并和未合并的分支
分支合并后,应该及时删除分支,否则未来可能会造成混乱。我们要能查看哪些分支是合并的,哪些是未合并的。
首先查看哪些分支已被合并。
git branch –merged
列出的分支是已合并的,我们可以安全地删除它,使用 -d 参数,如
git branch -d bugfix/login-form
同样的,也有另外一个命令用于查看哪些分支尚未被合并。
git branch –no-merged

合并冲突
合并是有可能冲突的,一般有三种情形况会产生冲突:不同的分支修改同一下文件;一个分支修改文件,另一个分支删除文件;两个分支同时添加相同的文件。

遇到冲突时,Git不能进行合并,我们就需要介入并决定如何处理。
看一个例子。
创建并切换到新分支bugfix/change-password,然后对change-password.txt文件进行修改,并进行提交。
再切换回master分支,同样对change-password.txt文件进行修改,也进行提交。
当前我们在master分支,这里进行合并bugfix/change-password分支,Git就会提示有冲突。

使用vscode打开change-password.txt,可以非常直观地看到冲突,同时vscode还提供了相关操作供我们选择,很方便。

使用 git status 查看,也能清楚看到冲突。
我们手动编辑来处理冲突,这里就同时保留两个分支的修改。一般而言,处理冲突只选择保留或不保留某个分支的修改,不要添加新修改,会造成混乱。
编辑完成后,我们暂存change-password.txt。再使用 git status查看,发现冲突已消失了。然后用 git commit 进行提交完成合并。

图形合并工具
前面我们使用过VSCODE的图形界面进行合并冲击,还有其他一些图形工具,比如kdiff、P4Merge、WinMerge,其中WinMerge只适用于Windows操作系统。
我们拿P4Merge举例。
首先访问官网下载安装。
安装完成后,还需要进行配置。
git config --glabal merge.tool p4merge
git config --global mergetool.p4merge.path "/Applications/p4merge.app/Contents/MacOS/p4merge"
注意mergetool的路径一定要正确,我们的路径是MacOS的路径,如果是Windows操作系统,就得设成类似这样”C:\Program File…”的路径。
完成配置后,在需要进行合并时,我们可以使用命令:
git mergetool
Git会弹出p4merge工具,允许我们进行选择具体的合并操作。这里就不再演示。
完成合并后,记得要像之前一样进行提交,也就是“git add . ”以及 “git commit ”操作。
中止合并
前合并发生冲突时,如果一下子不能确定如何解决的话,可以中止合并并返回到开始合并前的状态。使用的命令就是
git merge –abort
撤销错误的合并
合并后如果出现了问题,如何撤回?有两种方法。一种是删除提交,相当于重写历史提交,这种方法在只有本地存储库时才可行,如果有团队一起共享存储库协同工作,则不建议使用此方法。另一种是恢复,它在下一个提交中取消所有的修改。
先来看删除提交。如图所示,它是将当前的分支的快照设置成未合并前的快照点上。


现在,原合并的提交没有任何提交指向它,相当于就是存储库的一个废弃提交,Git会自动查找这些废弃提交并进行清除。
实际操作下。
git reset –hard HEAD~1
git reset我们使用 –hard参数,它有三个选项,soft表示只重置快照,mixed表示重置快照和暂存区,而hard则表示同时重置工作区、暂存区和快照,这样所有的工作环境都是一致的。
合并的提交不再在我们的快照历史中,但它仍在我们的存储库中,我们仍可以使用git reset 恢复它,不过不使用HEAD~1这样的方式,而是直接用提交的id。

再强调一次,这种方法只允许在本地存储库操作,有远程协作使用存储库的就不合适。
再来使用恢复。
git revert HEAD
提示出错,revert恢复不是删除之前的提交,而是创建一个新提交,在新交中取消之前的合并修改。合并前有2个分支,需要指定恢复到哪个分支,使用 -m 参数指定,1 表示主分支master。
git rever -m 1 HEAD
压缩合并(squash merging)
合并时,我们将临时分支比如修复某个Bug的所有的提交都记录下来未必是个好主意,因为这样很可能使主分支master看上去有点混乱。可能临时分支的粒度与主分支的粒度是不一致的,

如上图,B1和B2是临时的提交点,如果将其纳入历史快照,会污染主分支。我们希望将B1和B2合并,然后形成一个提交点,这个就叫做压缩合并。我们给合并的提交点取个合适的描述并与原主分支的粒度相当,这样整个历史快照更简单、更线性、更简洁。

注意,压缩合并不是一个合并提交,因为它没有指向2个父节点,如上图它只指向了master分支。后面我们将删除Bugfix分支,只留简单干净线性的历史记录,这是压缩合并的优点。
这并不是说,所有场合都该使用压缩合并。一般而言,我们只在短期的分支中才使用压缩合并,比如Bug修复或功能排错。
我们来实际操作下。
创建新分支bugfix/photo-upload并切换到该分支。
git switch -C bugfix/photo-upload
在新分支进行文件比如audience.txt并进行提交。
echo bugfix >> audience.txt
git commit -am “Update audience.txt”
再在新分支修改文件比如toc.txt并再一次提交。
echo bugfix >> toc.txt
git commit -am “update toc.txt”
切换回主分支master,并进行压缩合并。压缩合并加上 –squash参数。由于–squash与 –no-ff(前面章节全局设置了git config –global merge.ff false)不能同时使用,我们临时在在本存储设置为merge.ff为only(如本存储后续需要修改则可以键入命令git config merge.ff false)。
git switch master
git config merge.ff only
git merge –squash bugfix/photo-upload
压缩合并后,修改只有暂存区生效,还需进行提交。
git commit -m “Fix the bug on the photo upload page.”
然后,我们可以检查已合并的分支,我们会发现新的bugfix/photo-upload并示在已合并的清单里。
git branch –merged
而通过查看未合并的分支,我们能看到该新分支。为了避免混乱,我们可以删除它。
git branch –no-merged
git branch -D bugfix/photo-upload
注意,删除分支我们使用大写的D来强制删除,Git在我们试图删除未合并的分支(使用小写 –d 参数)时会有错误提示。
然后我们查看到整个历史快照,会发现非常整洁和线性化。
git log –oneline –all –graph
压缩合并时也可能会出现冲突,解决冲突的方法与前面介绍的方法是一致的。


变基(rebasing)
如下图,Master分支与Feature分支存在分叉,即在共同的父节点基础上都有了新的提交快照,合并时Git会使用三向合并创建一个新提交。

但还有一个选择,就是变基。它将Feature分支的父节点指向Master分支的最后一个快照,如下图。

接着我们通过快进合并将Master指针移至变基后的最后一个快照,提交的历史记录就会非常简洁和线性。

但是,我们必须知道,变基修改了快照的历史记录!变基仅适合于本地存储库,如果有多人共享存储库进行团队协作开发,则不应该使用变基。在Git中,是不允许修改F1节点的父节点的,所以变基实际的工作过程是创建与F1 和 F2 一样的两个提交(图中的*F1、*F2),然后再将Feature指向新创建的最后的提交节点。变基后F2节点并非变基前的F2节点。

如果存储库是共享的,其他用户在F2节点创建了新的提交,之后他获取我们变基后的提交快照,由于F2节点实际是不一样的,将会出错。
看实际操作。
首先创建并切换到分支feature/shopping-cart。
git switch -C feature/shopping-cart
做些修改,比如创建cart.txt文件。
echo hello>cart.txt
进行提交。
git add .
git commit -m “Add cart.txt”
查看确认下。
git log –oneline –all –graph
再切换回master。
git switch master
同样地做些修改,比如在toc.txt文件后添加一些文本。并进行提交。
echo hello >> toc.txt
git add .
git commit -m “Update toc.txt”
现在我们要进行合并,有两种选择:三向合并和变基。这里我就选择变基。首先切换回feature/shopping-cart分支
git switch feature/shopping-cart
再执行变基指令,将master进行变基。
git rebase master

查看结果。
git log –oneline –all –graph
我们看到,两个分支不再分叉,他们有一条线性的路径。目前master分支点还指向着变基点,我们需要进行一次快进合并将master指针向前推进。如果因某种原因快进合并失败,可以使用 git reset –hard feature/shopping-cart强制合并。
git switch master
git merge feature/shopping-card
再一次查看。
git log –oneline –all –graph
两个分支都指向了同一个提交。

在实际情况下,变基操作也经常会出现冲突。我们就来看一下冲突。在当前master分支下,修改toc.txt文件并进行提交。
echo ocean >> toc.txt
git commit -am “Update toc.txt”
切换到feature/shopping-cart,同样对toc.txt进行修改并提交。
git switch feature/shopping-cart
echo mountain > toc.txt
git commit -am “Write mountain to toc”
查看快照,现在我们的分支出现了分叉。
git log –oneline –all –graph
切换到feature/shopping-cart,尝试进行变基操作。
git switch feature/shopping-cart
git rebase master
我们发现toc.txt有冲突,解决冲突的方法可以使用之前的方法。当我们解决了冲突,并将修改添加到暂存区。回到终端窗口,让变基操作继续。
git rebase –continue
Git将在master分支顶部创建应用新的提交快照。

当我们查看冲突并不在意冲突,可以使用 skip 选项,跳过当前快照的变基并移到下一个快照。
git rebase –skip
我们还有另一个选项 –abort,如果我们没有足够时间或尚没有能力解决冲突时,可以使用该选项中断变基操作,这将会将我们带回开始变基前的状态。
git rebase –abort
最后需要提醒,有些合并工作会留下一个备份文件,我们可以删除它。
git clean -fd
或配置Git使其不保存备份文件。
git config –global mergetool.keepBackup false
挑选提交(Cherry Picking)
如图目前两个分支Master和Feature。

master分支如果想挑选feature分支的F1节点而不是全部节点加到它的分支上。这就叫做挑选提交(Cherry Picking)。

实际操作。
复制挑选的提交ID,比如 80876e6。
切换到master分支,执行挑选提交。
git cherry-pick 80876e6
如果发生冲突,跟之前处理冲突一样处理,然后进行提交。

从另一个分支挑选文件
如果我们只对另外分支的某个文件感兴趣,而不是整个快照,可以从另一个分支挑选文件。我们看一下。
首先创建示例分支feature/send-email
git switch -C feature/send-email
编辑某个文件比如toc.txt,交提行提交。
echo river > toc.txt
git commit -am “Update toc.txt”
然后切换回master分支,并从示例分支中获取toc.txt文件,使用的命令是之前学过的git restore。
git switch master
git restore –source = feature/send-email — toc.txt
这样,master分支的工作区将从示例分支里获取了toc.txt文件。后续进行暂存和提交。
图形界面使用分支
在vscode中使用分支,可以使用插件 GitLens。
如果想更直观查看分支,可以使用VScode的Git Graph插件或 直接使用GitKraken。
具体使用不赘述,相信有了分支的知识,图形界面使用起来非常容易理解。
小结
本文学习了如何从开发主线中分离出来独立地处理其他工作,如何比较分支以查看它们的差异,如何合并它们。我们讨论了不同的合并技术,如三向合并(three-way merging)、快进合并(fast-forward merging)、压缩合并(squash merging)和变基合并(rebase merging)。我们还讨论了解决合并冲突、撤销错误的合并以及使用Stashing和Cherry Picking等工具。