Git 工作流指南(总结&&实践)

译序

操作过程去感受指南的讲解:解决什么问题、如何解决问题,这样理解就深了,也方便活用。

工作流其实不是一个初级主题,背后的本质问题其实是 有效的项目流程管理 和
高效的开发协同约定,而不仅是Git或SVN等VCS或SCM工具的使用。

关于Git工作流主题,网上体系的中文资料不多,主要是零散的操作说明,希望这篇文章能让你更深入理解并在工作中灵活有效地使用起来。

Gitflow工作流是经典模型,处于核心位置,体现了工作流的经验和精髓。随着项目过程复杂化,你会感受到这个工作流中的深思熟虑和威力!

工作流指南

工作流有各式各样的用法,但也正因此使得在实际工作中如何上手使用变得很头大。这篇指南通过总览公司团队中最常用的几种Git工作流让大家可以上手使用。

集中式工作流

集中式工作流

但使用Git加强开发的工作流,Git有相比SVN的几个优势。 首先,每个开发可以有属于自己的整个工程的本地拷贝。隔离的环境让各个开发者的工作和项目的其他部分修改独立开来 —— 即自由地提交到自己的本地仓库,先完全忽略上游的开发,直到方便的时候再把修改反馈上去。

其次,Git提供了强壮的分支和合并模型。不像SVN,Git的分支设计成可以做为一种用来在仓库之间集成代码和分享修改的『失败安全』的机制。

工作方式

本工作流只用到master这一个分支。

开发者开始先克隆中央仓库。在自己的项目拷贝中像SVN一样的编辑文件和提交修改;但修改是存在本地的,和中央仓库是完全隔离的。开发者可以把和上游的同步延后到一个方便时间点。

要发布修改到正式项目中,开发者要把本地master分支的修改『推』到中央仓库中。这相当于svn commit操作,但push操作会把所有还不在中央仓库的本地提交都推上去。

上部分为中央仓库

冲突解决

如果开发者本地的提交历史和中央仓库有分歧,Git会拒绝push提交,防止覆盖中央仓库的正式提交

开发者拉取更新,解决冲突,再次提交

在开发者提交自己功能修改到中央库前,需要先fetch在中央库的新增提交,rebase自己提交到中央库提交历史之上。 这样做的意思是在说,『我要把自己的修改加到别人已经完成的修改上。』最终的结果是一个完美的线性历史,就像以前的SVN的工作流中一样。

如果本地修改和上游提交有冲突,Git会暂停rebase过程,给你手动解决冲突的机会。Git解决合并冲突,用和生成提交一样的git status和git add命令,很一致方便。还有一点,如果解决冲突时遇到麻烦,Git可以很简单中止整个rebase操作,重来一次(或者让别人来帮助解决)。

示例

有两个开发者小明和小红,看他们是如何开发自己的功能并提交到中央仓库上的。

初始化好中央仓库

中央仓库

所有人克隆中央仓库

克隆中央仓库

大家好,我是小明

小明克隆中央仓库,并且add commit, 不断开发,直至完成

1
2
3
4
5
$git clone ssh://user@host/path/to/repo.git

3273ecf (HEAD -> master, origin/master) HEAD@{0}: commit: 完成开发
f2faa43 HEAD@{1}: commit: 正在开发
335a4b4 HEAD@{2}: commit (initial): 准备阶段

小明发布功能,因此此时中央仓库是空,小明可以直接提价到中央仓库

1
$git push origin master

注意,origin是在小明克隆仓库时Git创建的远程中央仓库别名。master参数告诉Git推送的分支。 由于中央仓库自从小明克隆以来还没有被更新过,所以push操作不会有冲突,成功完成。


大家好,我是小红

小红也clone了中央仓库,并且进行了歼星舰项目开发,也不断到add commit,直至开发完成

1
2
3
4
5
$git clone http://luoyec@103.224.81.114:6605/luoyec/git_practise.git

ddc6c37 HEAD@{5}: commit: 舰艇开发完成
2899f26 HEAD@{6}: commit: 引擎装载完成
29b95cc HEAD@{7}: commit (initial): 材料准备中

