git 分支操作

概述

本文介绍使用 git 版本控制系统过程中所涉的各种分支命令。

几乎所有的版本控制系统都以某种形式支持分支。使用分支便意味着你可以把你的工作从开发主线上分离开来,
以免影响开发主线。在很多版本控制系统中,这是一个略微低效的过程——常常需要完全创建一个源代码副本。而对 Git 而言,其分支处理过程极其轻量。正是由于这一点,其分支模型也称为 Git 的 “必杀技特性” 。

Git 分支模型实战网址:https://learngitbranching.js.org/?NODEMO=&locale=zh_CN。

本地分支

正如 git 起源 中所言,每次提交将会产生一个项目快照。另外,每次提交还会产生一个提交对象 ,该对象保存项目快照引用、提交人的姓名和电子邮箱、父提交对象引用等信息。经过若干提交,我们将会得到一张有向无环图,该图以提交对象为顶点,按照父提交对象引用进行构建边。

图一:提交后形成的有向无环图

在 Git 之中,分支实质上就是指向该有向无环图中提交对象的可变指针,可变指针所指之处,即为当前分支最新状态。当构建 Git 仓库后,其默认存在初始分支 (主分支) master。每进行一次提交,有向无环图中就增加一个顶点及若干边,当前分支所示的可变指针便向前移动以使其指向当前分支的最新状态。

图二:master分支

分支创建

命令 git branch <branchName> 即可创建新分支,该过程本质上就是:创建一个可变指针,并令其与当前分支的可变指针指向相同。需要注意的是:创建分支后,当前项目仍然处于当前分支,而不会自动切换到新分支

图三:创建分支

HEAD 是 Git 中的特殊指针,它指向当前项目所在分支。之所以使用它,原因在于:需要确定当前项目具体在哪个分支之上。

分支切换

命令 git checkout <branchName> 用于切换至指定分支。如下图所示,切换分支之后,HEAD 指针便指向分支 testing

图四:切换分支

此时,如果我们在当前分支中作一次提交,将会得到如图结果。可以看到:当前项目所在分支 testing 向前移动一步,而分支 master 并无任何变化。

图五:testing分支作一次提交

随后,我们切换至分支 master 并完成一次提交,将会得到如图结果。可以看到:当前项目所在分支 master 向前移动一步,并与分支 testing 产生实际意义上的分支。

图六:master分支作一次提交

如果我们希望创建分支的同时,能够自动切换至新创建的分支,可使用命令 git checkout -b <branchName>

分支合并

命令 git merge <branchName> 用于将分支 branchName 合并至当前分支。

分支合并存在两种情况:

  • 当前分支为分支 branchName 的前驱

    这种情况的合并比较简单,只需要令当前分支所示指针与分支 branchName 所示指针指向相同即可 (进行合并之时,如果合并情况为此,GIt 将会提示 fast-forward)。

    如图七所示,需要合并分支 hotfix 至当前分支 master,其正好满足此条件,故而只需要令分支 master 所示指针指向 C4 即可。

    图七:分支合并:当前分支为分支branchName的前驱

  • 当前分支不为分支 branchName 的前驱

    这种情况的合并比较麻烦,它会提取当前分支、分支 branchName、两分支公共祖先的项目快照,做一三方合并 (这是最简单的合并策略),随后将合并内容作为当前分支的一次新提交。如果合并过程中发生冲突,需要手动处理冲突。

    图八:分支合并:当前分支不为分支branchName的前驱

    直观来看,应当直接合并当前分支、分支 branchName 的项目快照 (称为两方合并)。但是两方合并是不可行的,原因在于:我们无法根据二者差异决定合并策略 (具体见Post not found: git/gitstudy6)。

分支删除

命令 git branch -d/-D <branchName> 用于删除指定分支。当使用 -d 参数时,如果分支 branchName 已合并至其他分支,则可成功删除;否则由于存在未合并内容,则会删除失败。此时如果需要强制删除,则可使用 -D 参数进行强制删除。

分支查询

命令 git branch 用于查询当前项目的所有分支。

命令 git branch -v 不仅可查询当前项目的所有分支,而且可查看每个分支的最后一次提交信息。

命令 git branch --merged/--no-merged [<branchName>] 用于查询已合并或未合并至分支 branchName (不加参数便是指当前分支) 的分支。

远程分支

