看啥推荐读物
专栏名称: 科研者
万事万物,于愚者而同,于明者而异,于智者而统一!
今天看啥  ›  专栏  ›  科研者

Git分支规范(新):Git并行工作流程规范

科研者  · 简书  ·  · 2020-01-30 17:51
分支流转规范图

Git是一个十分优秀的版本控制工具,但仅仅依靠版本控制工具,还不能保证在多人协作的情况让项目的版本流转有条不紊,版本演进清晰整洁;还需要 Git工作流程规范 的约束;目前广泛使用的工作流程是 Git Flow ,但我个人认为它有很多不足之处,于是我就制定了一个新的规范,取名为 Git并行工作流程规范 ,为了清楚完整地描述 Git并行工作流程规范 ,我花了很多精力设计、撰写、绘图 和 制作动画,详见下文 和 相关文章!关于 Git并行工作流程规范 的设计思路请看 Git并行工作流程规范设计记录

目录

内容

1. GitFlow的问题

目前广泛使用的工作流程是 Git Flow ,如下图所示:

GitFlow分支工作流程图

它可以使得版本库的演进保持简洁,主干清晰,各个分支各司其职、井井有条;

但,我个人认为,它有许多不足之外,如下:

  1. 它需要维护两个相近的长期分支:Master 和 Develop;前者用于正式发布,后者用于日常开发;
  2. 每次完成 预发布分支 和 修复分支 时,都需要 分别合并到 Master 和 Develop 这两个长期分支中;
    GitFlow修复分支合并流程图
  3. 任务不独立;新任务的 功能分支 中 有可能包含 上个还未发布的 任务 的提交;
    比如:按照 Git Flow ,假设 功能A 已经开发完成,并且已经被合并到了 Develop 分支;
    这时又有 功能B 需要开发,则会从 Develop 新建 功能B 分支 用于开发 功能B ,则此时 功能B 分支 会包含 功能A 分支的所有提交;
  4. 不支持多任务并行开发;
    比如:按照 Git Flow ,假设 功能A 已经开发完成,并且已经走到了 预发布 阶段,则此时 功能A 分支已经被合并到了 Develop 分支;
    这时又有 功能B 需要开发,则会从 Develop 新建 功能B 分支 用于开发 功能B ,则此时 功能B 分支 会包含 功能A 分支的所有提交;
    如果这个时候发现 功能A 有问题 或者 因种种原因,需要暂停 功能A 的发布,但需要发布 功能B ,这时就不好办了,因为 功能B 分支中 已经包含了 功能A 的所有提交;

2. Git并行工作流程的设计宗旨

  • 支持 多任务 并行开发;
  • 避免同一分支需要分别向多个分支进行合并;
  • 多个任务 或 多个功能之间 独立、完整、互不干扰;
  • 分支各司其职,演进清晰整洁;
  • 分支的变更单元用提交节点表示;如:发布分支中的每一次提交都代表着一次发布,预发布分支的每一次提交都代表一次预发布,测试分支的每一次提交,都代表着一次转测;这样有助于各个分支演进清晰简洁;

3. 新概念的定义

为了方便 下方 或 今后 的表达,我需要定义以下概念:

临时性分支 终将会被合并到 长期分支中(除非弃用),如,开发完功能后,因转测,需要把 功能分支 合并到 测试分支 中,测试完后,测试分支 又会被合并到 预发布 分支中,最后,预发布分支 又会被合并到 发布分支 中;在这个过程中,功能分支 依次被合并到了 测试分支、预发布分支、发布分支;

像上面这样,分支 在被其它分支合并的过程 称为 分支 的 流转

分支A 被合并到 分支B ,也称为 分支A 流转到了 分支B

分支A 流转到了 分支B,分支B 又流转到了 分支C ,像这样的过程称为 分支A 的 连续流转 ,也称为 分支A 连续流转到了 分支C

连续流转过程中的每一个分支 都称为该连续流转的一个 流转环节 ,也称为该连续流转的一个 流转分支