小红也想发布自己功能,但此时,中央仓库也有小明提交但代码,本地历史已经和中央仓库有分岐了,Git不想理你,并扔给你一堆提示信息:

1
2
3
4
5
6
7
8
9
luoyecs-iMac:git_practise LW-luoyec$ git push
To http://103.224.81.114:6605/luoyec/git_practise.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'http://luoyec@103.224.81.114:6605/luoyec/git_practise.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

这避免了小红覆写正式的提交。她要先pull小明的更新到她的本地仓库合并上她的本地修改后,再重试。

git pull

小红用git pull合并上游的修改到自己的仓库中。 这条命令类似svn update——拉取所有上游提交命令到小红的本地仓库,并尝试和她的本地修改合并:

1
$git pull --rebase origin master

==–rebase==选项告诉Git把小红的提交移到同步了中央仓库修改后的master分支的顶部,如下图所示:
小红的仓库

如果你忘加了这个选项,pull操作仍然可以完成,但每次pull操作要同步中央仓库中别人修改时,提交历史会以一个多余的『合并提交』结尾。 对于集中式工作流,最好是使用rebase而不是生成一个合并提交。

小红解决合并冲突

解决合并冲突

冲突解决工具diffmerge

下载地址DMG版
全局配置

1
2
3
4
5
6
git config --global merge.tool diffmerge
git config --global mergetool.diffmerge.cmd "/Applications/DiffMerge.app/Contents/MacOS/diffmerge --merge --result=\$MERGED \$LOCAL \$BASE \$REMOTE"
git config --global mergetool.keepBackup false

git config --global diff.tool diffmerge
git config --global difftool.diffmerge.cmd "/Applications/DiffMerge.app/Contents/MacOS/diffmerge \$LOCAL \$REMOTE"

当出现冲突时,执行命令,弹出窗口手动解决冲突

1
$git mergetool

中间是合并后结果

完成合并,保存退出图形化工具
已经手动解决了冲突,这是让rebase继续工作,执行命令

1
2
3
git add <冲突文件/.  表示所有修改文件>
git rebase --continue //rebase继续工作
git push //代码成功提交

ps:

如果你碰到了冲突,但发现搞不定,不要惊慌。只要执行下面这条命令,就可以回到你执行git pull –rebase命令前的样子:

==git rebase –abort 是无风险的操作,会回到rebase操作之前的状态,2个分支的commits毫发无损。
git rebase –skip 是高风险的操作,引起冲突的commits会被丢弃(这部分代码修改会丢失)。==

1
2
3
4
//解决冲突失败,或者向重新解决冲突
$git rebase --abort
//Skip是跳过这个错误,继续本次操作
git rebase --skip

功能分支工作流

功能分支工作流背后的核心思路是所有的功能开发应该在一个专门的分支,而不是在master分支上。
这个隔离可以方便多个开发者在各自的功能上开发而不会弄乱主干代码。
另外,也保证了master分支的代码一定不会是有问题的,极大有利于集成环境。

工作方式

开发者每次在开始新功能前先创建一个新分支。
功能分支应该有个有描述性的名字,比如animated-menu-items或issue-#1061

在master分支和功能分支之间,Git是没有技术上的区别,所以开发者可以用和集中式工作流中完全一样的方式编辑、暂存和提交修改到功能分支上。

功能分支也可以(且应该)push到中央仓库中。这样不修改正式代码就可以和其它开发者分享提交的功能。
由于master是仅有的一个『特殊』分支,在中央仓库上存多个功能分支不会有任何问题

Pull Requests

一旦完成某个功能的开发,不是立即合并到master,而是push到中央仓库到功能分之,并且在分支上发起一个push requests请求合并到master。在修改成为主干代码前,这让其他的开发者有机会去review变更。

一个开发者开发功能需要帮助时,要做的就是发起一个Pull
Request,相关的人就会自动收到通知,在相关的提交旁边能看到需要帮助解决的问题。

一旦Pull Request被接受了,发布功能要做的就和集中式工作流就很像了。 首先,确定本地的master分支和上游的master分支是同步的。然后合并功能分支到本地master分支并push已经更新的本地master分支到中央仓库。

示例

