更优雅地使用 Git


一个小问题的思考

日常的开发和测试工作中,你是否有以下烦恼?

  • 分支生命周期过长,协作开发困难
  • 不敢轻易改动他人的代码,不敢重构
  • 为杂乱无序的提交记录费神,疲于解决提交冲突
  • 不同环境测试的代码具有不确定性
  • 为了保证合并代码不出错,反复进行回归测试

Git为开发和测试提供了一个高效的工具,但往往我们在使用的过程中没有发挥出他最大的潜力,甚至因为使用不当,人为造成不必要的问题。

我们的开发流程是比较标准传统的 GitFlow 分支开发流程:

  1. 研发基于不同的特性拉取不同的 feature 分支进行代码开发
  2. 开发完成之后,合并到 dev 分支,在开发环境完成联调和自测
  3. 自测完成,合并代码到 qa 分支,由测试开始执行测试用例
  4. 测试通过,合并代码到 release 分支并发布到预发布环境,由测试利用线上数据进行回归测试
  5. 回归完成,合并代码到 master 分支并发布到生产环境,由测试和产品回归和验收产品功能

整个流程涉及4次必要的代码合并和2次必要的回归测试流程。这种情况下,即使回归测试已经比较全面了,是否能保证上线的代码足够可靠呢?

近期我们团队在发布的时候发生了两个小插曲:

  1. 合并代码到 master 分支,没有任何冲突,但发布上线后发现代码错误
  2. 合并代码到 master 分支的过程中,对冲突不够熟悉,选择了错误的代码

这些问题可以通过分支管理规避吗?请往后看。

杂乱无序的提交记录

Git为开发和测试提供了方便且优雅的版本管理工具,但不同的人有不同的使用习惯,因此会产生不同的问题。 还是以上一节提到的合并小插曲为例,在我们发现 master 分支的代码不是我们想要的代码后,我们选择了如下处理方式:

  1. 使用 master 分支和正确代码分支做对比
  2. 利用对比工具,将正确代码分支上,我们认为正确的代码选择到 master 分支上,形成一次新的提交

这确实是一种解决冲突的方式,在开发流程中的一些场景下会有不错的效果(比如两个同时开发的分支 feature1feature2 ,双方有一部分共用代码,但又不想合并开发,这时可以选择上述方式拣取需要的代码部分)。但在进入测试和发布流程后,这种处理方式就欠妥了。 这样的方式会导致以下问题:

  1. 拣取过来的部分代码形成了新的提交,这个提交与原有代码没有任何逻辑上的关系。当我们需要再次合并原提交所在分支时,这次拣取所遇到的冲突,下一次合并还会遇到
  2. 原代码分支是经过测试和回归的,但通过对比拣取的方式选取代码到新的分支上,这个过程会导致代码变更,意味着对比拣取后的代码,需要重新进行回归测试

如果代码提交记录是下面这样,测试会不会觉得头大崩溃? mess-commit-1 mess-commit-2 mess-commit-3

9个提交记录里面只有2个提交描述是有用信息,其它的要么是因为合并冲突而生成的无意义提交,要么是没有是指信息的提交描述。如果一个开发或者测试想根据提交信息追本溯源,面对这样的提交信息他该怎么办?

造成上图问题的主要原因有三个:

  1. 不当的Git使用方式,打乱了提交间的逻辑关系
  2. 提交信息填写过于随意,没有描述清楚代码变更的作用,也无法对应上日常工作的内容
  3. 不当的分支管理导致了不必要的冲突

那么如何解决这几个问题呢?

更优雅地合并

"Git’s philosophy is to be smart about determining when a merge resolution is unambiguous, but if there is a conflict, it does not try to be clever about automatically resolving it. "