连续流转的所有 流转环节 的有序组合 称为 流转链

流转链中 最初的 流转环节(流转分支) 又称为该流转链的 原始分支 初始环节

流转链中 最终的 流转环节(流转分支) 又称为该流转链的 终点分支 终点环节

流转链中 原始分支 和 终点分支 之间的 流转分支 称为 中间分支 中间环节

如果 分支A 是基于 分支B 创建的,即: 分支A 是从 分支B 创建的,则称 分支B 是 分支A 的 母分支

4. 分支的分类

4.1. 按照生命周期来分

  • 长期:伴随Git项目一直存在的分支;
  • 临时:针对特定任务或目的而建的,且 当完成任务或达到目的后需要被删除的分支;

4.2. 按照作用来分

  • 发布:为存放已发布的、正式的版本而建的分支;
  • 预发布:为即将正式发布的版本做试运行而建的分支;如果没有试运行阶段,则可以不设 预发布 分支;
  • 功能:为实现一个 或 多个 功能而开设的分支;
  • 修复:为修复一个 或 多个 问题 而开设的分支;
  • 协作:为实现多人协作而开设的分支;
  • 合并:为合并多个分支而建的分支;
  • 测试:为测试而建的分支;

5. 按照流转环节来分

  • 流转分支
    • 原始分支
    • 中间分支
    • 终点分支
  • 母分支

5.1. 包含关系

按照作用来划分的类别 与 本规范中按照 生命周期来划分的类别的包含关系如下:

  • 长期
    • 发布
  • 临时
    • 预发布
    • 功能
    • 修复
    • 协作
    • 合并
    • 测试

即:

  • 发布分支 属于 长期分支;
  • 预发布分支、功能分支、修复分支、协作分支、合并分支、测试分支 属于 临时分支;

6. 分支的流转规范

根据上述的《Git并行工作流程的设计宗旨》,我为各类分支的流转设计了如下规范:

  • 发布分支:该分支从 预发布分支 合并而来;如果没有 预发布分支,则从 测试分支 合并而来;在将分支(如:预发布分支 或 测试分支)合并到发布分支前,需要确保 该分支 已经包含了发布分支上的所有版本;如果没有包含发布分支上的所有版本,则取消本次的分支流转,并重新转测 或 从 原始分支 重新发起流转;
  • 预发布分支:该分支从 测试分支 合并而来;如果不需要,也可以不设 预发布 分支;
  • 测试分支:该分支从 被转测的分支 合并而来;如:功能、修复、协作、合并;在转测前,需要确保 被转测的分支 已经包含了发布分支上的所有版本;即:如果被转测的分支在转测前,发布分支上有新的版本发布,且这些新的版本并没有包含在 被转测的分支上,则需要先把发布分支上那些新的版本合并到被转测分支,然后才能转测,即:将被转测分支合并到测试分支上;
  • 合并分支:该分支是对 需要合并的多个分支 进行合并而来的分支;
  • 功能分支:基于 发布 分支创建;
  • 修复分支:在没有不需要与被修复的问题一起转测的代码的情况下,应该在 问题所在的 流转链 中的 原始分支 上直接修复问题,不必创建单独的修复分支(这是为了保证原始分支的完整性);如果 问题 所在的 流转链 中的 原始分支 不存在 或者 原始分支 中包含了不能与被修复的问题一起转测的代码,则应该 基于 问题所在的 流转链 中的 终点分支 创建 修复分支,然后在该 修复分支 上修修复问题(这是为了保证 多个任务或多个功能 之间 互不干扰);当问题被修复完问题后,再将该 修复分支 合并到 其对应的专门用于测试该修复问题的测试分支中,不应该把 修复分支 合并到 该修复分支的 母分支 或者 母分支 对应的 测试分支 中;即:每个修复分支 都有其对应的、专门的测试分支 (这是为了保证 测试分支 和 对应的 被转测分支 的代码的一致性,多个任务 或 多个功能之间 独立、互不干扰);
    比如:
    如果,被修复的问题是由 功能A 导致的,且 功能A分支 上不包含不能与被修复的问题一起转测的代码,则应该直接在 功能A分支 中修复问题,不用创建专门的 修复分支;
    如果 功能A 分支 已经被删除了 或者 包含不能和 被修复的问题 一块转测的代码,则应该基于 被修复问题所在的 流转链 的 终点分支 创建 专门用于修复该问题的 修复分支;问题被修复完后,再将该修复分支 合并到 专门用于测试该问题的 测试分支 中;
  • 临时分支:被合并到发布分支(即正式发布之后)才可以被删除;建义正式发布运行一段时间之后再删除;如果不必要,也不建义永久留着,以便仓库保持整洁;
  • 每个 预发布分支 都有其唯一对应的 测试分支,每个 测试分支 都有其唯一对应的 被转测分支;即:每个 被转测分支 都有其 单独对应的 测试分支,每个 测试分支 都有其 单独对应的 预发布分支;(这是为了保证对应的 预发布分支、测试分支、被转测分支 的代码的一致性,多个任务 或 多个功能之间 独立、互不干扰)

