跳转至

你好,我是陈皓,网名左耳朵耗子。

与传统的代码版本管理工具相比,Git有很多的优势,因而越来越成为程序员喜欢的版本管理工具。我觉得,Git这个代码版本管理工具最大的优势有以下几个。

  • Git 是一个分布式的版本管理工具,而且可以是单机版的,所以,你在没有网络的时候同样可以提交(commit)代码。对于我们来说,这意味着在出差途中或是没有网络的环境中依然可以工作写代码。

这是不是听起来有点不对?一方面,以后你再也不能以“没有网络”作为不能工作的借口了。另一方面,没有网络意味着没有Google和StackOverflow,光有个本地的Git我也一样不能写代码啊……(哈哈。好吧,这已经超出了Git这个技术的范畴了,这里就不讨论了) - Git从一个分支向另一个分支合并代码的时候,会把要合并的分支上的所有提交一个一个应用到被合并的分支上,合并后也能看得到整个代码的变更记录。而其他的版本管理工具则不能。 - Git切换分支的时候通常很快。不像其他版本管理器,每个分支一份拷贝。 - Git有很多非常有用的命令,让你可以很方便地工作。

比如我很喜欢的git stash命令,可以把当前没有完成的事先暂存一下,然后去忙别的事。git cherry-pick命令可以让你有选择地合并提交。git add -p可以让你挑选改动提交,git grep $regexp $(git rev-list --all)可以用来在所有的提交中找代码。因为都是本地操作,所以你会觉得速度飞快。

除此之外,由Git衍生出来的GitHub/GitLab 可以帮你很好地管理编程工作,比如wiki、fork、pull request、issue 等等,集成了与编程相关的工作,让人觉得这不是一个冷冰冰的工具,而是真正和我们的日常工作发生了很好的交互。

GitHub/GitLab这样工具的出现,让我们的工作可以呈现在一个工作平台上,并以此来规范整个团队的工作,这才正是Git这个版本管理工具成功的原因。

今天,我们不讲Git是怎么用的,因为互联网上有太多的文章和书了。而且,如果你还不会用Git的话,那么我觉得你已经严重落后于这个时代了。在这节课中,我想讲一下Git的协同工作流,因为我看到很多团队在使用Git时,并没有用好。

注意,因为Git是一个分布式的代码管理器,所以,是分布式就会出现数据不一致的情况,因此,我们需要一个协同工作流来让工作变得高效,同时可以有效地让代码具有更好的一致性。

说到一致性,就是每个人手里的开发代码,还有测试和生产线上的代码,要有一个比较好的一致性的管理和协同方法。这就是Git协同工作流需要解决的问题。

目前来说,你可能以为我想说的是GitFlow工作流。恭喜你猜对了。但是,我想说的是,GitFlow工作流太过复杂,我并不觉得GitFlow工作流是一个好的工作流。如果你的团队在用这种工作流开发软件,我相信你的感觉一定是糟透了。

所以,这节课我会对比一些比较主流的协同工作流,然后,再抨击一下GitFlow工作流。

中心式协同工作流

首先,我们先说明一下,Git是可以像SVN这样的中心工作流一样工作的。我相信很多程序员都是在采用这样的工作方式。

这个过程一般是下面这个样子的。

  1. 从服务器上做git pull origin master把代码同步下来。
  2. 改完后,git commit到本地仓库中。
  3. 然后git push origin master到远程仓库中,这样其他同学就可以得到你的代码了。

如果在第3步发现push失败,因为别人已经提交了,那么你需要先把服务器上的代码给pull下来,为了避免有merge动作,你可以使用 git pull --rebase 。这样就可以把服务器上的提交直接合并到你的代码中,对此,Git的操作是这样的。

  1. 先把你本地提交的代码放到一边。
  2. 然后把服务器上的改动下载下来。
  3. 然后在本地把你之前的改动再重新一个一个地做commit,直到全部成功。

如下图所示,Git 会把 Origin/Master 的远程分支下载下来,然后把本地的Master分支上的改动一个一个地提交上去。


如果有冲突,那么你要先解决冲突,然后做 git rebase --continue 。如下图所示,git在做 pull --rebase 时,会一个一个地应用(apply)本地提交的代码,如果有冲突就会停下来,等你解决冲突。

功能分支协同工作流

