理解 Git 开发流程


没有了解设计Git初衷,你就会活在痛苦之中。你可以用很多的参数,来强制Git按照你的想法执行,而不以它自己默认的方式。这就好比拿螺丝刀当锤子用;尽管可行,但是低效,耗时并会损坏螺丝刀。

考虑一个通用的Git流程是如何失效的:

从Master上创建一个分支;写代码;完事后合并回Master。

大多数时候,这是你所期望的,因为Master在你创建分支后有变动。

某天,你合并某个特性分支回Master,但是Master并没有改动。Git会直接把Master的指针指向特性分支的最后一次提交,也叫快进(fast forward),而不是新建一个合并提交。不幸的是,你的特性分支包含一些检查点提交,就是那些备份你的工作,但是令代码处于不稳定状态的经常性提交。现在这些提交,跟Master的稳定提交不是那么容易区分了。这让你回滚时,很容就拿到灾难性的代码。

所以你添加了一条新的规则:“当合并特性分支时,使用-no-ff强制生成一次新的提交。”这样就好了,你可以继续工作了。

直到某天,你在产品代码中发现一个严重的Bug,并且你需要跟踪提交,看看这个Bug是什么时候引入的。你运行bisect,但总是处在检查点性的提交上。你无语放弃,只好手动检查。

现在,你把Bug的范围缩小到单个文件上了。你运行blame,看看该文件最近48小时是如何变动的。尽管你知道不可能,但是blame告诉你,此文件好多星期都没有变化了。原来是blame报告的是初次提交时的更改,而不是合并时的。你的检查点提交几个星期前修改这个文件,但改动在今天才合并进来。

创可贴似的-no-ff参数,不再好使的bisect和神秘的blame都说明了一件事–你在把螺丝刀当锤子使。


反思版本控制

版本控制有两个主要用处:

  1. 协助开发。你需要和组员同步更改,经常性的备份你的工作, 而邮件发送zip文件的方式,是不具备扩展性的。
  2. 配置管理。包括管理并行开发,例如,开发下一个大版本的同时,对产品现有版本应用非经常性的Bugfix。配置管理也用来发现代码改动的准确时间点,是Bug诊断中的一个犀利工具。

从传统意义上来看, 这2个用处是互相冲突的。

当开发一个特性的原型,你应该做经常性的检查点提交。但是,这些提交通常会导致项目构建失败。

理想情况下,历史版本中的每次修改都应是简明和稳定的。没有造成代码不和谐的检查点提交;也没有10000行的超长代码提交。干净的历史版本,不仅有利于撤销或者从各分支中cherry-pick,也有利于以后的检查和分析。然而,维持干净的历史版本意味着要等到代码变更达到完美时才能提交。

那么,你会怎么选?经常性提交,还是干净的历史版本?

如果你加入的是只有两人的初创公司, 干净的历史对你意义不大。你可以对Master随意提交,代码在任何时候部署也没有什么问题。

随着项目的发展–不论是项目成员的增长,还是用户基数变大–你会需要一些工具和技术来保证代码的正确性,包括自动测试,代码审查和干净的历史。

特性分支似乎是个不错的折中方案,它能解决并行开发的基本问题。在分支中开发时,你很少需要考虑项目集成,但将来,总还是要面对它。

当你的项目规模增长到足够大,简单的分支/提交/合并流程开始失效,管道式的开发时代也随之结束。你需要一个干净的历史版本!

Git的出现,很好的解决了上述提到的、相互冲突的2个用处,这是它具有革命性的原因。你可以在经常提交更改的同时,进行原型开发;但是当你完成时,交付一个干净的历史。如果这是你使用Git的目的,它的默认设置更有意义。


工作流程

考虑如下2种分支:公共的和私有的。

公共分支是项目的可靠历史拥有者,每次提交应该是简洁的,原子的,且拥有良好的注释。它应尽可能是线性的。公共分支包括Master分支和Release分支。

私有分支是你个人用的。在你解决问题时,它充当你的草稿本。

