Gitlab Flow 工作流程简介

一文搞懂 Gitlab Flow 工作流程

本文由 墨然 发布于 2021-05-09

Gitlab Flow
Gitlab Flow

Git 允许广泛的分支策略和工作流。因此,许多组织使用的工作流都是相当复杂且没有清晰定义的,也没有很好的与问题跟踪系统(issue tracking systems)集成在一起。所以,我们提出了 GitLab flow,让它作为一组清晰定义的最佳实践。它结合了feature-driven development(功能驱动开发)和带有问题跟踪的 feature branches(功能分支)。

从其他版本控制系统转到 Git 的组织常常会发现,发展出一套高效的工作流是很困难的。这篇文章描述了 GitLab flow,它集成了一套带有问题跟踪系统的 Git 工作流。它提供了一套可以透明高效地使用 Git 的方法。

Gitlab Flow 4 个阶段

在迁移到 Git 时,你必须习惯与同事分享提交(commit)需要三个步骤的事实。大多数版本控制系统只有一个步骤:把工作副本提交到共享服务器上。使用 Git 时,你要先把工作副本中的文件添加到暂存区,然后再把它们提交到你的本地仓库之中。第三步是推送到一个共享的远程仓库。在习惯了这三个步骤之后,下一个挑战是分支模型。

Gitlab Flow 令人混乱的分支

由于许多刚接触 Git 的组织对如何使用它没有约定,他们的仓库很快就会变得混乱不堪。最大的问题是出现了许多长期运行的分支,它们都包含了部分变化。人们很难弄清楚哪个分支有最新的代码,或者哪个分支应该部署到生产中。通常,这个问题的应对之策是采取一种标准化的模式,如Git flow和GitHub flow。我们认为仍有改进余地。在本文档中,我们描述了一套我们称为 GitLab flow 的实践。

有关这一切如何在 GitLab 中运作的视频介绍,见 GitLab Flow。

Git flow 和它的问题

git dash flow

Git flow 是使用 Git 分支最早的提议之一,并且它受到了广泛的关注。它建议使用一个master分支和一个分离的develop分支,以及用于功能、发布和热修复的辅助分支。开发工作在develop分支上进行,然后转移到发布分支,最后合并到master分支。

Git flow 是一个明确定义的标准,但它的复杂性引入了两个问题。其一是开发人员必须使用develop分支而不是master分支。master分支是用于存放那些发布到生产环境的代码的。按照惯例,你将你的默认分支称为master,你的大多数分支源自它并且合并回它。而且因为大多数工具都会自动地使用master作为默认分支,所以不得不切换到另一个分支是很烦人的。

Git flow 的第二个问题是由 hotfix(热修复)和 release(发布)分支所带来的复杂性。这些分支对某些组织来说可能是个好主意,但对绝大多数组织来说都太过了。如今,大多数组织都实行持续交付,这意味着你的默认分支可以被部署。持续交付消除了对 hotfix 和 release 分支的需求,包括引入它们所带来的所有仪式。这种仪式的一个例子就是 release 分支的回归合并。尽管确实存在专门的工具来解决这些问题,但它们需要文档并且增加了复杂性。经常有开发者犯错误,比如只把改动合并到master分支,却没有合并到 develop 分支。造成这些错误的原因是 Git flow 对大多数使用情况来说都太复杂了。比如,许多项目仅仅只需要发布,但却不需要做热修复。

Github flow:一种更简单的替代方案

Github Flow

作为对 Git flow 的应对,GitHub 创造了一种更简单的替代方案。GitHub flow仅仅拥有功能分支和master分支。这种工作流很干净直观,许多组织采用它并取得了巨大的成功。Atlassian 推荐了一种类似的策略,尽管它们会 rebase(变基)功能分支。把所有东西合并到master分支并且频繁地部署意味着你将未发布的代码量降到了最低。这种方案符合精简原则和持续交付的最佳实践。但是,这种工作流仍然留下了许多关于部署、环境、发布,和 issue(问题)集成的未解之谜。在 GitLab flow 中,我们给这些谜题提供了额外的指南。

GitLab flow 中的生产分支