上面的那种方式有一个问题,就是大家都在一个主干上开发程序,对于小团队或是小项目你可以这么干,但是对比较大的项目或是人比较多的团队,这么干就会有很多问题。

最大的问题就是代码可能干扰太严重。尤其是,我们想安安静静地开发一个功能时,我们想把各个功能的代码变动隔离开来,同时各个功能又会有多个开发人员在开发。

这时,我们不想让各个功能的开发人员都在Master分支上共享他们的代码。我们想要的协同方式是这样的:同时开发一个功能的开发人员可以分享各自的代码,但是不会把代码分享给开发其他功能的开发人员,直到整个功能开发完毕后,才会分享给其他的开发人员(也就是进入主干分支)。

因此,我们引入“功能分支”。这个协同工作流的开发过程如下。

  1. 首先使用 git checkout -b new-feature 创建 “new-feature”分支。
  2. 然后共同开发这个功能的程序员就在这个分支上工作,进行add、commit等操作。
  3. 然后通过 git push -u origin new-feature 把分支代码push到服务器上。
  4. 其他程序员可以通过git pull --rebase来拿到最新的这个分支的代码。
  5. 最后通过Pull Request的方式做完Code Review后合并到Master分支上。

就像上面这个图显示的一样,绿色的分支就是功能分支,合并后就会像上面这个样子。

我们可以看到,其实,这种开发也是以服务器为中心的开发,还不是Git分布式开发,它只不过是用分支来完成代码改动的隔离。

另外,我想提醒一下,为什么会叫“功能分支”,而不是“项目分支”?因为Git的最佳实践希望大家在开发的过程中,快速提交,快速合并,快速完成。这样可以少很多冲突的事,所以叫功能分支。

传统的项目分支开得太久,时间越长就越合不回去。这种玩法其实就是让我们把一个大项目切分成若干个小项目来执行(最好是一个小功能一个项目)。这样才是互联网式的快速迭代式的开发流程。

GitFlow协同工作流

在真实的生产过程中,前面的协同工作流还是不能满足工作的要求。这主要因为我们的生产过程是比较复杂的,软件生产中会有各式各样的问题,并要面对不同的环境。我们要在不停地开发新代码的同时,维护线上的代码,于是,就有了下面这些需求。

  1. 希望有一个分支是非常干净的,上面是可以发布的代码,上面的改动永远都是可以发布到生产环境中的。这个分支上不能有中间开发过程中不可以上生产线的代码提交。
  2. 希望当代码达到可以上线的状态时,也就是在alpha/beta release时,在测试和交付的过程中,依然可以开发下一个版本的代码。
  3. 最后,对于已经发布的代码,也会有一些Bug-fix的改动,不会将正在开发的代码提交到生产线上去。

你看,面对这些需求,前面的那些协同方式就都不行了。因为我们不仅是要在整个团队中共享代码,我们要的更是管理好不同环境下的代码不互相干扰。说得技术一点儿就是,要管理好代码与环境的一致性。

为了解决这些问题,GitFlow协同工作流就出来了。

GitFlow协同工作流是由Vincent Driessen于2010年在A successful Git branching model这篇文章介绍给世人的。

这个协同工作流的核心思想如下图所示。


整个代码库中一共有五种分支。

  • Master分支。也就是主干分支,用作发布环境,上面的每一次提交都是可以发布的。
  • Feature分支。也就是功能分支,用于开发功能,其对应的是开发环境。
  • Developer分支。是开发分支,一旦功能开发完成,就向Developer分支合并,合并完成后,删除功能分支。这个分支对应的是集成测试环境。
  • Release分支。当Developer分支测试达到可以发布状态时,开出一个Release分支来,然后做发布前的准备工作。这个分支对应的是预发环境。之所以需要这个Release分支,是我们的开发可以继续向前,不会因为要发布而被block住而不能提交。

一旦Release分支上的代码达到可以上线的状态,那么需要把Release分支向Master分支和Developer分支同时合并,以保证代码的一致性。然后再把Release分支删除掉。

  • Hotfix分支。是用于处理生产线上代码的Bug-fix,每个线上代码的Bug-fix都需要开一个Hotfix分支,完成后,向Developer分支和Master分支上合并。合并完成后,删除Hotfix分支。