注意: 本规范(分支的流转规范)中所述的合并操作 可以是 一般的 分支 合并 操作,也可以是 Pull requests ,这个取决仓库的管理策略;所以,本规范也适用于那些 用多个仓库管理同一个项目的 Git仓库管理策略;

分支流转规范图

高清图详见: 分支流转规范图-高清 分支流转规范图-高清透明
该图的设计思路详见: Git并行工作流程规范设计记录

7. 分支合并规范

为了使分支各司其职,演进清晰简洁,分支的变更单元可用提交节点表示,我们在合并分支时需要按照一定的规范来操作,具体如下:

  • 对于 分支的流转规范 (见上文 6 )中的分支合并操作必须使用 非快进式合并 (见下文 禁止快进式合并 );
  • 长期分支应以 孤儿分支 (见下文 创建空分支独立分支孤儿分支 )的方式被创建;
  • 创建临时分支时,可根据自己的喜欢选择是以 孤儿分支 的方式创建,还是不以孤儿分支的方式创建;
  • 拉取(pull)分支时,应以变基的方式(加上 --rebase|-r 选项)拉取(pull)(见下文 用变基的方式pull );

8. 提交规范

8.1. 提交操作规范

为了保持分支提交历史的清晰、独立,在提交更改时,我们应做到:

  • 每一个提交都应该是一个完整、独立的变更单元;
  • 撰写符合 提交说明规范 (见下文 8.2 )的提交说明信息;
  • 对于修复错别字、添加遗漏的更改等等之类的提交应与对应的提交合并,不应为其创建单独的提交;

多个提交合并成一个的各种方法请看 Git中合并多个提交的各种方法

8.2. 提交说明规范

Git 每次提交代码,都必须要写 提交说明; Git 对 提交说明 的格式是没有限制的,你想怎么写就怎么写,如下:

杂乱的提交说明

但是,类似这种没有格式的提交说明有以下缺点:

  • 不能很快分辨出提交的代码是增加了新功能、还是修复了bug、还只是更新了文档等等;
  • 不能有效地过滤某一类提交,比如:只想查看修复bug类的提交;
  • 不能根据需要过滤并导出提交信息,作为变更日志:比如,应用的升级的新功能说明、问题修复说明等等;

为了 方便 查看、过滤 提交说明,我需要将提交说明格式化、规范化;目前,有多种 提交说明 的写法规范。但我推荐 Angular提交说明规范 ,这是目前使用最广的写法,比较合理和系统化,并且有配套的工具。

关于 Angular提交说明规范 的详细文章请见:

下面是我对 Angular提交说明规范 一个汇总描述;

8.2.1. Angular提交说明规范

Angular提交说明的格式如下

