Git workflow 详谈

作为一名工程师, Git 在日常开发中是不可或缺的工具。
这里详细介绍几种比较常用的基于 Git 的工作流模型, 以便于团队协作的规范化和效率提升。

中心化工作流

使用过SVN的应该都知道, SVN使用的是集中式管理流程, 如果你刚从SVN 切换到 Git , 你可以尝试使用中心化工作流的方式。这样,你几乎不需要变更之前的工作方式, 就可以完成平滑的过渡了。 而且在使用过程中还可以看到 Git 优于 SVN 的地方:
第一,每个成员都可以在本地拥有一份完整的项目代码仓库,而不只是一个工作区的副本,任何人都可以在本地执行 addcommit ,而不需要考虑远端仓库是否有变更,直到需要的时候再去提交即可。
第二,Git 的工作区、暂存区、引用更新等设计,可以给开发者更多自由来切换当前工作,且不会造成代码丢失。

工作细节

中心化工作流的方式是:在远端(远端可以是服务器端,也可以是本地的任意目录)新建一个仓库,默认是 master 分支,作为唯一的中心仓库。 所有人都 clone 这个仓库作为本地仓库,并在本地仓库进行开发。本地的提交是和远端仓库无关的,等需要的时候再 push 进主仓库的 master 分支即可。

在这种方式下, 远端是唯一确定的中心仓库, 所有人都要以这个仓库为准。 所以,在提交之前要先 fetch 最新提交,在这些提交之上作出自己的更改(一般我们使用 rebase来完成)。

如果本地的修改和远端仓库中的变更发生了冲突,那么 Git 会暂停 rebase ,并让你来解决这些冲突。我们可以很简单的使用 git statusgit add 等命令完成冲突的合并。 另外, 如果我们解决不了冲突, 我们也可以使用 git rebase --abort 很容易的退出 rebase 的过程。

这样每天的工作方式就变成了,从中心仓库拉取最新代码, 然后开始一天的工作, 开发完成后,拉取中心仓库的更新, 合并代码后, 再提交至中心仓库, 结束一天的工作。 这样的好处就是不需要变更原先(使用SVN)的工作方式。当然弊端也很明显,你并不知道中心仓库的代码是否是稳定的,或者说并不能确定当你的代码和中心仓库代码合并后,是否是稳定的,带来的问题就是开发进度和回滚不那么方便控制。

示例

我们有两位程序员, A 和 B, 两人同时在对一个项目做开发, 并且使用 Git 的中心化工作流方式。

1.创建远端中心仓库

这里我们有两种方式:

  • 借助于已经搭建好的平台 GitHub/GitLab 之类的,点击 create repo 即可。

  • 在远端(这里只是为了区别本地仓库,事实上,使用任何一个其他人可以连通的机器都可以,包括自己本地其他目录) 创建一个 裸仓库 ,创建裸仓库和我们平时创建本地仓库的区别,可以参考我另一篇文章 Git 本地仓库和裸仓库

这里以第二种方式为例:

1
2
# --bare 参数必须有
git init --bare /the/repo/path.git

2.所有人都 clone 中心仓库到本地作为本地仓库

1
git clone /the/repo/path.git

注意仓库地址必须是正确的, 且有权限访问才能 clone 成功。

3.程序员 A 在他的本地仓库进行功能开发并进行发布

一般情况下,我们通过 git status 看看当前状态,并通过 git addgit commit 等命令完成本地仓库的提交。 当然这个提交影响的也只是本地仓库而已,并没有对中心仓库产生任何影响,所以我们既不需要关心别人有什么提交,也不用担心我们当前的提交是否对别人造成了影响。当 A 认为自己所开发的功能已经完成, 那他将执行 git push origin master 这样的操作,将自己本地仓库所有不存在于中心仓库的提交都 push 到远端的中心仓库上。

4.程序员 B 在他本地仓库进行功能开发

B 在 clone 中心仓库后所做的操作和 A 一样,在本地仓库进行项目开发,并在本地仓库进行提交,他不需要知道中心仓库发生了什么样的变化。

5.程序员 B 将自己开发的功能并进行发布

B 在确认自己开发的功能已经完成后,想要将自己的代码通过 git push origin master 这样的操作发布至中心仓库,但是却被中心仓库提示他的修改已经和中心仓库有了分叉, 需要他先执行 git pull 之类的操作, 将中心仓库上 A 的提交与 B 本地的提交进行合并才允许他并入中心仓库。所以,他执行了 git pull --rebase origin master 来将中心仓库的修改并入他的本地仓库。使用 --rebase 参数的意义在于 fetch 执行完成后,将把 B 的所有提交移至 master 顶端。