这就是整个GitFlow协同工作流的工作过程。我们可以看到:

  1. 我们需要长期维护Master和Developer两个分支。
  2. 这其中的方式还是有一定复杂度的,尤其是Release和Hotfix分支需要同时向两个分支作合并。所以,如果没有一个好的工具来支撑的话,这会因为我们可能会忘了做一些操作而导致代码不一致。
  3. GitFlow协同虽然工作流比较重。但是它几乎可以应对所有公司的各种开发流程,包括瀑布模型,或是快速迭代模型。

GitHub/GitLab 协同工作流

GitFlow的问题

对于GitFlow来说,虽然可以解决我们的问题,但是也有很多问题。在GitFlow流行了一段时间后,圈内出现了一些不同的声音。参看下面两篇吐槽文章。

其中有个问题就是因为分支太多,所以会出现git log混乱的局面。具体来说,主要是git-flow使用git merge --no-ff来合并分支,在git-flow这样多个分支的环境下会让你的分支管理的log变得很难看。如下所示,左边是使用–no-ff参数在多个分支下的问题。

所谓--no-ff参数的意思是——no fast forward的意思。也就是说,合并的方法是不要把这个分支的提交以前置合并的方式,而是留下一个merge的提交。这是把双刃剑,我们希望我们的--no-ff能像右边那样,而不是像左边那样。

对此的建议是:只有feature合并到developer分支时,使用–no-ff参数,其他的合并都不使用--no-ff参数来做合并。

另外,还有一个问题就是,在开发得足够快的时候,你会觉得同时维护Master和Developer两个分支是一件很无聊的事,因为这两个分支在大多数情况下都是一样的。包括Release分支,你会觉得创建的这些分支太无聊。

而你的整个开发过程也会因为这么复杂的管理变得非常复杂。尤其当你想回滚某些人的提交时,你就会发现这事似乎有点儿不好干了。而且在工作过程中,你会来来回回地切换工作的分支,有时候一不小心没有切换,就提交到了不正确的分支上,你还要回滚和重新提交,等等。

GitLab一开始是GitFlow的坚定支持者,后来因为这些吐槽,以及Hacker News和Reddit上大量的讨论,GitLab也开始不玩了。他们写了一篇blog来创造了一个新的Workflow——GitLab Flow,这个GitLab Flow是基于GitHub Flow来做的(参看: GitHub Flow )。

GitHub Flow

所谓GitHub Flow,其实也叫Forking flow,也就是GitHub上的那个开发方式。

  1. 每个开发人员都把“官方库”的代码fork到自己的代码仓库中。
  2. 然后,开发人员在自己的代码仓库中做开发,想干啥干啥。
  3. 因此,开发人员的代码库中,需要配两个远程仓库,一个是自己的库,一个是官方库(用户的库用于提交代码改动,官方库用于同步代码)。
  4. 然后在本地建“功能分支”,在这个分支上做代码开发。
  5. 这个功能分支被push到开发人员自己的代码仓库中。
  6. 然后,向“官方库”发起pull request,并做Code Review。
  7. 一旦通过,就向官方库进行合并。

这就是GitHub的工作流程。

如果你有“官方库”的权限,那么就可以直接在“官方库”中建功能分支开发,然后提交pull request。通过Code Review后,合并进Master分支,而Master一旦有代码被合并就可以马上release。

这是一种非常Geek的玩法。这需要一个自动化的CI/CD工具做辅助。是的,CI/CD应该是开发中的标配了。

GitLab Flow

然而,GitHub Flow这种玩法依然会有好多问题,因为其虽然变得很简单,但是没有把我们的代码和我们的运行环境给联系在一起。所以,GitLab提出了几个优化点。

其中一个是引入环境分支,如下图所示,其包含了预发布(Pre-Production)和生产(Production)分支。


而有些时候,我们还会有不同版本的发布,所以,还需要有各种release的分支。如下图所示。Master分支是一个roadmap分支,然后,一旦稳定了就建稳定版的分支,如2.3.stable分支和2.4.stable分支,其中可以cherry-pick master分支上的一些改动过去。


这样也就解决了两个问题:

  • 环境和代码分支对应的问题;
  • 版本和代码分支对应的问题。

老实说,对于互联网公司来说,环境和代码分支对应这个事,只要有个比较好的CI/CD生产线,这种环境分支应该也是没有必要的。而对于版本和代码分支的问题,我觉得这应该是有意义的,但是,最好不要维护太多的版本,版本应该是短暂的,等新的版本发布时,老的版本就应该删除掉了。

协同工作流的本质