<Type>[Scope]:<Subject>
<空一行>
[Body]
<空一行>
<Footer>
  • 提交说明包括三个部分:Header(第一行)、Body(可选) 和 Footer(可选),用空行分隔;其中 Body、Footer 都是可选的,可以省略;

  • 任何一行都不得超过72、100个字符;这是为了避免自动换行影响美观

  • Header:只占第一行,包括三个字段:Type(必需)、Scope(可选)和 Subject(必需);

    • Type:必需;用于说明提交的类别,只允许使用下面7个标识:

      • feat:新功能(feature)
      • fix:修补bug
      • docs:文档(documentation)
      • style: 格式(不影响代码运行的变动)
      • refactor:重构(即不是新增功能,也不是修改bug的代码变动)
      • test:增加测试
      • chore:构建过程或辅助工具的变动

      如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。其他情况(docs、chore、style、refactor、test)由你决定,要不要放入 Change log,建议是不要。
      提示: 为了醒目,也可以为每个 Type 分别指定一个 Emoji 表情,将其放在 Type 前面 或 后面;

    • Scope:可选;用于说明提交的影响范围,比如数据层、控制层、视图层等等,视项目不同而不同。

    • Subject:提交目的 的简短描述;要求如下:

      • 不超过50个字符。
      • 以动词开头,使用第一人称现在时,比如 change,而不是 changed 或 changes
      • 第一个字母小写
      • 结尾不加句号 .
  • Body:对本次提交的详细描述,

    • 可以分成多行。
    • 使用第一人称现在时,比如使用change而不是changed或changes。
    • 应该说明代码变动的动机,以及与以前行为的对比。
  • Footer:Footer 部分只用于两种情况。

    1. 不兼容变动:如果当前代码与上一个版本不兼容,则 Footer 部分以 BREAKING CHANGE 开头,后面是对变动的描述、以及变动理由和迁移方法。
    2. 关闭 Issue:如果当前 提交 针对某个 issue 的,那么可以在 Footer 部分用 Closes #234 关闭这个 issue ;也可以用 Closes #123, #245, #992 一次关闭多个 issue ;
  • 特殊情况: 如果当前 提交 用于撤销以前的 提交,则:

    • Header 必须以 revert: 开头,后面跟着被撤销的提交的 Header。
    • Body 部分的格式是固定的,必须写成 This reverts commit <hash>. ,其中的 hash 是被撤销的 提交 的 SHA 标识符。
      如果当前提交 与 被撤销的 提交,在同一个发布(release)里面,那么它们都不会出现在 Change log 里面。如果两者在不同的发布,那么当前 commit,会出现在 Change log 的 Reverts 小标题下面。

9. 相关技巧

规范有了,但在实施规范的过程中会遇到各种技术难题,为了完整性,我又研究并提供了实施规范过种中各种问题的解决方案,并把这些方案撰写成文 Git技巧和问题解决方案 ,下面仅是本文涉及到的相关技巧:

9.1. 禁止快进式合并

git在执行合并操作时(无论是通过 merge 命令,还是通过 pull 命令),比如 将 分支B 合并进 分支A 中,如果 分支A 完全包含在 分支B 的历史中(如下图),


快进合并前

那么,git 默认会以快进的方式进行合并,合并后的效果如下:


快进式合并

非快进式合并的效果如下:


非快进式合并

如果想禁用快进式合并,可以给命令传递 --no-ff 选项, ff 是 快进的英文 fast farward 缩写;如下:

merge命令:

git merge  --no-ff  <分支B>

pull命令:

git pull --no-ff

merge 和 pull 命令中 与快进式合并相关的选项如下:

  • --ff : 当可以快进合并时,使用快进合并的方式进行合并。这是默认行为。
  • --no-ff : 即使可以快进合并,不会使用快进式合并,而是也创建一个合并提交。这是合并注释(可能有符号)标记时的默认行为。
  • --ff-only : 如果可以快进式合并,则会进行快进式合并;否则,取消合并操作;