小红开始开发一个新功能
小红的新功能
在开始开发功能钱,小红需要一个独立到分支,这里新建一个分支

1
2
//-b 不存在则创建, 基于master但new-feature分支
git checkout -b new-feature master

小红按老套路编辑、暂存和提交修改,按需要提交以实现功能:

1
2
3
git status
git add <some-file>
git commit

小红完成开发,push功能分支到中央仓库

1
2
//-u选项设置本地分支去跟踪远程对应的分支, 之后到开发可以省略-u
git push -u origin new-feature

这样功能分支就被提交到中央仓库,这里可以看到有两个分支
新增到功能分支

小红在她的Git GUI客户端中发起Pull Request,请求合并marys-feature到master,团队成员会自动收到通知。这时候小黑,对新功能有自己对想法,决定拉取new-feature到本地,进行修改。

小黑对新功能的开发

1
2
3
4
5
6
7
8
//确保当前最新的
git pull
//查看当前所有对分支,可以看到小红提交的分支
git branch -a

add
commit
push

小黑完成了开发,准备将新功能合并功能到稳定项目代码中(可以由小黑或是小红来做这个操作)

发布流程

1
2
3
4
5
6
7
8
//检出到master分支
git checkout master
//拉取最新仓库
git pull
//new-feature分支到和已经和远程一致的本地master分支
git pull origin new-feature
//更新远程master分支
git push

无论谁来做合并,首先要检出master分支并确认是它是最新的。然后执行git pull origin
marys-feature合并marys-feature分支到和已经和远程一致的本地master分支。 你可以使用简单git merge
marys-feature命令,但前面的命令可以保证总是最新的新功能分支。 最后更新的master分支要重新push回到origin。

Gitflow工作流

Gitflow工作流通过为功能开发、发布准备和维护分配独立的分支,让发布迭代过程更流畅。严格的分支模型也为大型项目提供了一些非常必要的结构。

Gitflow

Gitflow工作流定义了一个围绕项目发布的严格分支模型。虽然比功能分支工作流复杂几分,但提供了用于一个健壮的用于管理大型项目的框架。

Gitflow工作流没有用超出功能分支工作流的概念和命令,而是为不同的分支分配一个很明确的角色,并定义分支之间如何和什么时候进行交互。 除了使用功能分支,在做准备、维护和记录发布也使用各自的分支。 当然你可以用上功能分支工作流所有的好处:Pull Requests、隔离实验性开发和更高效的协作。

工作方式

Gitflow工作流仍然用中央仓库作为所有开发者的交互中心。和其它的工作流一样,开发者在本地工作并push分支到要中央仓库中。

历史分支

相对使用仅有的一个master分支,Gitflow工作流使用2个分支来记录项目的历史。master分支存储了正式发布的历史,而develop分支作为功能的集成分支。 这样也方便master分支上的所有提交分配一个版本号。
历史分支

功能分支

每个新功能位于一个自己的分支,这样可以push到中央仓库以备份和协作。 但功能分支不是从master分支上拉出新分支,==而是使用develop分支作为父分支==。当新功能完成时,合并回develop分支。 新功能提交应该从不直接与master分支交互。

新功能分支不直接与master分支交互

发布分支

develop分支发布
一旦develop分支上有了做一次发布(或者说快到了既定的发布日)的足够功能,就从develop分支上checkout一个发布分支。 新建的分支用于开始发布循环,所以从这个时间点开始之后新的功能不能再加到这个分支上—— ==这个分支只应该做Bug修复、文档生成和其它面向发布任务==。 一旦对外发布的工作都完成了,发布分支合并到master分支并分配一个版本号打好Tag。 另外,这些从新建发布分支以来的做的修改要合并回develop分支。

使用一个用于发布准备的专门分支,使得一个团队可以在完善当前的发布版本的同时,另一个团队可以继续开发下个版本的功能。 这也打造定义良好的开发阶段(比如,可以很轻松地说,『这周我们要做准备发布版本4.0』,并且在仓库的目录结构中可以实际看到)。

常用的分支约定:

1
2
3
用于新建发布分支的分支: develop
用于合并的分支: master
分支命名: release-* 或 release/*

维护分支

分支维护
维护分支或说是热修复(hotfix)分支用于生成快速给产品发布版本(production releases)打补丁,这是唯一可以直接从master分支fork出来的分支。 修复完成,修改应该马上合并回master分支和develop分支(当前的发布分支),master分支应该用新的版本号打好Tag。

为Bug修复使用专门分支,让团队可以处理掉问题而不用打断其它工作或是等待下一个发布循环。 你可以把维护分支想成是一个直接在master分支上处理的临时发布。

示例

  • 创建开发分支
    新分支
    第一步为master分支配套一个develop分支。简单来做可以本地创建一个空的develop分支,push到服务器上
1
2
git branch develop
git push -u origin develop

以后这个分支将会包含了项目的全部历史,而master分支将只包含了部分历史。其它开发者这时应该克隆中央仓库,建好develop分支的跟踪分支:

1
2
git clone ssh://user@host/path/to/repo.git
git checkout -b develop origin/develop

现在每个开发都有了这些历史分支的本地拷贝。

  • 小红和小明开始开发新功能
    各自新建功能分支

这个示例中,小红和小明开始各自的功能开发。他们需要为各自的功能创建相应的分支。新分支不是基于master分支,而是应该基于develop分支:

1
git checkout -b some-feature develop

他们用老套路添加提交到各自功能分支上:编辑、暂存、提交:

1
2
3
git status
git add <some-file>
git commit
  • 小红完成功能开发
    添加了提交后,小红觉得她的功能OK了。如果团队使用Pull Requests,这时候可以发起一个用于合并到develop分支。 否则她可以直接合并到她本地的develop分支后push到中央仓库:
1
2
3
4
5
git pull origin develop
git checkout develop
git merge some-feature
git push
git branch -d some-feature

第一条命令在合并功能前确保develop分支是最新的。注意,功能决不应该直接合并到master分支。 冲突解决方法和集中式工作流一样。

  • 小红开始准备发布
    功能发布
    这个时候小明正在实现他的功能,小红开始准备她的第一个项目正式发布。 像功能开发一样,她用一个新的分支来做发布准备。这一步也确定了发布的版本号:
1
2
//新建一个基于develop对分支用于版本发布,此时并不会影响其他开发者
git checkout -b release-0.1 develop

这个分支是清理发布、执行所有测试、更新文档和其它为下个发布做准备操作的地方,像是一个专门用于改善发布的功能分支。

只要小红创建这个分支并push到中央仓库,这个发布就是功能冻结的。任何不在develop分支中的新功能都推到下个发布循环中。

  • 小红完成发布
    完成发布
    一旦准备好了对外发布,小红合并修改到master分支和develop分支上,删除发布分支。合并回develop分支很重要,因为在发布分支中已经提交的更新需要在后面的新功能中也要是可用的。 另外,如果小红的团队要求Code Review,这是一个发起Pull Request的理想时机。
1
2
3
4
5
6
7
8
9
10
/*
两次检出,两次合并,为的是合并到master和develop
*/
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分支,就应该打好Tag以方便跟踪。

1
2
git tag -a 0.1 -m "Initial public release" master
git push --tags

Git有提供各种勾子(hook),即仓库有事件发生时触发执行的脚本。 可以配置一个勾子,在你push中央仓库的master分支时,==自动构建==好对外发布。

  • 最终用户发现Bug
    bug分支
    对外发布后,小红回去和小明一起做下个发布的新功能开发,直到有最终用户开了一个Ticket抱怨当前版本的一个Bug。 为了处理Bug,小红(或小明)从master分支上拉出了一个维护分支,提交修改以解决问题,然后直接合并回master分支:
1
2
3
4
5
6
//从master分支中拉出一个bug分支,解决完后合并到master
git checkout -b issue-#001 master
# Fix the bug
git checkout master
git merge issue-#001
git push

就像发布分支,维护分支中新加这些重要修改需要包含到develop分支中,所以小红要执行一个合并操作。然后就可以安全地删除这个分支了:

1
2
3
4
git checkout develop
git merge issue-#001
git push
git branch -d issue-#001

到了这里,但愿你对集中式工作流、功能分支工作流和Gitflow工作流已经感觉很舒适了。 你应该也牢固的掌握了本地仓库的潜能,push/pull模式和Git健壮的分支和合并模型。