对于上面这些各式各样的工作流的比较和思考,虽然,我个人非常喜欢GitHub Flow,在必要的时候使用上GitLab中的版本或环境分支。不过,我们现实生活中,还是有一些开发工作不是以功能为主,而是以项目为主的。也就是说,项目的改动量可能比较大,时间和周期可能也比较长。

我在想,是否有一种工作流,可以面对我们现实工作中的各种情况。但是,我想这个世界太复杂了,应该不存在一种一招鲜吃遍天的放之四海皆准的银弹方案。所以,我们还要根据自己的实际情况来挑选适合我们的协同工作的方式。

而代码的协同工作流属于SCM(Software Configuration Management)的范畴,要挑选好适合自己的方式,我们需要知道软件工程配置管理的本质。

根据这么多年来我在各个公司的经历,有互联网的,有金融的,有项目的,有快速迭代的等,我认为团队协同工作的本质不外乎这么几个事儿。

  1. 不同的团队能够尽可能地并行开发。
  2. 不同软件版本和代码的一致性。
  3. 不同环境和代码的一致性。
  4. 代码总是会在稳定和不稳定间交替。我们希望生产线上的代码总是能对应到稳定的代码上来。

基本在上述的四个事儿中,上述的工作流大都是在以建立不同的分支,来做到开发并行、代码和环境版本一致,以及稳定的代码。

要选择适合自己的协同工作流,我们就不得不谈一下软件开发的工作模式。

首先,我们知道软件开发的趋势一定是下面这个样子的。

  • 以微服务或是SOA为架构的方式。一个大型软件会被拆分成若干个服务,那么,我们的代码应该也会跟着服务拆解成若干个代码仓库。这样一来,我们的每个代码仓库都会变小,于是我们的协同工作流程就会变简单。

对于每个服务的代码仓库,我们的开发和迭代速度也会变得很快,开发团队也会跟服务一样被拆分成多个小团队。这样一来, GitFlow这种协同工作流程就非常重了,而GitHub这种方式或是功能分支这种方式会更适合我们的开发。 - 以DevOps为主的开发流程。DevOps关注于CI/CD,需要我们有自动化的集成测试和持续部署的工具。这样一来,我们的代码发布速度就会大大加快,每一次提交都能很快地被完整地集成测试,并很快地发布到生产线上。

于是,我们就可以使用更简单的协同工作流程,不需要维护多个版本,也不需要关注不同的运行环境,只需要一套代码,就可以了。GitHub Flow或是功能分支这种方式也更适应这种开发。

你看,如果我们将软件开发升级并简化到SOA服务化以及DevOps上来,那么协同工作流就会变得非常简单。所以,协同工作流的本质,并不是怎么玩好代码仓库的分支策略,而是玩好我们的软件架构和软件开发流程

当然,服务化和DevOps是每个开发团队需要去努力的目标,但就算是这样,也有某些情况我们需要用重的协同工作的模式。比如,整个公司在做一个大的升级项目,这其中会对代码做一个大的调整(很有可能是一次重大的重构)。

这个时候,可能还有一些并行的开发需要做,如一些小功能的优化,一些线上Bug的处理,我们可能还需要在生产线上做新旧两个版本的A/B测试。在这样的情况下,我们可能会或多或少地使用GitFlow协同工作流。

但是,这样的方式不会是常态,是特殊时期,我们不可能隔三差五地对系统做架构或是对代码做大规模的重构。所以,在大多数情况下,我们还是应该选择一个比较轻量的协同工作流,而在特殊时期特例特办。

最后,让我用一句话来结束这节课:与其花时间在Git协同工作流上,还不如把时间花在调整软件架构和自动化软件生产和运维流程上来,这才是真正简化协同工作流程的根本

