没有了解设计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都说明了一件事–你在把螺丝刀当锤子使。
版本控制有两个主要用处:
从传统意义上来看, 这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的默认行为不给力,多问问自己:为什么会这样?
保证公共分支版本历史的不可变性,原子性,易跟踪性。私有分支则可随意而为。
下面是我推荐的开发流程:
- 从公共分支上创建私有分支;
- 经常性的提交代码到私有分支;
- 代码完美后,清理版本历史;
- 合并清理过的分支回公共分支。