Gitlab Flow 中的生产分支

GitHub flow 假定在你合入功能分支的任何时候都可以部署到生产环境。然而在某些情况下这将是一个问题,比如 SaaS 应用。许多情况下这都是不可能的,比如:

  • 你不能控制发布的时机。例如,一个 iOS 应用只有在它通过 App Store 的验证时才能发布。
  • 你有部署窗口。例如,工作日的上午 10 点到下午 4 点之间运维团队是满效运作的,但你也可能在其他时间合并代码。

在这些例子中,你可以创建一个生产分支来承载这些已经部署的代码。你可以通过将master分支合并到生产分支来部署一个新的版本。如果你想知道生产环境中的代码是什么样的,你可以检出生产分支来查看。部署的时间根据版本控制系统中的合并提交(merge commit)是大致可知的。如果你自动部署你的生产分支,这个时间是相当准确的。如果你需要一个更准确的时间,你可以让你的部署脚本在每次部署时创建一个标签。这个流程可以避免 Git flow 中出现的发布、标记和合并的开销。

GitLab flow 中的环境分支

Gitlab Flow 中的环境分支

拥有一个可以自动更新到 master 分支的环境可能是个好主意。只是,在这种情况下,这个环境的名称可能与分支的名称不同。假设你有一个阶段环境(staging environment),一个预生产环境(pre-production environment),以及一个生产环境(production environment)。在这种情况下,将 master 分支部署到阶段环境。要部署到预生产环境,请创建一个从 master 分支到预生产分支的合并请求(merge request)。通过将预生产分支合并到生产分支来实现上线。这种工作流(在其中提交只流向下游)可以确保所有东西在所有环境中都得到了测试。如果你需要 cherry-pick(挑选)一个带有热修复的提交,通常是在功能分支上开发,然后用合并请求将其合并到 master。在这种情况下,先不要删除功能分支。如果master通过了自动测试,你再把这个功能分支合并到其他分支。如果这因为需要更多人工测试而无法做到,你可以把来自功能分支的合并请求发送给下游分支。

GitLab flow 中的发布分支

Gitlab Flow 中的发布分支

只有当你需要向外界发布软件时,你才需要使用发布分支。在这种情况下,每个分支都包含一个次要版本(minor version),如 2-3-stable 或 2-4-stable。以 master 为起点创建稳定分支,并尽可能晚地创建。通过这样做,你可以最大限度地减少不得不将错误修复应用到多个分支上的耗时。在公布一个发布分支后,只向该分支添加严重错误修复。如果可能的话,先把这些错误修复合并到master,然后再把它们 cherry-pick 到发布分支。如果一开始就合并到发布分支,你可能会忘记把它们 cherry-pick 到 master 上,然后你就会在随后的发布中遇到同样的错误。先合并到 master 分支,然后再 cherry-pick 到发布分支叫做 “上游优先 “政策,Google 和 Red Hat 也是这样实践的。每当你在发布分支中加入一个错误修复,就通过设置一个新的标签来增大补丁版本(patch version)(以符合语义化版本)。有些项目也有一个稳定分支,它指向与最新发布的分支相同的提交。在这种流程下通常都不会有一个生产分支(或 Git flow 的master分支)。

GitLab flow 中的合并/拉取请求

Gitlab Flow 中的合并/拉取请求

合并或拉取请求是在 Git 管理程序中创建的。它们要求一个被指定的人来合并两个分支。GitHub 和 Bitbucket 等工具选择“拉取请求( pull request )“这一名称,因为第一个手动操作是拉取功能分支。GitLab 等工具选择了“合并请求( merge request )“这个名字,因为最后的行动是合并功能分支。本文将它们称为合并请求。

如果你在一个功能分支上工作超过了几个小时,然后想要将中间产物分享给团队的剩余成员。为了做到这一点,创建一个合并请求,不要把它指定给任何人。但是,可以在描述或评论中提及别人,例如,“/cc @mark @susan“。这表明虽然该合并请求还没有准备好被合并,但欢迎提供反馈。你的团队成员可以对合并请求进行总的评论,或者使用行评论( line comment )对特定的行进行评论。合并请求被用作代码审查工具,而不必使用单独的代码审查工具。如果审查发现了缺陷,任何人都可以提交并推送一个修复。通常情况下,做这个事的人是合并请求的创建者。当新的提交被推送到该分支时,合并请求中的差异会自动更新。