9.2. 创建空分支(独立分支、孤儿分支)

我们可以通过以下命令创建新分支:

  • git branch <新分支名>
  • git checkout -b <新分支名>

这种方式创建的新分支 都是 基于当前的 HEAD 来创建分支的,新的分支会和当前 HEAD 拥有共同的 提交历史;
例如,假设当前 HEAD 在 分支A 上,如下图所示,


创建独立分支前

通过 git branch 分支B git checkout -b 分支B 来创建 分支B 后,分支图如下所示:

创建非独立分支

新建的 分支B 和 分支A 会有相同提交历史;

如果我们想创建一个没有任何历史的分支,我们可以用 带 --orphan 选项的 checkout 命令,语法如下:

git checkout --orphan <新分支名> [开始点]

--orphan 选项指定创建一个 孤儿分支 ,即:独立的分支、没有任何提交历史的分支;并且会切换到这个新的分支;
此时,你通过 branch 命令列出的分支列表里是没有这个分支的,因为该分支里没有任何提交,分支没有可被引用的提交对象;
并且,此时,新分支的 暂存区 中存放的是 开始点 处目录树中的所有文件,如果没有指定 开始点 参数,则默认会把 当前的 HEAD 作为开始点;
如果不想要暂存区的任何东西,可以执行 git rm -rf . 命令清空暂存区;
如果想将 开始点 处的整个目录树作为新分支的第一个版本进行提交,直接执行 commit 命令 git commit -m "提交信息" 即可;

如:当前在HEAD分支A,执行如下命令:

git checkout --orphan 分支B
git commit -m "提交信息"

分支图如下所示:


创建独立分支

合并独立分支的方法请看 合并不相关的分支(没有共同历史的分支)

9.3. 合并不相关的分支(没有共同历史的分支)

Git默认的合并操作只会对有共同提交历史的分支进行合并;


有共同历史的分支

不过,对没有共同提交历史的分支进行合并的情况也是存在的,比如:

  • 合并孤儿分支;
  • 两个仓库,但文件内容相似,甚至就是同一个项目,需要将这两个仓库中的分支进行合并
不相关的分支

若想对没有共同历史的分支进行合并,只需给 merge pull 命令(pull命令也会有合并的操作)添加 --allow-unrelated-histories 选项,语法如下:
merge语法:

git merge  --allow-unrelated-histories  <分支名>

pull语法:

git pull  --allow-unrelated-histories
不相关分支的合并

9.4. 只合分支并保持历史的线性

假设有 分支A 和 分支B ,当前在 分支A,如下图:


squash合并前

当我们进行合并分支时,通常会将指定分支的变更合并到当前分支中,并产生一个提交,该提交会有两个父提交的引用;如下图:


非squash合并

如果我们想合并其它分支的变更,但又想保持当前分支的线性,使分支的提交历史上没有分支交汇的情况,即:每一个提交对象都有一个父提交的对象,而非多个父提交对象;那该怎么操作呢?

我们可以给 merge pull 命令(pull命令也会有合并的操作)添加 --squash 选项,语法如下:
merge语法:

git merge  --squash  <分支名>

pull语法:

git pull  --squash

注意: 执行完 --squash 的合并操作后,git默认不会生成合并的变更提交,只是把合并的变更放在暂存区中,并附有默认的提交信息,你需要再手动进行提交下;

示例:

git merge  --squash  分支B
git commit
squash合并

9.5. 用变基的方式pull

语法:

git pull -r [<远程仓库> [[远程分支][:<本地分支>]]
git pull --rebase [<远程仓库> [[远程分支][:<本地分支>]]

说明:
远程仓库 获取指定的 远程分支 的更新到相应的远程跟踪分支,并以变基 rebase 的方式合并到 本地分支 ,并将本地分支中新的提交拼接到上游分支的后面;
如果 本地分支 不存在,则会创建该本地分支;

示例:

git pull -r

10. 相关文章




原文地址:访问原文地址
快照地址: 访问文章快照