远程分支 (remote branch) 指代位于远程服务器中的分支,此节介绍与其相关的两个内容 —— 远程跟踪分支 (remote-tracking branch) 、跟踪分支 (tracking branch)。

通常存在 远程引用 这一概念,它是对远程仓库中分支、标签等内容的引用 (指针)。我们可借由命令 git ls-remote 显式获取远程引用的完整列表,也可使用命令 git remote 显式获取远程分支引用的完整列表。

远程跟踪分支

远程跟踪分支 是本地仓库对远程分支状态的记录,它们属于无法移动的本地引用,并以 <serverName>/<branchName> 形式命名。其作用类似于书签,用于标识该分支在远程仓库中的位置。当我们同步远程仓库内容时,它会自动更新其在远程仓库中的位置信息。

远程跟踪分支可能比较难理解,我们举例说明。

假定我们从 git.ourcompany.com 服务器中克隆项目至本地,git clone 命令会自动完成如下工作:设置远程服务器名为 origin、拉取项目完整内容、创建远程跟踪分支 origin/master 并自动设置其指向、创建本地分支 master 并令其与远程跟踪分支 origin/master 指向相同。克隆完成后,远程仓库与本地仓库可表示如下:

图九:克隆后的远程仓库与本地仓库

随后我们在本地分支 master 做些提交,同时间段又有人在远程仓库的 master 分支做些提交,此时远程仓库与本地仓库可表示如下。可以看到:远程分支 master 发生变动,而远程跟踪分支 origin/master 未曾发生变动。

图十:若干提交后的远程仓库与本地仓库

既然远程仓库内容有所变动,我们使用命令 git fetch 拉取其至本地,此时远程仓库与本地仓库可表示如下。可以看到:Git 会抓取本地仓库所没有的内容,随后更新远程跟踪分支 (如果存在相应的远程跟踪分支)。

图十一:拉取内容后的远程仓库与本地仓库

此时可使用命令 git merge 合并分支 origin/master 与分支 master,从而更新本地分支 master;当然也可使用命令 git checkout -b branchMaster origin/master 新建分支 branchMaster,并令其与远程跟踪分支 origin/master 指向相同。

图十二:与远程跟踪分支合并或新建分支与远程跟踪分支同步

跟踪分支

基于远程跟踪分支 checkout 出来的本地分支便是 跟踪分支,该分支所对应的远程跟踪分支称为它的上游分支。跟踪分支是与远程分支有直接关联的本地分支,如果在跟踪分支之上输入 git pull 命令,Git 能够自动识别应当去哪个远程服务器的哪个分支上抓取内容,然后将其合并至当前分支。值得一说的是,使用命令 git clone 克隆项目时,它会基于远程跟踪分支 origin/master 自动创建跟踪分支 master

跟踪分支的创建命令之前已经提及,就是 git checkout -b <branchName> <serverName/branchName>。由于此命令比较常用,故而存在两个简化命令,其一: git checkout --track <serverName/branchName>,其二:当使用命令 git checkout <branchName> 之时,如果该分支不存在、但是存在同名远程分支,则 Git 会自动创建相应的跟踪分支。

如果已经创建分支,但是需要将其关联至某远程分支,可使用命令 git branch -u/--set-upsteam-to <serverName/branchName> 设置当前分支的上游分支。

如果需要查看各分支的跟踪信息,可使用命令 git branch -vv。根据如下例子可以看到:分支 testing 没有跟踪任何远程分支;分支 master 跟踪远程分支 master,且两者处于同步状态;分支 serverfix 跟踪分支 server-fix-good,且存在 ahead 3,behind 1,此表示当前分支存在 3 个提交未曾推送至远程分支、远程分支存在 1 个提交未曾合并至本地分支。

1
2
3
4
$ git branch -vv
testing 5ea463a trying something new
master 1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it

此命令并不涉及与远程分支的通信,其比较对象为最后一次从远程分支所获取的服务器数据 (通过命令 git fetch 做到)。

变基

变基 (rebase) 属于分支合并中较为高级的操作,我们在此简单介绍。

简单情景

假定存在如图提交记录:

为合并两个分支,我们通常做法是采用命令 git merge,它会进行一次三方合并,并将合并结果作为一次提交。此时,提交记录如下:

可以看到,基于命令 git merge 的合并方式存在一大缺陷:使得提交记录不线性。为使得记录线性化,我们可使用命令 git rebase <topicBranch> ,它用于将当前分支上的所有修改移动至特定分支之上,这种操作便称为 变基

另有变基命令 git rebase <baseBranch> <topicBranch>,它用于将指定分支上的所有修改移动至特定分支之上。

以上图为例,简述变基原理:首先找到当前分支 experiment 和分支 master 的公共祖先 C2,随后将公共祖先 C2 至当前分支所指之处的修改保存至临时文件之中,切换至分支 master 并将临时文件中的修改依次添加至该分支,随后再切换回分支 experiment 。经变基操作后,其提交记录如下:

经变基操作后,提交 C4 等都仍然在,只是当前分支通过日志记录查不到

此时切换至分支 master,执行命令 git merge experiment,可以发现它属于 “fast-forward”,合并过程简单,只需调整相关指针即可。可以看到:基于变基操作实现的合并结果是线性的。此时,其提交记录如下:

复杂情景

上例仅涉及一个分支,比较简单。接下来我们看一个涉及多分支的例子。

假定存在如图提交记录,现在我们希望将分支 client 变基至分支 master,同时要求涉及分支 server 的任何修改不动。

基于此要求,我们可使用命令 git rebase --onto master server client 做到,该命令意思为:找到分支 client 与分支 server 的公共祖先,随后将公共祖先至分支 client 所指之处的修改移动并添加至分支 master。经过变基操作后,其提交记录如下:

风险

只要满足准则 —— “如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基”,变基操作就不会引人风险,否则就会引入麻烦。

我们同样通过例子说明该麻烦。假定我们从远程服务器克隆项目并对此执行若干提交,远程仓库与本地仓库的提交记录如下:

随后,其他人向远程服务器做若干提交,我们拉取其内容并合并至本地。此时,远程仓库与本地仓库的提交记录如下:

接下来,之前那个人决定使用变基操作替代合并操作,随后使用本地内容强制覆盖远程服务器中内容。如果我们此时拉取远程服务器中内容至本地,由于命令 git fetch 会将当前仓库中并不存在的内容拉取下来,故而会得到这样的提交记录:

为上传本地仓库至远程服务器,我们首先需要合并拉取下来的内容,这样我们就会重复合并提交 C4 (此为麻烦一,混乱日志信息)。此时,远程仓库与本地仓库的提交记录如下:

此时如果我们将其上传至远程服务器,之前那个人不想要的提交 C4C6 又会再次出现在远程服务器 (此为麻烦二)。为避免这种情况发生,最初我们应当将分支 master 变基至远程跟踪分支 teamone/master,这样就可以避免上面所述麻烦。此时,远程仓库与本地仓库的提交记录如下:

对远程服务器中分支采用变基操作有两种命令,其一:首先使用命令 git fetch 获取数据,随后使用 git rebase 执行变基操作;其二:直接使用命令 git pull --rebase 代替前面的两条命令。

其他应用

命令 git rebase 不仅可作用于不同分支以执行变基操作,也可作用于当前分支的若干提交以实现压缩提交功能。实现压缩提交功能的具体命令有 git rabse -i HEAD~<num>,它可压缩当前分支的最近 <num> 次提交为一次提交;git rebase -i <commit-id>,它可压缩当前分支提交 commit-id 之后的所有提交为一次提交。

变基vs合并

变基和合并均可实现分支合并,实际应用中应当着重选择哪种方式?

首先,我们需要回答问题:提交记录的作用何在?通常有两种观点:1. 提交记录用于记录实际发生过什么,它属于历史文档,不应当随意篡改;2. 提交记录用于记录项目开发过程中有意义的提交。如果持有观点 1,那么应当仅使用合并、杜绝使用变基以实现分支合并;如果持有观点 2,那么只有在满足准则情况下才使用变基、否则使用合并以实现分支合并。、

除分支合并功能外,变基还可压缩提交。如果持有观点 1,则不应使用变基以压缩提交;如果持有观点 2,则可适当使用变基以压缩提交,从而使提交记录更为清晰。

拓展

命令 git cherry-pick 与命令 git rebase 类似,它不是将当前分支上全部提交作用于特定分支之上,而是将当前分支上某些提交作用于特定分支之上。其具体命令为 git cherry-pick <commit-id> ,应用 ID 为 <commit-id> 的提交至当前分支之上。