第3章 管理代码——从分布式版本控制系统Git出发
3.1 版本控制系统构建与管理——Git
代码是项目的核心资产,代码的管理是保证产品质量的重要因素之一。尤其在较大规模的IT公司,由于产品线众多、项目规模大、参与开发的人数多、产品迭代迅速,对各开发团队及工程师间的代码协作开发能力提出了更高的要求。代码版本控制系统(Version Control System)是软件开发项目中不可或缺的生产力工具,用于管理项目中的代码、记录更改历史、回溯原有版本,是敏捷式开发、持续集成等的基础。流行的分布式版本控制系统Git具有灵活快速、出色的代码合并及跟踪能力、适合多种开发模式等特点,已被越来越多的项目团队应用在各种规模的产品开发中。本节着力介绍Git的基本使用方法及对应的经典开发模式,然后大致介绍如何利用公共代码托管平台GitHub和自建代码库GitLab优化代码版本管理流程,并将以实例形式提供一系列代码维护和定制的经验和方法。
3.1.1 Git如何工作
2005年,Linus Torvalds为了更高效地管理Linux内核开发工作,自己开发了一个开源版本控制工具。它与常用的版本控制工具SVN、CVS不同,采用了分布式版本控制理念,并且号称不需要服务器端软件支持,这就是Git。Git发展到现今,已经支持多操作系统平台和IDE,被越来越多的商业软件提供商或者开源项目采用,已成为应用最为广泛的版本控制系统。
在进入正式介绍Git前,我们先简单解释几个后文中会反复提到的Git相关术语:
(1)仓库(Repository)
仓库是指某一时刻自己或他人的代码工作区(Working Tree)的状态,包含了可追溯的所有分支(Branch)和提交(Commit)信息,同时也包括了用于指定现有工作区所在的分支和提交状态的头(HEAD)信息,从Git仓库克隆出的每一个副本也是一个完整的仓库。仓库在本书中有时也会写为代码库、版本库等。
(2)工作区(Working Tree)
工作区是与仓库关联的文件系统,包括所有文件和子目录。
(3)暂存区(Staging Area)
在从工作区向仓库提交更改(git commit)前,这些更改会先保存(git add)在暂存区内。暂存区包含了工作区的一系列更改快照,这些快照可以用来创建新的提交。
(4)提交(Commit)
提交可以简单地理解为某一次的代码改动后当前工作区的快照。每个提交都会产生一个新版本的唯一标识,在以后任何时候都可以通过此标识快速回溯到此时刻的代码状态。所有提交也即构成了每个分支的追溯信息。
(5)分支(Branch)
每个提交都会到达一个指定分支,因此,分支可以理解为独立拥有自己提交集合的代码线。仓库可拥有一个或多个分支,分支之间开发相互独立。可通过git merge合并其他分支的代码。
(6)头(HEAD)
头可以理解为一个象征性的指针,指向当前选择的分支,由commit表示。
(7)标记(Tag)
标记是为了标识一个特殊时间点的分支版本状态。在发布某个版本重要开发节点时,会经常在主分支打上相应标记,以后可以方便地切回到标记的版本。
(8)主分支(Master)
Master分支是建立仓库后产生的默认分支,通常其他分支的提交更改最终会并入主分支。
利用Git开发的最基本工作流是:开发者先建立本地仓库(通常是远程仓库的克隆),然后在工作区做开发工作,当某漏洞修正完成、某些接口开发完成或者一天开发工作结束时,开发者将这些代码通过git add保存到暂存区。当确认所有更改都已经保存在暂存区后,再将更改通过git commit提交到本地仓库。这个基本工作流可以由图3-1表示。
图3-1 Git基本工作流
Git之所以能够完整克隆仓库并追溯历史内容,要归功于其内容寻址(Content-Addressable)文件系统。任何仓库内部的改动(包括新文件的保存或者文件更新),都会在Git文件系统中保存为一个副本。这个副本即当前文件的完整快照,是一个二进制对象,对象名是计算文件内容后生成的一个SHA1哈希值,计算文件内容而非文件名生成的SHA1哈希值保证了同文件的每个历史版本都能保存为单独的二进制对象。这样,Git文件系统会保存每个文件的所有版本。每次改动时会在Git的暂存区内保存一条记录,这条记录是另一种Git对象——tree对象,对象名也是经过计算得到的SHA1哈希值。tree对象是用来组织二进制对象的数据类型,每个tree对象代表不同时间节点当前仓库内容的快照,它包含一个或多个二进制或子tree对象。git-add所做的工作就是保存记录文件内容的二进制、更新索引并生成tree对象。而git-commit创建了另一种Git对象——包含当前时间点仓库快照的顶部tree对象、作者及提交者信息、时间戳和注释信息的commit对象。同样,commit对象名也是一个唯一SHA1哈希值。由于每个commit对象都指向一个代表某历史状态的tree对象,因此,所有commit就构成了完整的仓库历史记录,再加上二进制、子tree对象指向文件内容的任意版本的完整快照,这样就保证了Git文件系统的完全可追溯性。
了解了二进制、tree和commit对象之后,就比较容易理解HEAD和分支了:假设仓库现在只有默认的master分支,那么当前HEAD就是一个指向master分支的引用标识符,此标识与master分支的最新commit对象相关。当由git-branch创建新分支时,其实就是通过HEAD引用标识符将master分支的最后一次commit的哈希值添加到所创建新分支的引用,那么,新分支的初始状态就是默认分支的当前状态。当用git-checkout切换到新分支时,背后仅仅是将HEAD引用标识符切向到新分支的引用。所有这些对象、引用的交叉对应关系都保存在Git的内容寻址文件系统内,可供之后随时检索。这套系统确保了仓库历史版本的完全可追溯性和控制系统的本地化,为分布式的灵活开发模式奠定了基础。笔者在这里只是大致介绍Git内部原理,仅为抛砖引玉,读者可参考Git官方文档,进行更深入的了解。
如果你刚刚接触Git,读到这里或许对Git工作方式仍然比较困惑,你可能想问:Git到底跟以前常用的SVN有什么本质区别呢?下面举一个简单的例子。
如图3-2上图所示,当用SVN提交一个新的改动文件的版本v4时:
● 从远程SVN仓库先checkout出来当前时间点最新版本v3。
● 本地修改为版本v4。
● 将新版本v4上传到远程SVN仓库。
这是一种非常清晰的提交模式,所有提交历史都是线性的。版本数据库都在中央服务器,每个提交都需要基于远程仓库的最新提交。那么问题来了,如果你在提交v4时,已经有其他开发者更改了v3并提交了新的版本v5(见图3-2下图),你的提交就会被阻止,因为v4基于的v3已不是远程仓库中该文件的最新版本了,SVN会提示你提交前解决代码冲突。SVN对版本库进行统一管理,只有中央服务器拥有完整的版本数据库,客户端仅仅下载远程仓库的工作备份,不包含版本数据库,因此也就不具有独立的版本控制功能。中央服务器一旦离线或宕机,SVN的这种集中式的版本控制方式就不能工作了。此外,SVN是一个增量式的版本控制,它仅记录版本之间的差异,而不是每个版本的完整快照。
图3-2 提交示意图
针对此例,我们看看使用Git时有什么不同,分两种情况讨论。
第一,如果还没有创建本地仓库,开发者的一般操作是:
● 先用git clone将远程仓库克隆到本地仓库。
● 在本地仓库中做开发工作,更改该文件现版本v3至版本v4。
● 用git add将版本v4添加至暂存区。
● 用git commit提交新版本v4至本地仓库。
克隆远程仓库将得到一个完整镜像,包括前面提到的内容寻址文件系统,使得本地仓库具有Git完整的版本控制功能。如果此本地仓库独立承担软件生命周期管理,不再需要远程仓库的代码更新,开发者可以基于本地仓库延续代码开发、构建及测试等工作。如果远程仓库和本地仓库之间还会有代码交换,本地仓库可按需从远程仓库拉取远程代码或者向远程仓库推送本地代码更新。如果针对该文件,远程仓库已经合并了他人提交的最新版本v5,你需要在推送或拉取代码时解决文件内容冲突。
第二,如果已经有本地仓库,项目有开发新功能或修正漏洞的需要,开发者的一般操作是:
● 由git branch创建新的分支,其初始状态是基于v3时间点当前分支的状态。
● 在新分支上开发新功能或修正漏洞的过程中,由于需要,该文件从v3版本更改至v4版本。
● 用git add将版本v4添加至暂存区。
● 用git commit提交新版本v4至新分支,之前的分支上该文件依然处于v3版本。
在新分支上工作的过程中,开发者可以随时切换至之前的分支继续主线开发工作。主线和新功能的开发发生在不同的分支上,它们之间相互隔离,成为完全独立的开发单元,开发者可以基于不同分支的代码状态分别做构建和测试工作。如果新功能开发完成,开发者可将代码更新一次性地合并入主线分支中,合并时需要解决不同分支上相同文件的内容冲突问题。假设在主线开发中,该文件由原来的v3已更新至v5版本,那么合并时就需要手动更改该文件,确保同时兼容v4和v5的代码更新。
注意,SVN的集中式代码控制使所有提交必须通过网络到达远程中央服务器,与其中的当前最新代码做差异化验证;而Git由于仓库的版本控制完备性原因,即使本地与远程仓库或者不同分支之间有代码差异,提交阶段依然不受影响,只是在代码归并时需要解决冲突。可以想象,Git这种灵活的提交方式和不同版本之间的代码交互机制会使其提交历史演化成图3-3所示的有向无环图。
图3-3 Git提交历史有向无环图
3.1.2 Git操作场景
相信读者已经对Git的原理和特性有了基本认识,本节将由浅入深地讲解Git实际的搭建和操作方法,主要涉及以下几点。
● 仓库的初始化。
● 文件新版本的提交。
● 远程协作。
● 常用版本控制命令。
请注意,本书中有关Git的实验都以Linux平台为例。
1.git init初始化Git仓库
假设在本地PC上已经有项目代码,想基于现有项目创建一个Git仓库,供后续更好地进行版本管理。那么可以cd到项目的根目录,通过git init为已有项目初始化Git仓库,自此本地项目就可以记录版本了。git init在整个项目生命周期内只需被执行一次:
当然,也可以在指定空目录下用git init生成一个全新的仓库,用于新项目的开发:
执行git init会生成一个.git子目录,.git是实现Git版本控制的核心目录,包含其独特的文件管理系统,即版本数据库。它的目录结构如下。
可以结合3.1.1小节中所述Git工作原理来观察这个目录:config目录包含仓库的配置信息,可由git config命令更改,我们会在下文详细介绍Git配置;objects目录存放Git对象(blob,tree,commit),是版本数据库的中心;refs/heads包含所有分支的引用,同样,refs/tags包含创建的所有标签的引用;HEAD文件记录当前分支的引用,初始化仓库时会默认创建一个master分支,可以看到HEAD文件的初始内容是:
git init创建本地仓库时,Git仅仅作为版本记录供你的私人项目所用。如果项目需要多人共同协作,彼此间要求代码同步,这就需要建立一个远程中央版本库。其他人执行git clone即可初始化并下载整个版本库至本地,在开发过程中,git pull和git push等命令能保证远程仓库和本地仓库之间的代码互通。我们会在后面详细讲到协作场景。
2.git add和git commit保存代码更新
有了本地仓库,就可以放心地开始项目开发工作了。你能随时向Git提交代码更新、回溯代码历史、管理分支开发单元等。我们首先从保存代码更新说起:类比操作系统层面应用程序的保存,应用程序的保存(Save)实际上是指覆盖一个已有文档或者是创建写入一个新文档,版本控制系统中的保存是对一组文件或者目录改动的集合的归档。对不同的版本控制系统,如Git和SVN,保存的含义也有些许区别。对Git来说,保存实际上就是指“提交”(Commit),只需在本地仓库提交更新,即将新的代码完整快照归档添加到本地版本数据库。SVN的保存也即svn commit,指的是通过网络将更新上传并入中央版本库,只有这样才能将新版本归档。这在Git中更类似于git push,git push是将本地仓库的更新并入远程仓库,以方面项目共同开发者获得你的最新代码。这种区别是由两种版本控制系统的基础架构所导致的,SVN是单点中心管理,本地只是中央版本库的工作目录备份,没有版本控制功能;Git是分布式管理,不依赖于远程中心节点。
开发者会结合使用git add和git commit做代码更新保存,无论你的项目采用哪种开发模式,这两个命令都需要每个Git使用者很好地掌握。
(1)git add
git add将工作区中的更新添加到暂存区,它告诉Git下一次git commit将会包含哪些状态更新。注意,git add阶段并没有实质性地影响Git版本库。
用Git版本控制来进行开发工作其实就是围绕编辑、暂存及提交这个核心工作单元展开的。首先,你需要根据开发需求打开源文件编辑代码。新更新或添加一段代码后,就可以用git add将当前代码状态保存到暂存区。在提交前,可以反复多次编辑代码、执行git add,直到对暂存区中尚未提交的这些新代码满意后,再通过git commit将暂存区中的最新的完整快照提交到Git版本库。如果对暂存区或者提交的代码不满意,还可以通过git reset撤销本次提交或者抹除暂存区快照。
git add的直接作用就是将工作区的代码更新推到Git版本控制系统中独有的暂存区,暂存区可以被想象成工作区和提交历史之间的缓存区域。缓存区到底赋予了Git什么优势呢?我们知道,使用任何版本控制系统的一个重要原则是尽量保证每次创建的提交都是原子性的,即单个提交仅相关某一项任务单元,不同提交间不要有任务的交叉。正是有了暂存区,才不需要一次性把上次提交之后的所有代码更新都在这次commit一起提交,而是可以在逻辑上将不同目的的代码分组提交。试想,在某一次漏洞修正的任务中,你顺手更改了与漏洞不相关的文件中的几个方法名,使它们看起来更合理。当你提交时,可以先将与漏洞修正相关的文件一起git add到暂存区,然后git add修改过方法名的其他文件,再另一次提交。
git add有不同的选项,举几个例子:
将〈file(s)〉指定的单个文件或多个文件的更新添加到暂存区。
将〈directory〉指定的目录下的所有文件更新都添加到暂存区。
以交互式方式逐个展示现有文件的更新(不包括新文件),并提供多种操作选项:可以确认添加、撤销添加、手动编辑更新内容等。
其他的选项和简单说明可以通过git add-h查看。
(2)git commit
当你读到这里时,想必已经对git commit有了大致的了解,笔者在这里单独将其提出做更详细的介绍。Git的核心价值实际是做时间轴事件管理,每个提交所记录的不同时间点的快照是时间轴各节点上的事件单元,所有提交组成了此Git项目的版本历史时间轴。需要深刻认识、也是我们前文反复提到的一点是,Git快照仅向本地仓库提交,而非如SVN那样向远程中央版本库提交,在项目需要并且开发者准备好之前,Git不强制将本地更改同步到远程中央版本库。这使得应用Git展开的开发模式更为灵活,开发者可以在本地任意开发、提交代码。例如,将一个完整的功能开发任务再细分成多个开发小单元,每个开发小单元都作为单独的提交,最终,可以一次性将本地积累的多次提交推送到远端。为了使得远程中央代码库的版本历史更加清晰,开发者在推送前,还可以任意组合或整理本地提交。另一方面,这也使得开发者的开发环境阶段性完全隔离,你只需集中精力思考,以独立的逻辑完成编码工作,不需要考虑其他协作者的潜在相关改动,在方便时再去做与他人代码合并的工作。这样,从整个团队看,就避免了很频繁、很微小的代码改动的相互同步,提升了效率。正如暂存区是工作区和版本库之间的缓存那样,开发者的本地仓库同样可以理解为开发者的项目贡献和远程中央仓库之间的缓存。
此外,git commit捕获的快照是工作区的完整代码状态,SVN提交记录的仅是内容差异。如图3-4和图3-5所示,提交了一个新文件“haha.py”(版本v1)而后连续两次对此文件修改并提交(vv2、v3),在SVN中,第一次提交保存了完整文件,第二次提交记录的是▲1=v2-v1,第三次提交记录的是▲2=v3-v2,而在Git中,每次提交保存的都是当前文件完整内容。这为Git的版本追踪提供了便利,Git可以直接从版本数据库内检索调取完整历史快照,而不需要像SVN那样回溯所有关联的历史差异再逐级计算出所需的版本,这样Git的速度更快。这种快照机制会影响Git从代码分叉、合并到协作工作流程的各个方面。
图3-4 SVN记录内容差异
图3-5 Git记录完整内容
同样,git commit也有不同的选项:
将当前暂存区的内容提交,命令执行后会开启一个编辑窗口,可以输入提交备注。
将暂存区内容提交时由参数-m指定提交备注,不再开启编辑窗口要求输入备注。
提交时由参数-m指定备注的同时,开启编辑窗口,可以再次在窗口中编辑指定备注。
amend是一个很常用的commit选项,它会修改上一次的提交,而不会创建一个新的提交。它将现在暂存区的内容加到上一次提交中,同时会弹出编辑窗口,你可以修改编辑上次提交的备注信息。这在第一次提交出现问题而又不想破坏提交的原子性时很有帮助。
3.Git远程协作场景
Git不仅在个人本地项目的建设开发中具有性能佳、安全性好等优势,它还在多人共同协作的大中型项目中具有强大功能。Git本地仓库的完整性使分布式开发成为现实,并且为协作中的多点同步问题提供了便利。既然讲到远程协作,那我们就从建立一个集中式仓库开始。
(1)git init--bare创建远程仓库及相应配置
这里,我们既提到Git分布式开发,又说要建立集中式仓库,是不是很矛盾呢?从开发角度来看,Git的协作工作模式确实是分布式的,每个开发者本地均有远程仓库的完整备份,开发时只需要集中自己的任务单元,代码的编辑提交都分布在各个开发者的本地服务器;但在商业软件的发布管理中,需要一个仓库集合所有开发者的代码以使它包含所有开发功能,用以构建、测试产品,继而发布给客户。这样来看,这个远程仓库又是集中式的,后文中有时也称为中央仓库。
带有bare选项的git init就会初始化这样的集中式仓库。当创建bare仓库后,可以看到当前目录下没有.git子目录,仅包含.git目录下的所有文件,这说明仓库没有工作区,你不能直接向其提交代码。实际上中央仓库仅用于共享代码——其他关联仓库通过网络向它推送代码或从它那里下载代码。bare仓库集中式地存储所有开发者的代码贡献,而真正的开发工作都发生在开发者的本地仓库中。
首先,登录用来集中管理代码的远程服务器;然后创建仓库的根目录,按惯例,bare仓库的根目录一般以.git为扩展名;最后在刚刚创建的根目录下执行git init--bare。在创建仓库前最好新添加一个名为git的独立用户,当其他开发人员克隆中央仓库时也会用该用户ssh连接远程服务器,并继承其对中央仓库的文件系统的读写权限,注意,出于安全考虑,需要设置git用户禁用shell登录。命令如下。
(2)git clone克隆远程仓库
git clone用来克隆远程仓库至本地,本地仓库是远程仓库的完整备份。由git clone创建的本地仓库不仅包含最新的代码状态,还拥有同远程仓库完全一致的历史版本数据库和独立的版本控制功能。git clone和git init一样,在本地只需被执行一次,执行后会产生包含git核心文件系统的.git子目录和包含当前HEAD指向分支的最新代码的工作目录。
和SVN从中央仓库checkout代码一样,git从远程仓库克隆是为项目做贡献的第一步,但SVN checkout的是中央仓库的工作备份,而Git克隆产生的是具有完整版本控制功能的Git仓库。因此,SVN协作模式基于本地工作备份和远程仓库的互动——本地备份向远程仓库提交代码;Git协作模式基于仓库和仓库之间的互动——推送和拉取远程仓库的代码提交。
Git克隆时会在本地仓库创建一个名为“origin”的引用指向被克隆的远程仓库,这样你的本地克隆就默认建立了与原仓库的联系,未来向远程仓库推送或拉取代码实际就是通过“origin”完成的。
要克隆刚才在远程服务器上创建的myproject.git仓库,只需在本机上直接执行:
第一条命令会提示你输入Git用户密码,你也可以通过公钥导入授权文件的方式配置客户端的ssh无密码连接,克隆执行后,当前目录会产生一个子目录myproject,在该子目录内本地镜像仓库被初始化。接下来就可以切换到子目录,开始编辑更新文件、提交新快照及与远程仓库代码同步等开发工作。
git clone命令提供多种选项,常用的例如:
远程仓库默认会被克隆到与url指定的项目名同名的目录。例如,git clone ssh://git@<server-host>:/path/to/repo/myproject.git,将先在当前路径下创建目录myproject,然后克隆远程仓库到myproject内。目录名也可以由git clone的directory参数自定义,如果directory目录不存在,将会被新建。
--branch选项指定要克隆的分支,如果不指定,克隆分支是远程仓库HEAD所引用的分支,默认为master分支。注意,指定克隆分支只是使初始工作区状态为指定分支,其他分支的引用(refs/heads)依然会被克隆至本地。
--bare选项会使克隆仅包含远程仓库的版本数据库,而忽略工作目录。也就是说,本地仓库只作为共享仓库供其他仓库拉取或者推送代码,而不能被直接提交修改。
--mirror选项除了使本地克隆仓库成为仅供共享的bare仓库外,还会克隆所有的引用(refs/heads、refs/tags、refs/notes等)以及跟踪远程仓库的分支。当在本地仓库上删除了某分支后再执行git remote时,远程分支的该分支也会被删除。远程仓库和本地仓库是完全对等的镜像。
(3)仓库间的内容同步——git-remote/git-fetch/git-pull/git-push
Git协同工作的核心是中央仓库和本地仓库或者不同开发者的仓库之间的内容同步。SVN通过一个个changeset向中央仓库提交自己的代码更新,而Git按需向中央仓库上传共享一系列自己的本地提交,除此之外,Git还允许分享自己的全新分支。
同步仓库工作的首要操作是建立不同仓库之间的联系,git remote就是查看、创建、删除这种追踪关系的命令。注意,git remote创建的关联更类似一个标签,你可以用一个特殊名标记这个关联,之后代码同步时可以直接引用这个标签,而非必须引用完整的远程仓库url。执行git remote可以查看关联标签列表:
加上-v选项会同时显示标签和关联的远程仓库url:
由远程仓库克隆的本地仓库会默认建立一个origin标签关联远程仓库,因此,你在克隆后执行git remote能看到类似以上的结果。
除了默认的origin关联,你还可以用git remote add命令添加其他的bare仓库:
url就是要关联的远程仓库资源,name即是此关联的标签名。
删除关联和重命名关联的命令分别是:
Git支持多种传输协议关联远程仓库,最常用的是http和ssh协议。http协议允许匿名用户拉取代码,但是不允许向远程仓库推送代码;ssh协议支持对远程仓库的读写权限,即可以拉取或推动代码,但ssh必须实名验证。
以上命令将合作者mike的仓库添加到你的本地关联。这种关联极大地方便了你和其他同事交换代码,你们可通过关联直接获取对方的代码更新,而不需通过中央仓库。
添加关联后,你还可以查看关联仓库的详细信息,包括它的所有分支配置和fetch及push的url:
当建立了与远程仓库的关联后,你就可以与远程仓库进行通信了。git fetch就是通过关联从远程仓库拉取新的提交、引用及其他文档。需要注意的是,它不会将远程仓库的代码真实合并到你的本地代码目录,因而不会对你的本地开发有任何影响。git fetch后,你可以通过git log查看历史日志了解远程仓库的进展。基本的git fetch命令如下。
git fetch默认会将远程仓库的所有分支的提交更新都下载到本地,你也可以通过branch参数指定需要fetch的分支:
--all选项可以一次性fetch关联的所有远程仓库,这等同于你对每一个远程关联都执行一遍git fetch<remote>:
将远程版本库状态拉取到本地后,你就可以随时与本地代码进行合并了。一般而言,合并发生在分支层面,所以合并前你需要先将被更新的本地分支checkout,然后选择希望同步的远程分支:
git pull实际是git fetch和git merge的快捷命令,它会先下载远程仓库的更新到本地版本库,然后将当前分支在远程仓库的更新合并到本地仓库:
上面的命令实际上与连续执行下面两条命令效果一样:
如图3-6所示,A为本地和远程master分支代码分叉的时间点,之后本地分支有了B、C提交,而远程分支有了不同的D、E、F提交。本地执行git pull会先从远程仓库的master分支下载快照D、E、F,再在本地自动执行一个合并提交,此提交G包含远程D、E、F提交的内容。
图3-6 git pull流程示意
git pull所执行的合并提交是一种特殊的提交,它的默认备注是类似“Merge branch 'master' of<server-host>:/path/to/repo”的字符串。--rebase选项提供了另外一种合并策略,它不会将D、E、F合并为一个新的提交,而是将D、E、F逐一提交到本地,这个过程可以由图3-7表示,可以看到最终合并时并没有产生一个新的提交G。
git pull rebase有一个直接的好处是,当你用git log查看合并后的提交日志时,可以看到和远程仓库中一样的D、E、F提交信息,完整的工作记录就被保存了下来,如果你希望看到线性的提交历史,而不是在提交日志中出现一些有点碍眼的“merging”提交,git pull rebase是正确的选择。但是任何事情都有两面性,git pull rebase重写了提交历史,使得你无法从日志中直观发现远程仓库什么时候被拉取合并过。
git pull rebase实际与连续执行以下两条命令的效果一样:
图3-7 git pull rebase流程示意
git pull是将远程仓库的更新同步到你的本地仓库,如果希望将本地更新同步到远程仓库呢?很容易猜到,就是git push操作。git push的基本命令是:
以上命令会将本地的<branch>分支并入远程仓库,远程仓库如果没有此分支,将新建立一个和原分支相同的<branch>分支。如果存在这个分支,它会将最新的提交和其他对象并入远程<branch>分支。需要注意的是,如果push时远程分支包含自上次与本地同步后的更新提交,本次push操作会被Git终止。因此,一般来说,开发者在git push前会先获取远程仓库的最新代码并与之同步:
你也可以用--force选项强制上传你的代码,即使远程代码已包含新的提交:
git push命令默认仅会将当前分支上传并与远程仓库的相同分支合并,--all选项可以一次性上传合并你本地的所有分支:
上面所列的git push命令不会上传tags标签,添加--tags选项能帮你单独上传tags标签:
git push的--delete选项用来删除远程分支:
至此,相信你已经对利用Git协作开发的基本操作有了一定了解,下一节会讲解git branch等其他Git概念和操作。
4.Git其他常用操作
前文涉及一些有关分支、查看提交日志、系统配置等的操作,但都是在讲解其他操作时一带而过,本节将对Git的一些常用操作做独立介绍。
(1)Git分支操作
分支是如今版本控制系统中的一个常见概念,分支操作在其他版本控制系统中一般比较耗费资源。而Git分支相关操作的效率较高,这要归功于Git的精妙设计:在Git中,分支实际仅代表一系列的提交按时序排列的关系,换句话说,一系列相关提交的历史组成了分支,分支本身并不装载任何提交,分支的呈现形式是指向其中最新提交快照的指针。
当你有漏洞修正或者新功能开发的任务时,可以在本地临时新建一个分支进行完全独立的开发,而不需担心代码改动会影响其他同事或者主线的开发工作,在任务完成后,你还可以在独立分支上实施充分测试,再选择适当时机与主线分支合并,最后可按需销毁此分支。图3-8表示从仓库主线分叉出来的两个独立分支,一个用于小功能的开发,另一个用于较大功能的开发,存活时间较长。由于开发任务分布在两个完全独立的不同分支,你可以放心地进行并行开发,更重要的是,它们都不会污染主线。
Git分支操作包括创建、销毁、查看、重命名分支等,在不同分支开发时还需要经常用到git checkout和git merge以切换和合并分支。
图3-8 Git分支示意
以下命令会创建一个本地新分支,分支名为<branch>:
删除本地分支的命令为:
这是一种较为安全的删除方式,如果当前分支有尚未被合并的新改动,删除操作会被Git阻止,如果想强制删除,可以用-D选项:
--list选项可用于查看本地仓库的所有分支列表:
-m选项用于重命名分支,以下命令将当前分支重命名为branch_changed:
以上讲述的均是本地分支的操作,同样也可以查看、创建、删除远程分支:
当创建了一个新分支后,可以切换到新分支上开始新任务的开发工作,而完全不用担心影响当前分支的状态:
上面两条命令实际可以仅用一条命令完成,-b选项会先创建一个新分支,再切换到新分支上:
Git创建的新分支默认会继承当前分支的HEAD,也就是说新分支的代码状态和历史快照同当前分支相同。你可以指定远程分支名,使即将创建的本地分支基于该远程分支。
新分支上的某项功能开发测试完成后,会用到git merge将其合并入主线分支,git merge会将指定分支合并入当前分支,因此,合并时需要先将主线分支checkout出来:
如果此分支只是为了小功能开发而设的临时分支,在分支任务完成且并入主线后,此分支即可被销毁,删除本地分支的命令为:
在合并时,如果相同文件的相同代码在两个分支中有所不同,那么本次合并会被终止,Git会提示在合并前解决冲突,提示内容非常友好,标签“<<<<<<<”和“========”之间的内容是冲突部分的合并接收分支的内容;标签“=========”和“>>>>>>>”之间的内容是合并分支的内容。当编辑代码解决分支问题后,可以用常规的git add/git commit重新提交有冲突的文件,之后再次合并。
(2)Git检查日志
任何版本控制系统的本质都是记录文件内容的变化并归档管理,这就使你能够在此后的开发过程中随时回顾内容历史,查看每个开发者的贡献、查找是从哪里被引入的或者恢复某个历史状态。Git完善的版本控制功能就包括详尽的历史追踪和日志查询功能。伴随着本地代码提交、代码分叉、分支之间或版本库之间的内容流动,均会产生版本库的变化,Git也都提供了可靠的日志追溯信息可供日后查询。最基本的Git提交日志查询命令是git log:
命令执行后,会弹出界面显示历史提交,包括提交ID、作者、时间及备注信息。你可用键盘上的〈↑〉和〈↓〉键浏览更多历史。Git提供了更多丰富选项来格式化输出日志,例如,--oneline会显示精简的日志信息,将提交信息压缩到一行显示,仅包括提交ID和提交备注:
--decorate选项除了显示基本log,还会显示与此次提交相关联的分支或者tag标签等的引用,如以下日志首行的合并提交列出了分支信息,就可以得知此次合并了远程和本地master分支:
--stat选项会列出提交的文件名,以及每个文件的改动行数:
-p选项不仅会列出每个文件的改动统计行数,还会列出详细的更改内容:
git shortlog可以按照作者分组显示日志信息,仅显示作者名、作者的提交数以及每个提交备注的首行:
除了这些快捷选项,Git也可以由--pretty=format:"<string>"自定义格式输出,如以下命令中的格式化占位符%cn、%h和%cd分别被提交作者、提交ID和时间代替:
其他的占位符含义可参考附录。
git log还提供了过滤选项,可以按需要输出过滤后的指定日志,如-<n>选项指定输出的提交日志数目,以下命令只会输出最近的三条提交信息:
--after和--before选项可以分别按晚于和先于指定时间过滤显示日志,以下命令显示的是2017年12月1日和12月4日之间的提交历史:
git log还能方便查看两个分支提交历史的不同,只需在git log后面加上<branch1>..<branch2>选项,就会列出所有存在于branch2但是没有存在于branch1的提交历史:
相反,<branch2>..<branch1>会显示存在于branch1但branch2没有的提交。假设branch1和branch2分别是feature和master分支,当你想了解与master分支分叉后,feature分支内演化的情况时,就可以用命令:
这个过程可以清晰地表示为图3-9所示。
图3-9 查看分叉历史
git status用于查询工作目录和暂存区状态,也即尚未被提交到版本库的信息。准确来说,git status输出内容有4种:第一,新加的文件,从来没有被git add添加到暂存区,它会被标记为“Untracked files”;第二,工作区修改更新的文件,但尚未被添加到暂存区,它会被标记为“Changes not staged for commit”;第三,已添加到暂存区但尚未被提交的文件,它会被标记为“Changes to be committed”;第四,当用git merge合并入其他分支时,发生合并冲突的文件,它会被标记为“Unmerged paths”,例如:
(3)Git系统配置
git config用于设置Git全局或本项目的系统配置,不同作用域的配置实际是通过修改不同路径下的.gitconfig文件达到效果的。你可以通过git config设定开发者的姓名和邮箱地址,该设定信息会反映在之后的每个提交日志中。git config也可以设置命令的快捷方式,将一些常用的Git命令用更短的字符表示。git config能有效地帮你定制一个更高效的Git工作流。
git log的常用用法模式是git log--[level] [section.key] [value],level是指不同层级的作用域,section.key和value是指不同的配置选项和对应的配置设定值。
local是默认作用域,可显示由--local指定为本地作用域。本地作用域即说明仅影响当前git config所执行的Git仓库。本地配置实际根据你的选项section.key和设定值value修改本版本库下的配置文件.git/config中的相应内容。
--global限定全局作用域,指当前系统用户层面的Git配置,也就是说当前用户的所有Git项目都会遵循此配置。实际是根据设定值修改用户家目录下的配置文件(~/.gitconfig)实现的。
--system限定系统作用域,设定机器上所有Git项目都会遵循的配置。实际是通过修改系统级的配置文件实现的,一般该文件是/etc/gitconfig。
配置的优先级是本地作用域优先于全局作用域,而全局作用域优先于系统作用域。例如,当在不同层级的作用域中为同一项配置设定不同值时,本地作用域的设定值将会产生作用。
一般在创建完Git项目后,会使用git config配置你的用户名和邮箱地址。以下命令设置了全局的用户名和邮箱地址,每次提交都会使用该设定名和邮箱:
Git配置alias为Git命令创建快捷方式。例如,下面这条命令即在本地作用域中将ci设置为commit的别名,这使你在本项目的版本库执行git ci和git commit是等价的:
设置别名时还可以引用已设置的别名组合成其他快捷方式,下面这条命令使git amend同git commit--amend等价。
git config还可以配置你的默认文本编辑器,Git在git提交等需要输入一些消息的操作时唤醒该文本编辑器。Git默认使用系统的默认编辑器,一般是vi或者vim。如果你想使用一个不同的文本编辑器,例如Emacs,可以做如下操作。
Git还有许多其他的配置选项,这里不再一一介绍,读者可以查阅官方文档获得完整的配置选项。
3.1.3 Git协作开发的经典模式
Git的使用流程与所选择的协作方式密切相关,Git工作流是为了满足开发的稳定性和高效性,根据Git的设计初衷以及项目的特定需求而设定的模式。单个开发者利用Git管理版本的方式非常灵活,并没有一个标准的工作流程。而一个项目组的所有开发成员在使用Git进行协作开发时,一定会对如何顺畅运维代码变化预先达成共识。人们根据Git工作实践的经验,总结出以下几种公认的Git协作工作流模式。
● 集中式工作流。
● 功能分支工作流。
● Git流工作流。
● 叉状工作流。
需要注意的是,Git工作流模式并没有一个固定的规则,本节所述的仅是几种经过大众验证的较为可靠的模式。你可以根据实际项目需要,选择一种或择多种模式混合使用。如何衡量你所使用的工作流是否合适呢?一般需要考虑以下几点。
● 工作流是否与你的项目组规模(即合作人数)匹配。
● 工作流是否方便撤销有问题的提交而恢复至正常状态。
● 开发成员使用这个工作流时会不会超出他们对于开发流程的固有认知范围。
1.集中式工作流
如果你的项目组是从SVN迁移到Git的,那么集中式工作流会是一个很好的模式,它提供了跟SVN相似的协作模式。集中式工作流有一个中心仓库,它包括开发者对于此项目的所有更新,中心仓库的默认分支master将作为唯一分支供开发者提交本地更新。集中模式不再需要其他分支。
使用SVN的开发者很容易熟悉Git的集中工作流模式,但是开发者依然能享受到分布式版本控制系统带来的好处:第一,本地仓库的完整性保证了独立的本地开发环境,开发者编写代码时只需要考虑自己的任务集,本地提交代码时完全可以忘掉其他开发者或者中心仓库的更改对自己的影响,当时机合适(这个时机完全由自己掌握)时,再去与中心仓库合并,那个时候才考虑代码的相互影响;第二,Git健全的分支和合并机制确保你在和中心仓库交互时丝毫不用担心操作失误或代码错误,你可以先按开发需求建立多个本地分支,在不同分支开发不同的功能,继而在本地做分支合并,将需要的代码整理筛选后再推动到中心仓库。虽然Git集中式工作流也是基于中央代码库而进行的代码多端共享同步过程,但Git使得其更为安全可靠并对开发者更为友好。
采用集中式工作流模式,所有开发者需要首先从中央仓库克隆版本库到本地工作站,然后在本地完成开发工作,即编辑、提交代码更新。注意,和SVN不同的是,此时提交仅发生在本地版本库,尚未和中央仓库关联。在合适时,你再将自上次与中央仓库同步后的所有本地更新推送到远端,自此,你的代码更新被公开——其他开发者可以将其拉取到他们的本地仓库。什么时机将本地更新推送到远端,完全结合自己的开发情况和项目需求决定。
我们以集中式工作流中的一个典型场景为例说明整个协作流程:假设项目开发人员为老张和小李,他们各自开发应用的不同功能。当两人开始参与项目时,首先会将中央仓库克隆至各自的本地服务器,有了本地版本库,老张开始基于现有代码进行他的功能开发工作。本地的开发流程大同小异,老张编辑代码、更新代码,然后由git add将一批代码添加到暂存区,继而由git commit提交到本地仓库。由于开发工作都发生在本地,老张可以周而复始地重复这个流程,而不必担心影响中央仓库的状态。同时,小李也正在执行他的新功能开发任务,他同样在进行本地编辑、提交新代码。几天后,老张完成了他的功能开发工作,准备将新的代码推送到中央仓库让其他项目成员看到。老张简单执行git push origin master,将本地master分支的代码推到远端中央仓库的master分支,使中央仓库的master分支和老张的本地master分支保持一致,这里的origin是在克隆时默认创建的与中央仓库的远程连接。由于中央仓库在老张克隆后尚未有其他项目人员向其中添加新代码,老张能很顺利git push成功。当小李完成他的功能开发后,也执行了同样的命令,希望将本地代码推送到中央仓库,但是,由于小李本地仓库状态已经与中央仓库的最新状态偏离,Git将会阻止此次推送,并且抛出non-fast-forward的错误提示信息:
这时,小李需要先和中央仓库同步,可以用git pull将中央仓库的最新代码拉取到本地与本地代码合并,如果小李和老张开发的是完全不相干的功能,那么他俩大概率不会改动同一处代码,也就不会产生合并时的冲突,但如果他们开发的功能有相关性,那么就有可能因为对同一处代码进行了改动而产生合并冲突,这样小李就必须先解决冲突,然后再由git push推送本地代码到中央仓库。
由此可以看出,利用Git的部分命令可以很轻松地复制SVN的典型工作流,这对于从SVN转换到Git的团队非常友好,但是在集中式工作流模式下,Git强大的分布式版本控制功能并没能发挥出来。集中式工作流模式协作简单、流程清晰,比较适合较小的项目团队,但随着团队人员的增加,以上讲到的本地与远端冲突的场景可能就会成为瓶颈。这个时候,如果你还想继续享受集中式带来的便利而又不想使问题代码同步变得过于复杂,可以考虑功能分支工作流。
2.功能分支工作流
功能分支工作流作为集中式工作流的扩展,主张各功能开发在各独立分支进行,而非将所有代码都仅揉入master分支。众人合作开发的单独功能发生在独立分支,这样做的一个直接好处是尚未完成的半成品代码不会被包含到master分支,这对于持续集成环境非常友好。
反过来,功能分支工作流使一个开发小组能集中精力合作开发某项功能,而不需要受主分支中时刻会并入进来的其他功能代码的干扰。开发人员可以随时推送自己本地代码到功能分支,触发代码审核请求,让与此功能相关的开发者共同讨论检查代码的合理性或向代码贡献者提供下一步的建议。
功能分支工作流依然依赖于中央仓库,但中央仓库中的master分支将仅容纳官方代码用于产品分布,一般开发者不会直接向master分支推送代码。开发者在本地建立相应的功能分支后可以将其推向远端,在中央仓库建立相应的功能分支,中央仓库的功能分支将用于开发者本地分支的备份或者与此功能的其他开发者共享代码,其他开发者也可以向这个功能分支提交相关代码改动。中央仓库的功能分支和master分支同时存在,它们各司其职,互不影响。
举一个此种工作流的典型场景例子加以说明。假设老张和小李正准备共同开发产品的一项新功能A,首先由老张基于最新的中央仓库的master分支代码在本地创建了功能A的feature-A分支:
老张可以照常在新分支内编辑、提交代码。小李也有了他的思路,想为新功能添砖加瓦,他要求老张赶紧推送初始代码到这个新分支。老张将最新本地代码提交到feature-A分支后,第一次把新分支推向中央仓库,这样中央仓库中会自动建立新分支feature-A:
紧接着,小李可以同步一次中央仓库,然后基于中央仓库的新分支feature-A创建本地分支:
自此,小李就将来自老张的初始代码下载到本地分支feature-A,小李在本地编码提交了他的代码贡献。几个功能完成后,小李希望将本地代码推送到中央仓库,一是为了对本地工作备份,更重要的是希望老李能够帮他把把关、审核一下新代码。无论是采用第三方代码托管服务(如GitHub等),或自己搭建Gerrit代码审核工具,都能在向中央仓库主动推送或是请求拉取本地代码时引发一个代码审核任务,你可以指定其他开发人员与你一起讨论本次改动,待大家都一致认为代码没问题后,代码才会合并入中央仓库。回到本例,老张收到审核代码的请求之后,界面上会清晰地显示小李的代码改动,并可方便地按行添加反馈,小李和老张可以就此展开讨论修改代码,直到最后都对代码满意合并入中央仓库。
老张和小李按这个流程开发完新功能后,需要在某个合适的时间节点正式交付——将新功能A的代码并入master分支,作为产品新功能上市。按项目需要或组织架构规定,并入master分支的工作可以由老张或小李完成,也可以由其他项目技术管理人员完成:
以上操作先将本地master分支与远程中央仓库同步,下载最新代码;接着拉取中央仓库(origin作为关联标记)的feature-A分支与本地master分支合并,本地将产生一个日志为“Merge branch 'feature-A'…”的额外合并提交;最后由git push推送到远端master分支,完成feature-A的代码交付。相比于产生一个独立的合并提交,有些工程师更倾向于完全线性的提交历史,也就是基于master分支逐个添加属于feature-A分支的所有提交,这样能在未来方便地对作用于master分支的所有提交清晰还原,只需将第三步改成git pull--rebase origin feature-A就能实现线性的提交历史。
在老张和小李开发新功能的同时,老刘和小青正用同样的流程开发另一个功能feature-B,在feature-B开发完毕前,老刘和小青的新代码一般不会进入master分支,而仅在feature-B分支。老刘和小青的开发发生在自己的feature-B功能分支,而不会受到老张和小李的feature-A分支改动的影响。
功能分支工作流使不同功能的开发被完全隔离,互不影响,相比于集中式工作流,中央仓库的master分支不会再频繁地被开发者的推送请求干扰,增强了master分支代码的稳定性。功能分支工作流聚焦于分支,下面要讲到的Git流工作流和叉状工作流更多地聚焦于整个仓库,但在Git流工作流和叉状工作流中又可以利用分支导向的功能分支工作流。
3.Git流工作流
在开展任何软件项目之前,商定一个大家都会遵守的工作流程非常重要,可以根据项目组需求结合Git特点制定协作工作流模式,也可以采用Git提供的一种规范工作流,即Git流(Gitflow)工作流。Git提供了一个标准工具git flow去定义Git流工作流,git flow是Git的扩展,它实际上将Git的一些基本命令用脚本组合了起来。
git flow工具并不包含在标准的Git环境中,简单执行以下命令便可以安装git flow:
如果尚未安装包管理工具brew,可参考其官方网站先安装brew。安装完git flow,就可以利用git flow init初始化项目目录了:
git flow init实际上只是基于git init的扩展命令,它仅会以交互方式引导使用者预定义几个分支,对版本库功能不会有实质影响。当然也可以不使用git flow而仅依赖基本的Git命令去创建这些分支,git flow的价值核心是提供了一种工作流指导思想。创建分支时你可以用git flow的默认命名,也可以自定义命名。方便起见,以下都按照默认命名进行讲解。git flow会预设两个贯穿整个软件生命周期的主分支:
● master分支存放产品代码,用于产品发布。开发者一般不会直接在master分支工作或者随意向master分支推送代码,由于master分支直接与产品的每个发布版本有关,哪些功能、哪些修正在哪个时间点能被并入master分支都需要根据产品规划,经过项目开发人员、管理人员、产品经理的共同研讨决定。
● develop分支一般用于所有已完成或者正在进行的功能代码的汇总,同时也是新功能开发的基础分支。下一次的产品发布,通常会根据预先制定好的发布计划先从develop分支选取相应的代码改动并入master分支,再从master分支构建发布产品。
一旦有新功能需求,开发者便可以在默认已创建的feature分支上进行开发工作,当多个功能并行开发时,项目会建立多个不同的功能分支。功能开发完成后,分支代码会被合并入develop分支,此后该分支便可以被销毁。在大型的较为规范的商业软件项目中,即使功能开发完毕,功能分支通常也会被保留下来以组成项目的全历史追溯系统。
图3-10为master分支、develop分支和功能分支的代码流动示意图,可以看到功能分支feature-A和feature-B都是基于最新的develop分支状态建立的。此外在Git流工作流模式中,功能分支的代码不会直接进入master分支,都是先并入develop分支,继而进入master分支用于产品发布的。产品v1.0、v1.1、v1.2版本相继由master分支交付,而feature-A和feature-B在v1.2中被发布。
图3-10 Git流分支代码流动
没有git-flow的情况下创建功能分支:
由于git flow默认所有功能分支都基于develop分支,因此在git flow帮助下创建功能分支就会简洁一些:
功能开发完成后,代码会被并入develop分支,用基本Git命令合并:
用git flow命令:
也许已经发现,在Git流工作流模式中,develop分支和各功能分支的交互几乎与功能分支工作流无异,确实如此,Git流包含了功能分支工作流模式,而其本身又远不止于此。
在大型商业软件项目中,所有计划的新功能可能在产品发布前半年已开发完成,甚至产品v2.0的功能有时在产品v1.92未发布前就已经完成。为了优化产品发布管理,这时会从develop分支分叉出一个release分支,用于装载所有下一个产品发布所需的新功能。从待发布的功能并入完毕至下一个产品版本发布前的周期内,不会再有功能分支向release分支并入新代码,仅会有与漏洞修正、文档编辑等与发布相关的新提交被并入release分支。在产品发布时,再由release分支向master分支合并。用一个单独的release分支专为下一个产品发布进行代码管理,保证了其他功能的开发依然能按序进行,因此develop分支还会持续有新代码进入,在release分支并入master分支后,也会将develop分支与release分支的更新同步。release分支就像develop分支和master分支之间的桥梁,既进一步隔离了产品发布和开发两个阶段,又使得产品的开发到发布能够有序、高效地执行。
release分支的命名一般会加上产品发布版本号,由git flow创建一个用于产品v2.0的release分支非常简单:
一旦产品v2.0即将发布,需要将release/2.0的分支和master分支及develop分支合并,最好再与master分支合并时打上即将发布的版本号的标签,自此,release/2.0分支使命已经完成,可以被销毁。单条git flow的release finish命令可以完成以上所有事项:
软件产品发布后,客户抱怨流程错误或使用不便问题是时常发生的事,这时,项目组可能需要快速响应,马上按照客户要求修复这个问题。Git流工作流模式就提供了专门的hotfix流程去修复线上的紧急问题,这个流程依靠的是一个特殊的hotfix分支。hotfix分支和release及feature分支在代码流向上非常相似,不同的是release和feature分支基于develop分支,而hotfix直接基于master分支。hotfix分支是Git流工作流模式中仅有的从master分支直接分叉而来的,相应的工程师在hotfix分支上修正问题后,hotfix分支会被立即同步到master分支和develop分支(或者当前的release分支),并且master分支上会打上新的版本标签,例如v2.0_hotfix1。
由git flow创建hotfix分支的命令:
同样地,hotfix工作完成后,可由git flow的hotfix finish命令将hotfix分支更新同步到develop和master分支,并在弹出的窗口中编辑指定master分支的并入tag(此处为v2.0-hotfix1),最后删除该hotfix分支:
图3-11是包括release和hotfix分支在内的完整Git流工作流的代码流动示意图。
图3-11 Git流所有分支代码流动
Git流工作流模式对于需要发布管理的商业软件生产流程非常适用,各分支分工明确,代码流合理又清晰,方便不同规模的项目团队开发管理。
4.叉状工作流
前面讲到的功能分支工作流和Git流工作流虽然不像集中式工作流那样,所有本地开发都严重依赖中央仓库的master分支,但它们确实都围绕位于远程服务器的“中心”仓库进行本地工作的备份、分享或者软件发布的管理。叉状(forking)工作流同它们的模式有着本质区别,项目除了有一个远程公开的中心仓库,每个开发者还拥有一个独立的位于远程服务器的私有仓库,在后文中我们将它称为分叉仓库(forked repository)。
叉状工作流的一大优势就是所有人都可以以向自己独有的分叉仓库推送的方式来为项目贡献代码,而不需要向中央仓库推送。中央仓库会由项目管理者或维护者按需挑取开发者的代码并入,因此中央仓库时常不会对每个开发者开放写权限。叉状工作流的代码流动实质上类似Git流工作流,也拥有独立开发的功能分支,且这些功能分支的目的是最终汇入中央仓库。叉状工作流更加灵活,适合庞大、“野蛮生长”(可能包含未知的第三方)的项目团队安全地协作,因此,它更多应用于开源项目。
同上述其他的工作流模式一样,叉状工作流也起始于一个共同可读的远端中央仓库,当一个新的开发者进入项目组时,他不会直接将远程仓库克隆到本地,取而代之的是从中央仓库分叉出一个同样存在中央服务器的远端仓库,这个仓库有两个特点:第一,为此开发者独有,其他开发人员不能向其推送代码;第二,代码库公开,即其他开发人员可以从中拉取代码到自己的本地或分叉出自己的远端仓库。这是叉状工作流的关键所在。接着这个开发者会将独有远端仓库克隆到本地,和其他工作流模式一样,在本地仓库开始开发工作。
当开发者完成了某些重要功能的开发,想要将本地提交推送到项目主代码库时,他需要先将本地代码推送到自己的独有远端仓库,然后向中央仓库开起一个拉取请求(pull request),该拉取请求会通知项目管理者/维护者,有一个推送已准备好汇入中央仓库,管理者/维护者会在方便时检阅该代码更新,并给予有关代码修改的反馈,开发者经过反复讨论、修改、论证,已经对此次代码推送一致同意并入时,项目管理者/维护者才会将其并入代码主仓库,否则此次拉取请求将被拒绝并关闭。
读到这里,你可能会对到底什么是“分叉仓库”比较疑惑。分叉并不是什么特殊的Git操作,它实质上是在远程服务器端用git clone将中央仓库克隆,分叉仓库对于中央仓库依然是克隆的完整复制,对于开发者是服务器端的仓库。通常分叉仓库被托管在第三方的代码托管服务商。与功能分支工作流和Git流工作流一样,分叉工作流中也将各项单独功能隔离到独立的分支进行开发,但利用分叉仓库,开发者可以更方便地向他人分享自己的分支,他人只需从开发者自己的分叉仓库中拉取关心的分支代码,而在其他工作流中,需要将本地代码推送到中央仓库。
叉状工作流的另外一个特点是,开发者的本地仓库包含两个远程连接标识——一个指向中央仓库,另一个指向自己的私有分叉仓库。通常,默认的origin标识指向分叉仓库,需要你新建一个upstream标识指向中央仓库:
通过upstream连接,用git pull能使你的本地仓库可以时刻与官方的中央仓库保持同步:
一旦功能开发完毕,需要做两件事将新功能分享给其他人:
第一,推送新代码到你的分叉仓库。
第二,告知项目管理或维护人员将新功能代码拉取到中央仓库。在第三方代码管理平台(如GitHub)的开源项目中,只需要单击界面上的“拉取请求”按钮、填写完相应表单就能通知项目管理人员审核代码。
叉状工作流不需要项目管理人员为每个项目成员都配置详细的权限管理清单,它会对所有项目人员都开放版本仓库级别的复制权限,而项目管理者只需要用拉取指令挑选需要的代码更新进入官方版本仓库。叉状工作流被广泛地应用在开源项目中,第三方代码托管服务商提供了非常友好的web界面帮助项目各角色完成流程管理工作。在下一节中,我们将针对典型的基于Git的第三方代码托管商GitHub进行详细介绍。