Forking工作流

Forking工作流是分布式工作流,充分利用了Git在分支和克隆上的优势。可以安全可靠地管理大团队的开发者(developer),并能==接受不信任==贡献者(contributor)的提交。

Forking工作流和前面讨论的几种工作流有根本的不同,这种工作流不是使用单个服务端仓库作为『中央』代码基线,而让各个开发者都有一个服务端仓库。这意味着各个代码贡献者有2个Git仓库而不是1个:一个本地私有的,另一个服务端公开的。

fork仓库成为私有仓库
Forking工作流的一个主要优势是,贡献的代码可以被集成,而不需要所有人都能push代码到仅有的中央仓库中。 开发者push到自己的服务端仓库,而只有项目维护者才能push到正式仓库。 这样项目维护者可以接受任何开发者的提交,但无需给他正式代码库的写权限。

效果就是一个分布式的工作流,能为大型、自发性的团队(包括了不受信的第三方)提供灵活的方式来安全的协作。 也让这个工作流成为开源项目的理想工作流。

工作方式

和其它的Git工作流一样,Forking工作流要先有一个公开的正式仓库存储在服务器上。 但一个新的开发者想要在项目上工作时,不是直接从正式仓库克隆,而是fork正式项目在服务器上创建一个拷贝。

这个仓库拷贝作为他个人公开仓库 —— 其它开发者不允许push到这个仓库,但可以pull到修改(后面我们很快就会看这点很重要)。 在创建了自己服务端拷贝之后,和之前的工作流一样,开发者执行git clone命令克隆仓库到本地机器上,作为私有的开发环境。

要提交本地修改时,push提交到自己公开仓库中 —— 而不是正式仓库中。 然后,给正式仓库发起一个pull request,让项目维护者知道有更新已经准备好可以集成了。 对于贡献的代码,pull request也可以很方便地作为一个讨论的地方。

为了集成功能到正式代码库,==维护者pull贡献者的变更到自己的本地仓库中,检查变更以确保不会让项目出错, 合并变更到自己本地的master分支, 然后pushmaster分支到服务器的正式仓库中==。 到此,贡献的提交成为了项目的一部分,其它的开发者应该执行pull操作与正式仓库同步自己本地仓库。

正式仓库

在Forking工作流中,『官方』仓库的叫法只是一个约定,理解这点很重要。 从技术上来看,各个开发者仓库和正式仓库在Git看来没有任何区别。 事实上,让正式仓库之所以正式的唯一原因是它是项目维护者的公开仓库。

Forking工作流的分支使用方式

所有的个人公开仓库实际上只是为了方便和其它的开发者共享分支。 各个开发者应该用分支隔离各个功能,就像在功能分支工作流和Gitflow工作流一样。 唯一的区别是这些分支被共享了。在Forking工作流中这些分支会被pull到另一个开发者的本地仓库中,而在功能分支工作流和Gitflow工作流中是直接被push到正式仓库中。

示例

  • 项目维护者初始化正式仓库

    和任何使用Git项目一样,第一步是创建在服务器上一个正式仓库,让所有团队成员都可以访问到。 通常这个仓库也会作为项目维护者的公开仓库。

公开仓库应该是裸仓库,不管是不是正式代码库。 所以项目维护者会运行像下面的命令来搭建正式仓库:

1
2
ssh user@host
git init --bare /path/to/repo.git

Bitbucket和Stash提供了一个方便的GUI客户端以完成上面命令行做的事。 这个搭建中央仓库的过程和前面提到的工作流完全一样。 如果有现存的代码库,维护者也要push到这个仓库中。

  • 开发者fork正式仓库


其它所有的开发需要fork正式仓库。 可以用git clone命令用SSH协议连通到服务器, 拷贝仓库到服务器另一个位置 —— 是的,fork操作基本上就只是一个服务端的克隆。 Bitbucket和Stash上可以点一下按钮就让开发者完成仓库的fork操作。

这一步完成后,每个开发都在服务端有一个自己的仓库。和正式仓库一样,这些仓库应该是裸仓库。

  • 开发者克隆自己fork出来的仓库