当你准备好让你的功能分支被合并时,把合并请求指定给最了解你正在修改的代码库的人。此外,提及你希望从他们那里得到反馈的其他人员。在被指定的人对结果感到满意后,他们就可以合并该分支了。如果被指定的人觉得不太好,他们可以要求更多的修改,或者关闭合并请求而不进行合并。

在 GitLab 中,通常会保护长期存在的分支,比如master分支,所以大多数开发者无法修改它们。因此,如果你想合并到一个受保护的分支,请将你的合并请求分配给有维护者权限的人。

在合并一个功能分支后,应该从源码控制软件中删除它。在 GitLab 中,你可以在合并时做到这一点。删除已完成的分支可以确保分支列表中只显示正在进行的工作。这也确保了如果有人重新打开该 issue,他们可以使用相同的分支名称而不会造成问题。

当你重新打开一个 issue 时,你应该创建一个新的合并请求。

Gitlab Flow 接受合并请求

GitLab flow 中的问题跟踪

Gitlab Flow 合并请求

GitLab flow 是一种使代码和 issue tracker(问题跟踪器)之间的关系更加透明的方法。

任何对代码的重大修改都应该以一个描述目标的 issue 开始。为每一次代码修改提供一个理由,有助于告知团队的其他成员,并使一个功能分支的粒度尽可能的小。在 GitLab 中,每一次对代码库的修改都始于问题跟踪系统中的一个 issue 。如果还没有 issue,并且该变更需要进行一个小时以上的工作时,那就创建一个。在许多组织中,提出 issue 是开发流程的一部分,因为它们被用于冲刺计划之中。issue 的标题应该描述系统的期望状态。例如,issue 标题“作为一个管理员,我想在不报错的情况下删除用户“要比“管理员不能删除用户“好。

当你准备开始编码时,为该 issue 从master分支上创建一个分支。这个分支是任何与此变化有关的工作的地方。

一个分支的名称可能听令于组织标准。

当你完成工作或想讨论代码时,打开一个合并请求。合并请求是在线讨论修改和审查代码的地方。

如果你打开了合并请求,但没有把它分配给任何人,它就是一个合并请求草案。这些草案是用来讨论提议的实现的,但它们还没有准备好纳入master分支。在合并请求的标题中以[Draft]、Draft:或(Draft)开头,以防止它在准备好之前被合并。

当你认为代码已经准备好了,就把合并请求分配给一个评审员( reviewer )。当评审员认为代码已经为纳入master分支做好准备时,他们就可以合并这些变更。当他们按下合并按钮时,GitLab 就会合并代码并创建一个合并提交,使这一事件在之后可见。即使该分支在合并时可以不产生合并提交,合并请求也总是会创建一个合并提交。这种合并策略在 Git 中被称为“no fast-forward(非快进模式)“。合并后,将删除该功能分支,因为它不再被需要了。在 GitLab 中,这种删除是合并时的一个选项。

假设一个分支被合并了,但出现了问题,issue 被重新打开。在这种情况下,重新使用相同的分支名是没有问题的,因为第一个分支在合并时已经被删除了。在任何时候,每个 issue 最多拥有一个分支。一个功能分支解决多个 issue 却是可能的。

链接和关闭来自合并请求中的 issue

Gitlab Flow 关闭来自合并请求中的 issue

通过在提交信息或合并请求的描述中提及 issue 来链接它们,例如,“Fixes #16 “或 “Duck typing 是首选。见#12“。然后,GitLab 会创建指向所提及 issue 的链接,并在 issue 中创建链接回合并请求的注释。

要自动关闭被链接的 issue,可以用关键字“fixes“或“closes“来提及它们,例如,“fixes #14“或“closes #67“。当代码被合并到默认分支时,GitLab 就会关闭这些 issue 。

