Git是一个十分优秀的版本控制工具,但仅仅依靠版本控制工具,还不能保证在多人协作的情况让项目的版本流转有条不紊,版本演进清晰整洁;还需要 Git工作流程规范 的约束;目前广泛使用的工作流程是 Git Flow,但我个人认为它有很多不足之处,于是我就制定了一个新的规范,取名为 Git并行工作流程规范 ,为了清楚完整地描述 Git并行工作流程规范 ,我花了很多精力设计、撰写、绘图 和 制作动画,详见下文 和 相关文章!关于 Git并行工作流程规范 的设计思路请看 Git并行工作流程规范设计记录。
目录
- 1. GitFlow的问题
- 2. Git并行工作流程的设计宗旨
- 3. 新概念的定义
- 4. 分支的分类
- 4.1. 按照生命周期来分
- 4.2. 按照作用来分
- 5. 按照流转环节来分
- 5.1. 包含关系
- 6. 分支的流转规范
- 7. 分支合并规范
- 8. 提交规范
- 8.1. 提交操作规范
- 8.2. 提交说明规范
- 8.2.1. Angular提交说明规范
- 9. 相关技巧
- 9.1. 禁止快进式合并
- 9.2. 创建空分支(独立分支、孤儿分支)
- 9.3. 合并不相关的分支(没有共同历史的分支)
- 9.4. 只合分支并保持历史的线性
- 9.5. 用变基的方式pull
- 10. 相关文章
内容
1. GitFlow的问题
目前广泛使用的工作流程是 Git Flow,如下图所示:
它可以使得版本库的演进保持简洁,主干清晰,各个分支各司其职、井井有条;
但,我个人认为,它有许多不足之外,如下:
- 它需要维护两个相近的长期分支:Master 和 Develop;前者用于正式发布,后者用于日常开发;
- 每次完成 预发布分支 和 修复分支 时,都需要 分别合并到 Master 和 Develop 这两个长期分支中;
- 任务不独立;新任务的 功能分支 中 有可能包含 上个还未发布的 任务 的提交;
比如:按照 Git Flow,假设 功能A 已经开发完成,并且已经被合并到了 Develop 分支;
这时又有 功能B 需要开发,则会从 Develop 新建 功能B 分支 用于开发 功能B ,则此时 功能B 分支 会包含 功能A 分支的所有提交; - 不支持多任务并行开发;
比如:按照 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提交说明规范
- Commit message 和 Change log 编写指南
下面是我对 Angular提交说明规范 一个汇总描述;
8.2.1. Angular提交说明规范
Angular提交说明的格式如下
[Scope]:
<空一行>
[Body]
<空一行>
提交说明包括三个部分: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 部分只用于两种情况。
- 不兼容变动:如果当前代码与上一个版本不兼容,则 Footer 部分以
BREAKING CHANGE
开头,后面是对变动的描述、以及变动理由和迁移方法。 - 关闭 Issue:如果当前 提交 针对某个 issue 的,那么可以在 Footer 部分用
Closes #234
关闭这个 issue ;也可以用Closes #123, #245, #992
一次关闭多个 issue ;
- 不兼容变动:如果当前代码与上一个版本不兼容,则 Footer 部分以
-
特殊情况: 如果当前 提交 用于撤销以前的 提交,则:
- Header 必须以
revert:
开头,后面跟着被撤销的提交的 Header。 - Body 部分的格式是固定的,必须写成
This reverts commit
,其中的. hash
是被撤销的 提交 的 SHA 标识符。
如果当前提交 与 被撤销的 提交,在同一个发布(release)里面,那么它们都不会出现在 Change log 里面。如果两者在不同的发布,那么当前 commit,会出现在 Change log 的Reverts
小标题下面。
- Header 必须以
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,如下图:
当我们进行合并分支时,通常会将指定分支的变更合并到当前分支中,并产生一个提交,该提交会有两个父提交的引用;如下图:
如果我们想合并其它分支的变更,但又想保持当前分支的线性,使分支的提交历史上没有分支交汇的情况,即:每一个提交对象都有一个父提交的对象,而非多个父提交对象;那该怎么操作呢?
我们可以给 merge
或 pull
命令(pull命令也会有合并的操作)添加 --squash
选项,语法如下:
merge语法:
git merge --squash <分支名>
pull语法:
git pull --squash
注意: 执行完 --squash
的合并操作后,git默认不会生成合并的变更提交,只是把合并的变更放在暂存区中,并附有默认的提交信息,你需要再手动进行提交下;
示例:
git merge --squash 分支B
git commit
9.5. 用变基的方式pull
语法:
git pull -r [<远程仓库> [[远程分支][:<本地分支>]]
git pull --rebase [<远程仓库> [[远程分支][:<本地分支>]]
说明:
从 远程仓库
获取指定的 远程分支
的更新到相应的远程跟踪分支,并以变基 rebase 的方式合并到 本地分支
,并将本地分支中新的提交拼接到上游分支的后面;
如果 本地分支
不存在,则会创建该本地分支;
示例:
git pull -r
10. 相关文章
- Git并行工作流程规范
- Git基础教程
- Git命令大全
- Git技巧和问题解决方案
- Git中合并多个提交的各种方法
- Git并行工作流程规范设计记录
- 弃用SVN选择Git的理由
- Git和Subversion的命令的对比
- 分布式和集中式版本控制系统的区别
- Git的存储机制