当然这里不使用 --rebase 参数也会成功,只不过是会生成一个合并提交,有些情况下使用 --ff 参数也可以避免产生合并提交。在这里使用 --rebase 只是一个建议操作。

如果 A 和 B 修改的文件没有关联,一般情况下会直接完成合并,如果发生冲突,Git 将会暂停 rebase 的过程,并列出当前冲突的文件,你可以简单的使用 git statusgit add 等命令进行合并,合并后使用 git rebase --continue 继续 rebase 的过程。或者使用 git rebase --abort 退出 rebase 过程。

在 B 合并完成后,可以执行 git push origin master 将自己开发的功能发布至中心仓库。

至此,基础的中心化工作流方式就介绍完了,但是这里也很容易看出来其中的问题,除了前面说到过的以外,还有就是效率低下,如果很多人都在持续进行提交,那很影响新功能的提交(多人持续性进行提交)。 一个比较容易提升效率的方式就是切换到特性分支工作流的方式。

特性分支工作流

基于特性的分支工作流,可以为每个特性做隔离,避免对中心仓库主干代码造成影响。

工作细节

顾名思义, 就是根据每个特性都会开一个新的分支,每个分支都应该包含着描述性的名称,无论是一个人开发,还是多人协同,该特性的全部开发工作都在这个分支上进行。待该特性开发完成后, 并入主分支,然后删除分支,代码上线。

这种情况下, 最大的优势在于, 所有的特性开发都可以并行处理。 不必要像中心化工作流方式, 每个人的变动都可能引起其他的人的代码合并, 并且所有功能都杂糅在一起, 从测试和回滚都会变得很繁琐。 另外的一个好处就是特性分支可以推送到中心仓库,这样也便于单独测试。

这里需要注意的是,特性分支往主分支合并的时机,应该是该特性开发完成,并测试通过,避免对主干代码造成污染。

在进行分支隔离后,我们发现,我们当前只处理了开发模式,但并没有涵盖一个很完备的产品生命周期, 开发、发布、维护等过程,所以,我们有了 Gitflow 工作流。

Gitflow 工作流

基于Gitflow 的工作流方式, 这种工作流方式, 主要是管理着新功能开发,发布及维护等模式,根据不同类型的工作对分支进行定义, 分为 特性分支修复分支release 分支开发分支主分支

主分支:中心仓库建立后的默认 master 分支(当然使用其他分支也可以,但要保证该分支是受保护的)。主分支随时保持代码是稳定的,并且有明确的版本标签,后续代码回滚等操作都将从主分支进行。

开发分支:中心仓库建立后,从 master 分支切出来,此时与 master 分支保持一致。后续演进中,开发分支随时保持代码最新,但却不一定是线上实际运行的代码。

1
git checkout -b develop

特性分支:应该从开发分支切出,开发完成后, 再合并进入开发分支, 如果达到了发布标准, 则从开发分支切出 release 分支, 切出来的这个分支,只做该版本内的代码修复, 不再加入新功能, 这时此分支处于锁定的状态。

修复分支, 用于对线上主分支代码的及时修复, 待修复完成后, 合并进入主分支, 再并入开发分支。 修复分支只能从主分支切出。

发版分支, 一般命名为 release-xxx 这个分支只能从开发分支切出, 最后并入主分支,打上版本号的标签,它也应该并入开发分支,如果中间有其他修复的话。

fork 工作流

fork 分支流和上面介绍的所有工作流都不太一样。它的上游有一个唯一仓库, 所有人都是 fork 这个仓库, 在自己的远端和自己的本地各维护一个仓库,待开发完成后推入自己的远端仓库,并结合 GitHub/GitLab等提交 Pull Request,进入了 review 阶段,待通过后,将会被合并入上游唯一的仓库。这种方式比较适合 GitHub 中的大型开源项目, 对于小团队的内部项目, 这种方式可能未必合适。
而且 fork 工作流, 会占用更多的资源(毕竟每个人都维护一份远端仓库)。 而且每个人都看不到其他人的动态,只有当提交 Pull Request 的时候, 才知道每个人发生了什么。

总结

我个人比较推荐的是 Gitflow 的开发工作流, 这种方式下,一切都是可控的, 每个分支都有各自独立的功能,目的性很明确, 同时,在做代码回滚之类的操作也是可以直接剔除。 另外, 在这种工作流方式下, 团队中的每个人都能很轻易的知道其他人在做什么, 做出了什么样的改变, 对于团队协作, 或许更加合适。

当然所有的工作流并不一定能完全套用, 可以吸取一些规范, 合并入自己的日常工作, 将代码仓库的合作流程标准化和规范化, 这也是一切自动化的基础。

<全文完>