如果你有一个跨越多个仓库的 issue,为每个仓库创建一个 issue,并将所有 issue 链接到一个父 issue 。

使用 rebase 来压缩提交

Gitlab Flow rebase

Git 中,你可以使用交互式 rebase(rebase -i)来将多个提交压缩成一个,或者重新排列它们。这个功能可以帮助你将几个小提交替换为单一提交,或是帮你让多个提交的顺序更合理。

然而,如果你在同一分支有其他活跃的贡献者,你应该避免对你已经推送到远程服务器的提交进行 rebase 。因为 rebase 会为你所有的变更创建新的提交,这会造成困惑,因为同一个变更会有多个标识符。这将导致任何在同一分支上工作的人出现合并错误,因为他们的历史与你的历史不匹配。这对作者或其他贡献者来说真的很麻烦。另外,如果有人已经审查过你的代码,rebase 会让人难以分辨自上次审查后发生了什么变化。

你永远不应该 rebase 别人的提交,除非你们另有协定。这不仅会重写历史,而且还会丢失作者信息。rebase 会使其他作者无法被属性化并且无法共享部分git blame。

如果一个合并涉及到许多提交,它可能看起来更难撤销。为了解决这个问题,你可以考虑使用 GitLab 的 Squash-and-Merge 功能在合并之前将所有的变更压缩到一个提交之中。幸运的是,你可以撤消一个合并及其所有的提交。这样做的方法是 revert(恢复)合并后的提交。保留这种 revert 合并的能力是一个在手动合并时总是使用 no fast-forward(--no-ff)策略的很好的理由。

如果你 revert 了一个合并提交,然后又改变了主意,那么 revert 那个 revert 提交就可以撤销合并了。否则 Git 不允许你再次合并代码。

减少功能分支中的合并提交

Gitlab Flow 合并提交

大量的合并提交会使你的仓库历史变得混乱。因此,你应该在功能分支上尽量避免合并提交。通常,人们避免合并提交的方法是,在提交到master分支后使用 rebase 来重新排序他们的提交。使用 rebase 可以避免将 master 分支合并到功能分支时产生合并提交,而且可以创造一条整洁的的线性历史。然而,正如有关 rebase 部分所讨论的那样,你应该避免 rebase 与他人共享的功能分支中的提交。

rebase 可能会产生更多的工作,因为每次 rebase 时你都可能需要解决同样的冲突。有时你可以重复使用已记录的解决方案(rerere),但 merge(合并指令)更好,因为你只需要解决一次冲突。Atlassian 在他们的博客上对 merge 和 rebase 之间的权衡有更透彻的解释。

防止产生许多合并提交的一个好方法是,不要经常将 master 分支合并到功能分支。合入 master 有三个理由:利用新代码、解决合并冲突、更新长期运行的分支。

如果你需要使用一些在你创建功能分支后在 master 中引入的代码,通常只需 cherry-pick 一个提交即可解决。

如果你的功能分支有合并冲突,创建一个合并提交是解决这个问题的标准方法。

有时你可以使用.gitattributes来减少合并冲突。例如,你可以将你的更新日志( changelog )文件设置为使用联合合并驱动,这样多个新实体就不会相互冲突。

创建合并提交的最后一个理由是让长期运行的功能分支与项目的最新状态保持同步。解决此类合并提交的办法是让功能分支短命。大多数功能分支的工作时间应少于一天。如果你的功能分支经常要花费一天以上的时间,那就尝试把你的功能拆分成更小的工作单元。

如果你确实需要让一个功能分支存在超过一天的时间,有一些策略可以让它保持最新状态。一种选项是使用持续集成( CI ),让它在一天的开始之时将master合并进来。另一个选项是只从定义好的时间点进行合入,比如说,一次打标签的发布的时候。你也可以使用feature toggle(功能开关)来隐藏不完整的功能,这样你仍然可以每天合回 master。

不要把自动化分支测试和持续集成混为一谈。Martin Fowler 在一篇关于功能分支的文章中做了这样的区分。

[人们]说他们在进行 CI,因为他们会在每个分支上的每次提交时都运行构建(也许是使用 CI 服务器)。这是持续构建,是件好事,但这儿并没有集成,所以这不是 CI 。