Git是什么我想大家都清楚,常见功能也都很熟练,我就不过多介绍了。这一节我们主要集中在冲突这个概念上,因此我们不得不先复习一下Git的合并流程。 合并操作有多种策略,默认策略是resursive,递归三路合并:

  1. 从两个待合并的提交 head1、head2 上,寻找最近的公共祖先 ancestor
  2. 如果存在两个公共祖先,以两个公共祖先为基础执行步骤1 (递归)
  3. 如果 head2 的祖先是 head1 本身,执行 Fast-Forward 策略,直接改变 head1 指向 head2,合并完成
  4. 进行对比 head1 diff ancestor = diff1, head2 diff ancestor = diff2,根据 diff1 diff2 判断是否冲突
  5. 如果存在冲突,中断,等待解决冲突
  6. 形成新的提交(即自动形成的 " Merge head2 into head1 "),head1 指向新的提交

从这里,我们发现了前文提到的杂乱且无实际意义的提交记录是怎么来的。 如何避免这些记录呢?答案也在合并策略中。

第三点我们提到,如果待合并的目标提交的祖先就是当前提交,会触发 Fast-Forward 策略,只是改变当前 head 指向,不需要形成新的提交来记录这次合并行为。 要做到这一点,我们需要借助三个常用的命令, git rebasegit cherry-pickgit stash。什么情况下使用它们呢?

  • 执行 git merge 之前,先执行 git rebase,确保被合并的分支的祖先是当前分支 head
  • 不要直接执行 git pull ,在本地有和远端分支不同的提交记录的情况下,执行 git pull --rebase,保证本地提交的祖先节点是远端分支的提交
  • 不想合并分支,只需要其它分支的某一个提交时,使用 git cherry-pick,效果和使用对比工具拣取需要的代码差不多,但会获取整个提交
  • 如果本地同分支有变更尚未形成提交,但又需要同步远端代码,可以使用 git stash将变更暂存起来,执行完git pull之后,再执行git stash --pop释放暂存区变更

以上几个命令使用起来并不复杂,但却带来了以下好处:

  • 大幅减少合并过程中的冲突,如果我们使用gitlabMergeRequest进行合并,rebase操作几乎是必须的
  • 使提交间的逻辑关系更加清晰,有助于研发和测试追溯变更,同时也能减少后续提交发生冲突的概率

rebase在提交次数较多且差异较大的情况下,并不友好,可能出现多次解决冲突的问题。这种情况有两种解决方案:

  1. 在被中断时,使用 git rebase --skip 继续,到最后一次提交的时候一起解决冲突
  2. 直接使用git merge,一次性解决冲突,形成一次新的提交 上述情况在代码同步频率得当的情况下很少发生

更优雅地提交

前一节的两个命令解决了不规范合并的问题,还有一个问题是如何让提交信息更有价值? 如何定义一个提交信息是否有价值呢?通过经验,我们认为有价值的描述具备以下几点特性:

  • 阐述清楚变更的作用(必备)
  • 简单描述程序实现思路(推荐)
  • 提醒需要特别注意的地方(推荐)
  • 包含需求来源(强烈推荐)

首先是变更的目的或者说作用,我们推荐使用以下几个词来表述常见场景的变更:

  • feat: feature,特性,提交是为了完成某个需求或功能,强烈建议附加实现方案描述和需求来源链接,通常对应一个开发任务
  • fix: 修复,提交是为了修复某个已提测的问题,通常关联一个提测后的BUG单
  • hotfix: 热修复,指提交是为了修复某个线上问题,需要快速发布,通常关联一个线上BUG单
  • refactor: 重构,提交是进行代码结构或设计的重构,通常需要回归测试
  • chore: 杂项,提交完成一些日志、注释、文档编写,或是构建工具等变动,通常不需要回归测试
  • test: 测试,单元测试、集成测试编写,通常不需要回归测试

示例:

feat: 添加《Use Git Elegantly》博文

说明:
  1.从《Practice Agile with TBD》中拆分冗余博文内容
  2.增加git实现原理部分章节

链接: https://www.tapd.cn/123/prong/tasks/456

为了强化团队协作的一致性,在大家都达成共识后,我们可以基于githook对提交信息进行验证,甚至将提交与具体的任务进行关联等。 如何使用githook,有机会我们单独讨论一下,这里不做过多说明。

使用githook验证提交信息

To Be Done

更优雅地管理分支

更优雅的分支管理策略

© Cheng all right reserved,powered by GitbookModified At: 2021-12-10 14:52:54

results matching ""

    No results matching ""