精选留言(15)
  • 王磊 👍(15) 💬(2)

    git state还是个git stash?

    2018-06-15

  • 龍蝦 👍(108) 💬(5)

    Git-Flow 和 GitLab-Flow 都有 release 分支,虽然都称为 release,但作用完全不同。 在 Git-Flow 中,release 是介于 dev 和 master 之间,作用是发布前的准备工作(通常应该是用于更全面测试),从 dev 分叉 release 是为了避免阻塞 dev 分支上继续开发新功能;release 属于临时性分支,准备工作完成后是以 Fast-Forward 方式合并到 master;release 上可能会修复一些 bug,所以还要把 release 合并回 dev;发布软件是从 master 上构建。从名称看,release 应该叫做 pre-release,而 master 应该叫 release 。 在 GitLab-Flow 中,release 是从 master 分叉出来,可能会有多个 release 分支,对应软件不同版本。从 master 分叉一个 release (版本)后,可能发现还需要合入一些新功能,所以通过 cherry-pick 方式从 master 中挑选某些功能,不能直接从 master 合并,因为仅需要 master 上的某些新功能而不是全部新功能。release 属于长期分支,而且分叉以后不会再与其它分支合并。除非某个版本不再维护,不然对应 release 会持续存在,因为发布出去的软件随时可能发现 bug 需要修复。

    2018-01-17

  • cellardoor 👍(31) 💬(2)

    有些人喜欢把事情做的很复杂,以至于看不出有什么问题。有些人喜欢把事情做的很简单,看起来似乎有些问题。

    2018-01-19

  • mgxian 👍(26) 💬(2)

    请问老师的架构图 示意图 是用什么软件画的 感觉很不错

    2018-01-22

  • Rain 👍(24) 💬(0)

    分享个小技巧 mac下使用iterm2终端搭配zsh,有非常好用的git快捷键(当然也可以自己alias),类似gco,gcd,gcm,gl,ggpush,ggsup等等,能少敲不少重复的字符,极大提升效率

    2018-12-13

  • tao 👍(16) 💬(0)

    我觉得看团队大小吧。7人以下的团队小而快,直接全部用单master分支就可以,所有人用rebase,防止出现太多分叉。feature分支和bug分支都开发人员自己管理就好

    2018-06-17

  • 温LELE🍀 👍(15) 💬(0)

    几年前,微软终于开始普及Git了,选择的协同工作流是GibHub flow,大大滴改善了生产效率。 读了这篇文章,了解了还有其他工作流方式,还是觉得GitHub简单些,更适合服务化的场景。

    2019-03-09

  • jerry Zhou 👍(12) 💬(8)

    为了避免有 merge 动作,你可以使用 git pull --rebase 。这样就可以把服务器上的提交直接合并到你的代码中,对此,Git 的操作是这样的。 为什么要避免merge动作?

    2018-08-14

  • 飞扬 👍(10) 💬(3)

    在百度内部开发使用一种叫做「分支开发分支发布」的工作流,虽然不算银弹吧,但是总的来说比文中提到的更简单清晰一些,也更容易上手,配合agile这个持续集成工具,是我目前见到的最好的协同工作流了。

    2019-03-21

  • 绿茶 👍(7) 💬(0)

    Git只是工具,花大把时间还不如花在软件架构上自动化上,哈

    2020-04-24

  • edisonhuang 👍(5) 💬(0)

    git协同工作流的背后,其实隐藏着公司软件架构和自动化软件生产和运维。当采用SOA服务式的架构后,团队自然的被拆分得比较小,在团队内部几乎是采用中心协同工作流的方式,开发迭代速度非常快,而整个系统在paas集成又确保了公司范围内不同组并行协作,从而保证公司级别的人效最大化。 不同的协作方式,工作流模式,目标都在于尽可能的在开发效率和沟通协作两个方面做权衡,让模式尽可能适应团队的规模,从而保证人效的提高。

    2019-05-31

  • 金霖 👍(4) 💬(0)

    读了之后有个很明显的感觉就是--万事万物没有银弹: 很少有一种工具、方法,适合所有的情况,更多是根据自己的情况挑选最适合的方法。 同时要意识到,环境是在变化、技术在更新,随时有都有可能有更适合当前环境的工具,也随时有可能发生大的环境变化。 关心事物的本质特征,关心工具解决的核心问题,关心工具的核心原理,这样就更容易选择、切换合适的工具来提升效率、质量。

    2019-12-14

  • 小毅 👍(4) 💬(2)

    皓哥,文章里面的说:一个大型软件会被拆分成若干个服务,那么代码应该也会跟着服务拆解成若干个代码仓库~ 这样随着组里面的项目越来越多,开发维护起来会不会不方便?

    2018-01-16

  • 佐倉 桜 👍(3) 💬(1)

    git push -f 一把梭 : )

    2021-06-24

  • BeginYan 👍(2) 💬(1)

    很多大公司都推崇主干开发,分支上线的方式,耗子叔怎么看?

    2020-04-27