总之,你应该努力避免合并提交,但不是消除它们。你的代码库应该干净,但你的历史需要反映真实发生的情况。开发软件是以小的、混乱的步骤进行的,让你的历史表现这一点是完全可以的。你可以使用工具来查看提交的网络图,来理解创建你代码的混乱历史。如果你 rebase 了代码,历史就不正确了,并且工具也没办法补救,因为它们不能处理变化了的提交标识符( commit identifiers )。

频繁地提交和推送

另一个让你的开发工作更轻松的方法是经常提交。每当你有了一套可用的测试和代码,你就应该提交一次。将工作分割成单独的提交,可以为以后查看你代码的开发人员提供上下文。较小的提交可以让人清楚地明白一个功能是如何发展的。它们可以帮助你回滚到一个特定的时间点,或是仅仅恢复一个代码变更而不是好几个不相关的变更。

经常提交也有助于你分享你的工作,这很重要,因为这样大家都知道你在做什么。你应该经常推送你的功能分支,即使它还没有做好被审查的准备。通过分享你功能分支或合并请求中的工作,你可以避免团队成员重复工作。在你的工作完成之前分享你的工作,也可以让大家讨论和反馈这些变更。这种反馈可以帮助在审查之前改进代码。

如何写一个好的提交消息

Gitlab Flow 好的提交和不好的提交对比

提交消息应该反映你的意图,而不仅仅是提交的内容。你可以在提交中看到改动,所以提交消息应该解释你为什么做这些改动。一个好的提交消息的例子是:“合并模板以减少用户视图中的重复代码”。“change(改变)”、“improve(改进)”、“fix(修复)”、“refactor(重构)”这些词并不能为提交消息增加多少信息。例如,“改进 XML 生成”可以更好地写成 “在 XML 生成中正确转义特殊字符”。如果想了解更多关于提交消息的格式信息,请看Tim Pope 的这篇优秀博文。

为了给提交消息添加更多的上下文,可以考虑添加关于变更源头的信息。例如,GitLab issue 的 URL,或是 Jira 的 issue 编号,为需要变更的详尽上下文的那些用户囊括更多信息。

例如:

1
2
Properly escape special characters in XML generation.
Issue: gitlab.com/gitlab-org/gitlab/-/issues/1

合并前测试

Gitlab Flow 合并请求前测试

在旧的工作流中,持续集成( CI )服务器通常只在master分支上运行测试。开发人员必须确保他们的代码不会破坏master分支。当使用 GitLab flow 时,开发人员从这个master分支创建他们的分支,所以它永远不会被破坏是至关重要的。因此,每个合并请求在被接受之前都必须经过测试。像 Travis CI 和 GitLab CI/CD 这样的 CI 软件会在合并请求中直接显示构建结果,以简化这一过程。

测试合并请求有一个缺点:CI 服务器只测试功能分支本身,而不会测试合并后的结果。理想情况下,服务器也可以在每次修改后测试master分支。但是,在每次提交到master的时候重新测试,计算成本很高,而且也意味着你要更频繁地等待测试结果。因为功能分支应该是短命的,所以只测试它的风险是可以接受的。如果master中的新提交引起了与功能分支的合并冲突,可以将master合回功能分支,让 CI 服务器重新运行测试。如前所述,如果你的功能分支经常持续几天以上,那么你应该把你的 issue 变得更小。

使用功能分支

Gitlab Flow Pull

当创建功能分支时,总是基于最新的 master 分支。如果在你开始之前,你知道你的工作依赖于另一个分支,那也可以基于那个分支。如果在开始之后你需要合入另一个分支,请在合并提交中解释原因。如果你还没有将你的提交推送到一个共享位置,你也可以 rebase master 或者其他功能分支来纳入变更。如果你的代码可以正常运作,并且即使不合并上游也可以干净合并的话,那就别再次合并上游。只在有需要的时候才合并,这样可以避免在功能分支中产生合并提交(这些提交最终会在 master 历史中留下痕迹)。

全文完。


更多精彩文章