下一步,各个开发者要克隆自己的公开仓库,用熟悉的git clone命令。

在这个示例中,假定用Bitbucket托管了仓库。记住,如果这样的话各个开发者需要有各自的Bitbucket账号, 使用下面命令克隆服务端自己的仓库:

1
git clone https://user@bitbucket.org/user/repo.git

相比前面介绍的工作流只用了一个origin远程别名指向中央仓库,Forking工作流需要2个远程别名 —— 一个指向正式仓库,另一个指向开发者自己的服务端仓库。别名的名字可以任意命名,常见的约定是使用origin作为远程克隆的仓库的别名 (这个别名会在运行git clone自动创建),upstream(上游)作为正式仓库的别名。

1
git remote add upstream https://bitbucket.org/maintainer/repo

需要自己用上面的命令创建upstream别名。这样可以简单地保持本地仓库和正式仓库的同步更新。 注意,如果上游仓库需要认证(比如不是开源的),你需要提供用户:

1
git remote add upstream https://user@bitbucket.org/maintainer/repo.git

这时在克隆和pull正式仓库时,需要提供用户的密码。

  • 开发者开发自己的功能

    在刚克隆的本地仓库中,开发者可以像其它工作流一样的编辑代码、提交修改和新建分支:
1
2
3
git checkout -b some-feature
# Edit some code
git commit -a -m "Add first draft of some feature"

所有的修改都是私有的直到push到自己公开仓库中。如果正式项目已经往前走了,可以用git pull命令获得新的提交:

1
git pull upstream master
  • 开发者发布自己的功能
    由于开发者应该都在专门的功能分支上工作,pull操作结果会都是快进合并。

一旦开发者准备好了分享新功能,需要做二件事。 首先,通过push他的贡献代码到自己的公开仓库中,让其它的开发者都可以访问到。 他的origin远程别名应该已经有了,所以要做的就是:

1
git push origin feature-branch

这里和之前的工作流的差异是,origin远程别名指向开发者自己的服务端仓库,而不是正式仓库。

第二件事,开发者要通知项目维护者,想要合并他的新功能到正式库中。 Bitbucket和Stash提供了Pull Request按钮,弹出表单让你指定哪个分支要合并到正式仓库。 一般你会想集成你的功能分支到上游远程仓库的master分支中。

  • 项目维护者集成开发者的功能


当项目维护者收到pull request,他要做的是决定是否集成它到正式代码库中。有二种方式来做:

直接在pull request中查看代码
pull代码到他自己的本地仓库,再手动合并
第一种做法更简单,维护者可以在GUI中查看变更的差异,做评注和执行合并。 但如果出现了合并冲突,需要第二种做法来解决。这种情况下,维护者需要从开发者的服务端仓库中fetch功能分支, 合并到他本地的master分支,解决冲突:

1
2
3
4
git fetch https://bitbucket.org/user/repo feature-branch
# 查看变更
git checkout master
git merge FETCH_HEAD

变更集成到本地的master分支后,维护者要push变更到服务器上的正式仓库,这样其它的开发者都能访问到:

1
git push origin master

注意,维护者的origin是指向他自己公开仓库的,即是项目的正式代码库。到此,开发者的贡献完全集成到了项目中。

  • 开发者和正式仓库做同步

    由于正式代码库往前走了,其它的开发需要和正式仓库做同步:
1
git pull upstream master

如果你之前是使用SVN,Forking工作流可能看起来像是一个激进的范式切换(paradigm shift)。 但不要害怕,这个工作流实际上就是在功能分支工作流之上引入另一个抽象层。 不是直接通过单个中央仓库来分享分支,而是把贡献代码发布到开发者自己的服务端仓库中。

示例中解释了,一个贡献如何从一个开发者流到正式的master分支中,但同样的方法可以把贡献集成到任一个仓库中。 比如,如果团队的几个人协作实现一个功能,可以在开发之间用相同的方法分享变更,完全不涉及正式仓库。

这使得Forking工作流对于松散组织的团队来说是个非常强大的工具。任一开发者可以方便地和另一开发者分享变更,任何分支都能有效地合并到正式代码库中。

参考链接:xirongGit仓库