最保险的做法是保持私有分支的本地化。也许为了同步公司和家里的电脑,你确实需要推送私有分支到远程服务器。如果你这样做了,告诉你的组员,你推送的分支是私有的,让他们不要基于你的分支做任何开发。

千万不要直接使用简单的merge命令,把你的私有分支合并到公共分支上去。先用reset,rebase,squash merge和commit amending这些工具清理一下你的代码吧。把自己当做一个作家,把每次提交看成是书的章节。作家从不刊发自己的草稿。Michael Crichton说过:“伟大的书不是写出来的,是改出来的。”

如果你没用过Git,修改版本历史感觉像是一种禁忌。你习惯于这样认为:所有已提交的都是板上钉钉的,是不能修改的。如果这样想,那我们就应该禁用文本编辑器上的撤销功能了。

实用主义者在变更造成项目不和谐之前,都不会关心它。从配置管理上讲,我们只关注大的变动,检查点提交只是在线的撤销缓存而已。

如果你的公共分支是干净的,那么,快进式合并不仅是安全的,而且应该是首选。这让版本历史保持线性并易于跟踪。

仅剩的使用-no-ff的理由是文档化。人们用合并提交来表示最近一次部署的产品代码版本。这样做是反模式的,用tag比较靠谱。


指南和用例

根据变动的大小,时长和分支的与Master的偏离程度,我采用三种不同的开发流程。

短时任务

绝大多数时候,我的代码清理只是简单的squash merge。想象我创建了一个特性分支,在接下来得几个小时里,我做了一系列的检查点提交:

1
2
3
4
git checkout -b private_feature_branch
touch file1.txt
git add file1.txt
git commit -am "WIP"

开发完成后,不能只是做简单的merge,我会这样:

1
2
3
git checkout master
git merge --squash private_feature_branch
git commit -v

接下来,我会花一分钟的时间,来编写本次提交的详细注释。

中等任务

有时,一个特性需要几天的时间来做,过程中,会产生十多个小的提交。

我决定把我的改动细化为多个更小的改动,这样,squash就有点太简单粗暴了。(根据经验,我会问自己:“这样会有利于代码审查吗?”)如果检查点提交合乎逻辑,我会使用rebase交互模式。交互模式很好很强大,你能用它编辑提交,分割提交,还能改变提交的顺序。我只需要squah部分提交就可以了。

在特性分支,输入:

1
git rebase --interactive master

这会打开一个编辑窗口,显示所有的提交。每行的格式是这样的:操作,提交的SHA1值,提交注释。窗口下面也给出了可选的命令。默认情况下,每个提交使用pick,这不会修改提交。

1
2
3
pick ccd6e62 Work on back button
pick 1c83feb Bug fixes
pick f9d0c33 Start work on toolbar

我将第二个提交squash到第一个上:

1
2
3
pick ccd6e62 Work on back button
squash 1c83feb Bug fixes
pick f9d0c33 Start work on toolbar

当保存关闭后,新的编辑窗口会弹出,输入提交注释,搞定。

舍弃旧分支

开发时,也许我的特性分支要存在很久,也需要合并其他几个分支的代码,来使它保持同步。这样版本历史就会很复杂。最简单的做法就是:创建一个新分支,把特性分支的代码以squash方式合并过来。

1
2
3
4
git checkout master
git checkout -b cleaned_up_branch
git merge --squash private_feature_branch
git reset

现在,我的工作树上拥有特性分支上的所有代码,版本历史也很简单。然后,我继续。


总结

如果你觉得Git的默认行为不给力,多问问自己:为什么会这样?

保证公共分支版本历史的不可变性,原子性,易跟踪性。私有分支则可随意而为。

下面是我推荐的开发流程:

  1. 从公共分支上创建私有分支;
  2. 经常性的提交代码到私有分支;
  3. 代码完美后,清理版本历史;
  4. 合并清理过的分支回公共分支。

原文地址:Understanding the Git Workflow