不同开发规模下Git工作流的选择
一、小规模项目 => 集中式工作流
1. 什么是 集中式工作流
集中式工作流以中央仓库作为项目所有修改的单点实体。所有修改提交到这个分支上。该工作流只用到 master 这一个分支。
开发者开始先克隆中央仓库。在自己的项目拷贝中,像 SVN 一样的编辑文件和提交修改;但修改是存在本地的,和中央仓库是完全隔离的。开发者可以把和上游的同步延后到一个方便时间点。
要发布修改到正式项目中,开发者要把本地 master 分支的修改『推(push)』到中央仓库中。这相当于 svn commit 操作,但 push 操作会把所有还不在中央仓库的本地提交都推上去。
解决冲突
中央仓库代表了正式项目,如果开发者本地的提交历史和中央仓库有分歧,Git 会拒绝 push 提交否则会覆盖已经在中央库的正式提交。
在开发者提交自己功能修改到中央库前,需要先 fetch 在中央库的新增提交,rebase 自己提交到中央库提交历史之上。这样做的意思是在说,『我要把自己的修改加到别人已经完成的修改上。』最终的结果是一个完美的线性历史,就像以前的SVN 的工作流中一样。
如果本地修改和上游提交有冲突,Git 会暂停 rebase 过程,给你手动解决冲突的机会。Git 解决合并冲突,用和生成提交一样的 git status 和 git add 命令,很一致方便。还有一点,如果解决冲突时遇到麻烦,Git 可以很简单中止整个 rebase 操作,重来一次(或者让别人来帮助解决)。
2. 示例描述
1、有人先初始化好中央仓库
第一步,有人在服务器上创建好中央仓库。如果是新项目,你可以初始化一个空仓库;否则你要导入已有的 Git 或 SVN 仓库。
中央仓库应该是个裸仓库(bare repository),即没有工作目录(working directory)的仓库。
2、所有人克隆中央仓库
下一步,各个开发者创建整个项目的本地拷贝。通过 git clone 命令完成:
$ git clone https://github.com/path/to/repo.git
基于你后续会持续和克隆的仓库做交互的假设,克隆仓库时 Git 会自动添加远程别名 origin 指回『父』仓库。
3、小明开发功能
在小明的本地仓库中,他使用标准的 Git 过程开发功能:编辑、暂存(Stage)和提交。如果你不熟悉暂存区(Staging Area),这里说明一下:暂存区的用来准备一个提交,但可以不用把工作目录中所有的修改内容都包含进来。这样你可以创建一个高度聚焦的提交,尽管你本地修改很多内容。
$ git status # 查看本地仓库的修改状态
$ git add # 暂存文件
$ git commit # 提交文件
请记住,因为这些命令生成的是本地提交,小明可以按自己需求反复操作多次,而不用担心中央仓库上有了什么操作。对需要多个更简单更原子分块的大功能,这个做法是很有用的。
4、小红开发功能
与此同时,小红在自己的本地仓库中用相同的编辑、暂存和提交过程开发功能。和小明一样,她也不关心中央仓库有没有新提交;当然更不关心小明在他的本地仓库中的操作,因为所有本地仓库都是私有的。
5、小明发布功能
一旦小明完成了他的功能开发,会发布他的本地提交到中央仓库中,这样其它团队成员可以看到他的修改。他可以用下面的 git push 命令:
$ git push origin master
注意,origin 是在小明克隆仓库时 Git 创建的远程中央仓库别名。master 参数告诉 Git 推送的分支。由于中央仓库自从小明克隆以来还没有被更新过,所以 push 操作不会有冲突,成功完成。
6、小红试着发布功能
一起来看看在小明发布修改后,小红 push 修改会怎么样?她使用完全一样的 push 命令:
$ git push origin master
但她的本地历史已经和中央仓库有分岐了,Git 拒绝操作并给出下面很长的出错消息:
# error: failed to push some refs to '/path/to/repo.git'
# hint: Updates were rejected because the tip of your current branch is behind
# hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
# hint: before pushing again.
# hint: See the 'Note about fast-forwards' in 'git push --help' for details.
这避免了小红覆写正式的提交。她要先 pull 小明的更新到她的本地仓库合并上她的本地修改后,再重试。
7、小红在小明的提交之上 rebase
小红用 git pull 合并上游的修改到自己的仓库中。这条命令类似 svn update ——拉取所有上游提交命令到小红的本地仓库,并尝试和她的本地修改合并:
$ git pull --rebase origin master
--rebase 选项告诉 Git 把小红的提交移到同步了中央仓库修改后的 master 分支的顶部,如下图所示:
如果你忘加了这个选项,pull 操作仍然可以完成,但每次 pull 操作要同步中央仓库中别人修改时,提交历史会以一个多余的『合并提交』结尾。对于集中式工作流,最好是使用 rebase 而不是生成一个合并提交。
8、小红解决合并冲突
rebase 操作过程是把本地提交一次一个地迁移到更新了的中央仓库 master 分支之上。这意味着可能要解决在迁移某个提交时出现的合并冲突,而不是解决包含了所有提交的大型合并时所出现的冲突。这样的方式让你尽可能保持每个提交的聚焦和项目历史的整洁。反过来,简化了哪里引入 Bug 的分析,如果有必要,回滚修改也可以做到对项目影响最小。
如果小红和小明的功能是相关的,不大可能在 rebase 过程中有冲突。如果有,Git 在合并有冲突的提交处暂停 rebase 过程,输出下面的信息并带上相关的指令:CONFLICT (content): Merge conflict in
Git 很赞的一点是,任何人可以解决他自己的冲突。在这个例子中,小红可以简单的运行 git status 命令来查看哪里有问题。冲突文件列在 Unmerged paths(未合并路径)一节中:
$ git status
# Unmerged paths:
# (use "git reset HEAD <some-file>..." to unstage)
# (use "git add/rm <some-file>..." as appropriate to mark resolution)
#
# both modified: <some-file>
接着小红编辑这些文件。修改完成后,用老套路暂存这些文件,并让 git rebase 完成剩下的事:
$ git add
$ git rebase --continue
要做的就这些了。Git 会继续一个一个地合并后面的提交,如其它的提交有冲突就重复这个过程。
如果你碰到了冲突,但发现搞不定,不要惊慌。只要执行下面这条命令,就可以回到你执行 git pull --rebase 命令前的样子:
$ git rebase --abort
9、小红成功发布功能
小红完成和中央仓库的同步后,就能成功发布她的修改了:
$ git push origin master
二、中规模项目 => 功能分支工作流
1. 什么是功能分支工作流
功能分支工作流背后的核心思路是所有的功能开发应该在一个专门的分支,而不是在 master 分支上。这个隔离可以方便多个开发者在各自的功能上开发而不会弄乱主干代码。另外,也保证了 master 分支的代码一定不会是有问题的,极大有利于集成环境。
工作方式
功能分支工作流仍然用中央仓库,并且 master 分支还是代表了正式项目的历史。但不是直接提交本地历史到各自的本地 master 分支,开发者每次在开始新功能前先创建一个新分支。功能分支应该有个有描述性的名字,比如 animated - menu - items 或 issue -#1061 ,这样可以让分支有个清楚且高聚焦的用途。
另外,功能分支也可以(且应该)push 到中央仓库中。这样不修改正式代码就可以和其它开发者分享提交的功能。由于 master 仅有的一个『特殊』分支,在中央仓库上存多个功能分支不会有任何问题。当然,这样做也可以很方便地备份各自的本地提交。
Pull Requests
功能分支除了可以隔离功能的开发,也使得通过 Pull Requests 讨论变更成为可能。一旦某个开发完成一个功能,不是立即合并到 master ,而是 push 到中央仓库的功能分支上并发起一个 Pull Request 请求去合并修改到 master 。在修改成为主干代码前,这让其它的开发者有机会先去 Review 变更。
Code Review 是 Pull Requests 的一个重要的收益,但 Pull Requests 目的是讨论代码一个通用方式。你可以把 Pull Requests 作为专门给某个分支的讨论。这意味着可以在更早的开发过程中就可以进行 Code Review 。比如,一个开发者开发功能需要帮助时,要做的就是发起一个 Pull Request ,相关的人就会自动收到通知,在相关的提交旁边能看到需要帮助解决的问题。
一旦 Pull Request 被接受了,发布功能要做的就和集中式工作流就很像了。首先,确定本地的 master 分支和上游的 master 分支是同步的。然后合并功能分支到本地 master 分支并 push 已经更新的本地 master 分支到中央仓库。
2. 示例描述
1. 小红开始开发一个新功能
在开始开发功能前,小红需要一个独立的分支。使用下面的命令新建一个分支:
$ git checkout -b marys-feature master
这个命令检出一个基于master名为marys-feature的分支,Git的-b选项表示如果分支还不存在则新建分支。这个新分支上,小红按老套路编辑、暂存和提交修改,按需要提交以实现功能:
$ git status
$ git add
$ git commit
2. 小红要去吃个午饭
早上小红为新功能添加一些提交。去吃午饭前,push功能分支到中央仓库是很好的做法,这样可以方便地备份,如果和其它开发协作,也让他们可以看到小红的提交。
$ git push -u origin marys-feature
这条命令push marys-feature分支到中央仓库(origin),-u选项设置本地分支去跟踪远程对应的分支。设置好跟踪的分支后,小红就可以使用git push命令省去指定推送分支的参数。
3. 小红完成功能开发
小红吃完午饭回来,完成整个功能的开发。在合并到master之前,她发起一个Pull Request让团队的其它人知道功能已经完成。但首先,她要确认中央仓库中已经有她最近的提交:
$ git push
然后,在她的Git GUI客户端中发起Pull Request,请求合并marys-feature到master,团队成员会自动收到通知。Pull Request很酷的是可以在相关的提交旁边显示评注,所以你可以很对某个变更集提问。
4. 小黑收到Pull Request
小黑收到了Pull Request后会查看marys-feature的修改。决定在合并到正式项目前是否要做些修改,且通过Pull Request和小红来回地讨论。
要再做修改,小红用和功能第一个迭代完全一样的过程。编辑、暂存、提交并push更新到中央仓库。小红这些活动都会显示在Pull Request上,小黑可以断续做评注。
如果小黑有需要,也可以把marys-feature分支拉到本地,自己来修改,他加的提交也会一样显示在Pull Request上。
5. 小红发布她的功能
一旦小黑可以的接受Pull Request,就可以合并功能到稳定项目代码中(可以由小黑或是小红来做这个操作):
$ git checkout master
$ git pull
$ git pull origin marys-feature
$ git push
无论谁来做合并,首先要检出master分支并确认是它是最新的。然后执行git pull origin marys-feature合并marys-feature分支到和已经和远程一致的本地master分支。你可以使用简单git merge marys-feature命令,但前面的命令可以保证总是最新的新功能分支。最后更新的master分支要重新push回到origin。
这个过程常常会生成一个合并提交。有些开发者喜欢有合并提交,因为它像一个新功能和原来代码基线的连通符。但如果你偏爱线性的提交历史,可以在执行合并时rebase新功能到master分支的顶部,这样生成一个快进(fast-forward)的合并。
一些GUI客户端可以只要点一下『接受』按钮执行好上面的命令来自动化Pull Request接受过程。如果你的不能这样,至少在功能合并到master分支后能自动关闭Pull Request。
与此同时,小明在做和小红一样的事
当小红和小黑在marys-feature上工作并讨论她的Pull Request的时候,小明在自己的功能分支上做完全一样的事。
通过隔离功能到独立的分支上,每个人都可以自主的工作,当然必要的时候在开发者之间分享变更还是比较繁琐的。
三、大规模项目 => Gitflow工作流
1. 什么是 GitFlow
GitFlow 是一种 Git 工作流,这个工作流程围绕着project的发布(release)定义了一个严格的如何建立分支的模型。它是团队成员遵守的一种代码管理方案 。
每一个特性(feature)的开发并不直接在主干上开发,而是在分支上开发,分支开发完毕后再合并到主干上。
这样做的好处是:
- 还处于半成品状态的feature不会影响到主干
- 各个开发人员之间做自己的分支,互不干扰
- 主干永远处于可编译、可运行的状态
GitFlow则在这个基础上更进一步,规定了如何建立、合并分支,如何发布,如何维护历史版本等工作流程。
master分支:只存放历史发布(release)版本的源代码。即用于存放对外发布的版本,任何时候在这个分支获取到的都是稳定的已发布的版本。各个版本通过tag来标记。上图里的v0.1和v0.2就是tag
develop分支:用来整合各个feature分支。开发中的版本的源代码存放在这里。即用于日常开发,存放最新的开发版
feature分支:每一个特性(feature)都必须在自己的分支里开发,feature分支派生自develop分支。feature分支只存在于开发者本地,不能被提交到远程库。当feature开发完毕后,要合并回develop分支。feature分支永远不会和master分支打交道
release分支:release分支不是一个放正式发布产品的分支,你可以将它理解为“待发布”分支。
我们用这个分支干所有和发布有关的事情,比如:
- 把这个分支打包给测试人员测试
- 在这个分支里修复bug
- 编写发布文档
所以,在这个分支里面绝对不会添加新的特性。
当和发布相关的工作都完成后,release分支合并回develop和master分支。
单独搞一个release分支的好处是,当一个团队在做发布相关的工作时,另一个团队则可以接着开发下一版本的东西
hotfix分支:一个项目发布后或多或少肯定会有一些bug存在,而bug的修复工作并不适合在develop上做,这是因为develop分支上包含还未验证过的feature,用户未必需要develop上的feature。
develop还不能马上发布,而客户急需这个bug的修复。
这时就需要新建hotfix分支,hotfix分支派生自master分支,仅仅用于修复bug,当bug修复完毕后,马上回归到master分支,然后发布一个新版本,比如,v0.1.1。
同时hotfix也要合并回develop分支,这样develop分支就能享受到bug修复的好处了。
2. 示例描述
1. 创建develop分支
第一步是给默认的master配备一个develop分支。一种简单的做法是:让一个开发者在本地建立一个空的develop分支,然后把它推送到服务器。
$ git branch develop
$ git push -u origin develop
develop分支将包含项目的所有历史,而master会是一个缩减版本。现在,其他开发者应该克隆(clone)中央仓库,并且为develop创建一个追踪分支。
$ git clone ssh://user@host/path/to/repo.git
$ git checkout -b develop origin/develop
到现在,所有人都把包含有完整历史的分支(develop)在本地配置好了。
3.2 小马和小明开始开发新功能
我们的故事从小马和小明要分别开发新功能开始。他们俩各自建立了自己的分支。注意,他们在创建分支时,父分支不能选择master,而要选择develop。
$ git checkout -b some-feature develop
他们俩都在自己的功能开发分支上开展工作。通常就是这种Git三部曲:edit,stage,commit:
$ git status
$ git add <some-file>
$ git commit
3.3 小马把她的功能开发好了
在提交过几次代码之后,小马觉得她的功能做完了。如果她所在的团队使用“拉拽请求”,此刻便是一个合适的时机——她可以提出一个将她所完成的功能合并入develop分支的请求。要不然,她可以自行将她的代码合并入本地的develop分支,然后再推送到中央仓库,像这样:
$ git pull origin develop
$ git checkout develop
$ git merge some-feature
$ git push
$ git branch -d some-feature
第一条命令确保了本地的develop分支拥有最新的代码——这一步必须在将功能代码合并之前做!
注意,新开发的功能代码永远不能直接合并入master。
必要时,还需要解决在代码合并过程中的冲突。
3.4 小马开始准备一次发布
尽管小明还在忙着开发他的功能,小马却可以开始准备这个项目的第一次正式发布了。
类似于功能开发,她使用了一个新的分支来做产品发布的准备工作。在这一步,发布的版本号也最初确定下来。
$ git checkout -b release-0.1 develop
这个分支专门用于发布前的准备,包括一些清理工作、全面的测试、文档的更新以及任何其他的准备工作。
它与用于功能开发的分支相似,不同之处在于它是专为产品发布服务的。
一旦小马创建了这个分支并把它推向中央仓库,这次产品发布包含的功能也就固定下来了。
任何还处于开发状态的功能只能等待下一个发布周期。
3.5 小马完成了发布
一切准备就绪之后,小马就要把发布分支合并入master和develop分支,然后再将发布分支删除。
注意,往develop分支的合并是很重要的,因为开发人员可能在发布分支上修复了一些关键的问题,而这些修复对于正在开发中的新功能是有益的。再次提醒一下,如果小马所在的团队强调代码评审(Code Review),此时非常适合提出这样的请求。
$ git checkout master
$ git merge release-0.1
$ git push
$ git checkout develop
$ git merge release-0.1
$ git push
$ git branch -d release-0.1
发布分支扮演的角色是功能开发(develop)与官方发布(master)之间的一个缓冲。
无论什么时候你把一些东西合并入master,你都应该随即打上合适的标签。
$ git tag -a 0.1 -m"Initial public release" master
$ git push --tags
Git支持钩子(hook)的功能,也就是说,在代码仓库里某些特定的事件发生的时候,可以执行一些预定义的脚本。
因此,一种可行的做法是:在服务器端配置一个钩子,当你把master推送到中央仓库或者推送标签时,Git服务器能为产品发布进行一次自动的构建。
3.6 用户发现了一个bug
当一次发布完成之后,小马便回去与小明一起开发其他功能了。
突然,某个用户提出抱怨说当前发布的产品里有一个bug。
为了解决这个问题,小马(或者小明)基于master创建了一个用于维护的分支。
她在这个分支上修复了那个bug,然后把改动的代码直接合并入master。
$ git checkout -b issue-#001 master
# Fix the bug
$ git checkout master
$ git merge issue-#001
$ git push
跟用于发布的分支一样,在维护分支上的改动也需要合并入develop分支,这一点是很重要的!
因此,小马务必不能忘了这一步。
随后,她就可以将维护分支删除。
$ git checkout develop
$ git merge issue-#001
$ git push
$ git branch -d issue-#001
四、开源项目 => Forking 工作流
Forking 工作流和前面讨论的几种工作流有根本的不同。这种工作流不是使用单个服务端仓库作为『中央』代码基线,而让各个开发者都有一个服务端仓库。这意味着各个代码贡献者有 多 个 Git 仓库而不是 1 个:一个本地私有的,另一个服务端公开的。
Forking 工作流的一个主要优势是,贡献的代码可以被集成,而不需要所有人都能 push 代码到仅有的中央仓库中。开发者 push 到自己的服务端仓库,而只有项目维护者才能 push 到正式仓库。这样项目维护者可以接受任何开发者的提交,但无需给他正式代码库的写权限。
效果就是一个分布式的工作流,能为大型、自发性的团队(包括了不受信的第三方)提供灵活的方式来安全的协作。也让这个工作流成为开源项目的理想工作流。
工作方式
和其它的 Git 工作流一样,Forking 工作流要先有一个公开的正式仓库存储在服务器上。但一个新的开发者想要在项目上工作时,不是直接从正式仓库克隆,而是 fork 正式项目在服务器上创建一个拷贝。
这个仓库拷贝作为他个人公开仓库 —— 其它开发者不允许 push 到这个仓库,但可以 pull 到修改(后面我们很快就会看这点很重要)。在创建了自己服务端拷贝之后,和之前的工作流一样,开发者执行 git clone 命令克隆仓库到本地机器上,作为私有的开发环境。
要提交本地修改时,push 提交到自己公开仓库中 —— 而不是正式仓库中。然后,给正式仓库发起一个 pull request,让项目维护者知道有更新已经准备好可以集成了。对于贡献的代码,pull request 也可以很方便地作为一个讨论的地方。
为了集成功能到正式代码库,维护者 pull 贡献者的变更到自己的本地仓库中,检查变更以确保不会让项目出错,合并变更到自己本地的 master 分支,然后 push master 分支到服务器的正式仓库中。到此,贡献的提交成为了项目的一部分,其它的开发者应该执行 pull 操作与正式仓库同步自己本地仓库。
正式仓库
在 Forking 工作流中,『官方』仓库的叫法只是一个约定,理解这点很重要。从技术上来看,各个开发者仓库和正式仓库在Git看来没有任何区别。事实上,让正式仓库之所以正式的唯一原因是它是项目维护者的公开仓库。
Forking 工作流的分支使用方式
所有的个人公开仓库实际上只是为了方便和其它的开发者共享分支。各个开发者应该用分支隔离各个功能,就像在功能分支工作流和 GitFlow 工作流一样。唯一的区别是这些分支被共享了。在 Forking 工作流中这些分支会被 pull 到另一个开发者的本地仓库中,而在功能分支工作流和 GitFlow 工作流中是直接被 push 到正式仓库中。
1. 项目维护者初始化正式仓库
和任何使用 Git 项目一样,第一步是创建在服务器上一个正式仓库,让所有团队成员都可以访问到。通常这个仓库也会作为项目维护者的公开仓库。公开仓库应该是裸仓库,不管是不是正式代码库。所以项目维护者会运行像下面的命令来搭建正式仓库:
$ ssh user@host
$ git init --bare /path/to/repo.git
这个搭建中央仓库的过程和前面提到的工作流完全一样。如果有现存的代码库,维护者也要 push 到这个仓库中。
2. 开发者 fork 正式仓库
其它所有的开发需要 fork 正式仓库。可以用 git clone 命令用 SSH 协议连通到服务器,拷贝仓库到服务器另一个位置 —— 是的,fork 操作基本上就只是一个服务端的克隆。Bitbucket 和 Stash 上可以点一下按钮就让开发者完成仓库的 fork 操作。
这一步完成后,每个开发都在服务端有一个自己的仓库。和正式仓库一样,这些仓库应该是裸仓库。
3. 开发者克隆自己 fork 出来的仓库
下一步,各个开发者要克隆自己的公开仓库,用熟悉的 git clone 命令。
$ git clone https://user@bitbucket.org/user/repo.git
相比前面介绍的工作流只用了一个 origin 远程别名指向中央仓库,Forking 工作流需要 2 个远程别名 —— 一个指向正式仓库,另一个指向开发者自己的服务端仓库。别名的名字可以任意命名,常见的约定是使用 origin 作为远程克隆的仓库的别名(这个别名会在运行 git clone 自动创建),upstream(上游)作为正式仓库的别名。
$ git remote add upstream https://bitbucket.org/maintainer/repo
需要自己用上面的命令创建 upstream 别名。这样可以简单地保持本地仓库和正式仓库的同步更新。注意,如果上游仓库需要认证(比如不是开源的),你需要提供用户:
$ git remote add upstream https://user@bitbucket.org/maintainer/repo.git
这时在克隆和 pull 正式仓库时,需要提供用户的密码。
4. 开发者开发自己的功能
在刚克隆的本地仓库中,开发者可以像其它工作流一样的编辑代码、提交修改和新建分支:
$ git checkout -b some-feature
# Edit some code
$ git commit -a -m "Add first draft of some feature"
所有的修改都是私有的直到 push 到自己公开仓库中。如果正式项目已经往前走了,可以用 git pull 命令获得新的提交:
$ git pull upstream master
由于开发者应该都在专门的功能分支上工作,pull 操作结果会都是快进合并。
5. 开发者发布自己的功能
一旦开发者准备好了分享新功能,需要做二件事。首先,通过push他的贡献代码到自己的公开仓库中,让其它的开发者都可以访问到。他的 origin 远程别名应该已经有了,所以要做的就是:
$ git push origin feature-branch
这里和之前的工作流的差异是,origin 远程别名指向开发者自己的服务端仓库,而不是正式仓库。
第二件事,开发者要通知项目维护者,想要合并他的新功能到正式库中。
Bitbucket 和 Stash 提供了 Pull Request 按钮,弹出表单让你指定哪个分支要合并到正式仓库。
一般你会想集成你的功能分支到上游远程仓库的 master 分支中。
6. 项目维护者集成开发者的功能
当项目维护者收到 pull request,他要做的是决定是否集成它到正式代码库中。有二种方式来做:
直接在 pull request 中查看代码
pull 代码到他自己的本地仓库,再手动合并
第一种做法更简单,维护者可以在 GUI 中查看变更的差异,做评注和执行合并。
但如果出现了合并冲突,需要第二种做法来解决。
这种情况下,维护者需要从开发者的服务端仓库中 fetch 功能分支,合并到他本地的 master 分支,解决冲突:
$ git fetch https://bitbucket.org/user/repo feature-branch
// 查看变更
$ git checkout master
$ git merge FETCH_HEAD
变更集成到本地的 master 分支后,维护者要 push 变更到服务器上的正式仓库,这样其它的开发者都能访问到:
$ git push origin master
注意,维护者的 origin 是指向他自己公开仓库的,即是项目的正式代码库。到此,开发者的贡献完全集成到了项目中
开发者和正式仓库做同步.
由于正式代码库往前走了,其它的开发需要和正式仓库做同步:
$ git pull upstream master
- 感谢你赐予我前进的力量