版本控制是前端工程化中的重要一环,而最具代表性的版本控制工具就是Git。
1. Git的起源
Git 是一个开源的分布式版本控制系统,它的起源与 Linux 内核开发息息相关。在 Linux 内核开发过程中,世界各地的开发者共同协作,对代码进行修改和维护。
-
2002年,Linux 启用了 BitKeeper 作为版本控制系统来管理代码版本、合并不同开发者的修改。
-
2005 年,开发 BitKeeper 的商业公司 收回了对 Linux 社区的免费使用授权。于是Linux 内核的创始人 Linus Torvalds 决定开发一个新的版本控制系统,这个系统就是 Git。它的设计目标就是速度快、设计简单、以及支持分布式开发和非线性开发流程,可以高效管理类似 Linux 内核的超大规模项目。
Git 最初是为了满足 Linux 内核开发的需要,但很快就被其他开源项目和开发者所采用。由于其开源的性质,许多开发者为 Git 的功能扩展和性能优化做出了贡献。自从2005年诞生以来,Git 日臻成熟完善,成为了软件开发领域占据绝对优势地位的版本控制系统,广泛应用于各种规模的软件项目。
下面我们来看看如何利用 Git 版本控制工具来实现工程质量保障。
2. 约定式提交规范
使用 Git 进行版本控制,每次编写完新功能都要提交(commit
),并带上提交信息(commit message
)。如果对提交信息不作约束,就难以通过提交信息对项目工程的改动进行有效的追踪和管理。因此需要建立一套完整的规范对commit message进行约束,规范化项目工程中的提交过程。
业内最成熟的规范就是约定式提交规范(Conventional Commits),它最初是由 Angular 团队提出的,通过制定一些基本且简单的规则来创建清晰的提交历史,方便开发人员持续地管理项目。后来,它被越来越多的开源项目和企业级软件开发团队采用,并且广泛传播。
约定式提交规范带来的好处包括:
- 对于开发人员:能够快速了解每个提交的意图和影响范围,减少沟通成本,提高协作效率。结构化的提交历史可以提高项目的可维护性,尤其对于开源项目可以降低对项目做出贡献的难度。
- 对于持续集成:很多工具可以基于提交规范进行自动化操作。比如,可以自动生成变更日志,或者根据提交类型来确定版本号的更新规则等等。
约定式提交规范的结构组成包含header
、body
、footer
三部分。
<type>[optional scope]: <subject>
[optional body]
[optional footer(s)]
header
:commit message的第一行,用来描述本次commit的变化。Body
:可选的详细描述,包括代码变更的动机,以及前后行为对比。可以分多行。footer
:可选的补充描述,只适用于不兼容变更和关闭issues这两种情况。
通常情况下,开发人员在commit message中只需要填写header
中的type
和subject
的部分,其余部分都是可选项,用于补充信息。比如:
git commit -m "feat(pages): add homepage"
header
包含三个部分:<type>[optional scope]: <subject>
- 类型(type):标识提交的类型
- 作用域(scope):可选,指定提交影响的范围,通常是某个模块或功能区域的名称
- 主题(subject):对提交内容的简洁描述,说明本次提交做了什么
常用的提交类型有:
feat
:完成某个特定的功能模块fix
:缺陷修复style
:代码格式调整,不改变代码逻辑,如统一缩进refactor
:代码重构perf
:性能优化test
:添加或修改测试docs
:文档修改chore
:项目构建、工具配置等杂项事务,比如升级依赖ci
:持续集成相关的配置或脚本调整build
:构建系统相关的配置或脚本调整revert
:回滚操作,用于撤销之前的一个或多个提交。提交信息必须以revert
开头,后面跟着要撤销提交的header,并且body部分是固定格式,指定要撤销的SHA标识。git commit -m "revert: feat(pages): add homepage This reverts commit abc123."
不同的提交类型在构建变更日志(CHANGELOG)时起到了分类作用。
feat
:通常会出现在变更日志的新功能部分,说明本次发布中包含了哪些新的功能特性。fix
:通常会出现在变更日志的“修复问题” 或 “Bug 修复” 部分revert
:如果被撤销的提交属于同一个发布,那么revert commit不会出现在变更日志中;否则,它通常会出现在变更日志的Reverts的小标题下。- 其他类型:不会直接影响软件的功能的改动,通常不会出现在变更日志中
3. 分支管理
几乎所有的版本控制工具都支持某种形式的分支管理,使用分支可以让开发人员将自己的工作从开发主线上分离开来,以免影响开发主线。而 Git 在分支管理方面极其高效,正是这一特性使它从众多版本控制工具中脱颖而出。
Git的分支创建和分支切换非常轻量快速。从数据结构来看:
- Git 使用一种基于内容哈希的存储系统。在 Git 的内部,每个文件的内容都通过哈希算法(如 SHA - 1)计算出一个唯一的哈希值来标识。这些文件内容以对象的形式存储在
.git/objects
目录下。 - Git 分支本质上是一个指向特定提交(commit)的指针。
这种存储和引用方式使得分支创建和切换的操作不需要大量的数据复制或移动。
- 当创建一个新分支时,Git 并没有复制整个代码仓库的内容,只是创建一个指针指向当前commit,这个操作几乎是瞬间完成的。
- 当切换分支时,Git 会根据要切换到的分支所指向的提交来更新索引(暂存区)和工作目录中的文件,这个过程是通过比较分支的提交历史和文件内容的哈希值来实现的,也非常便捷。
因此,在Git中可以方便地使用分支与合并来进行版本管理。下面介绍几种常见的分支模式。
分支模式:主干开发模式
主干开发模式(Trunk - Based Development,TBD)下,开发团队主要在一个被称为 “主干”(通常是master
分支)的分支上进行开发工作。当需要发布时,根据版本号拉一个release发布分支。
master
:所有的开发都在主干分支上进行,包括功能开发、缺陷修复等release
:根据发布节奏,从master分支拉取特性版本进行发布
它的工作流程:
- 持续集成与小步提交:每次变更提交的改动功能点要小而可控,同时要快速验证,使得主干分支始终处于可发布的状态。
- 功能开关与特性管理:采用特性开关(Feature Flags)隔离未完成的特性,避免对主干造成影响。发布时必须隐藏未完成的特性,直到特性完成后再打开开关。比如很多开源项目的特性都需要手动开启实验模式才可以使用。
这种模式的优点:
- 分支少,合并冲突小,方便管理。
- 适合持续交付及部署,以及简单密集的需求交付
这种模式的缺点:
- 要求有较高的团队协作熟练度,以及完善的集成规范
- 一般不适合开发持续时间长的、功能复杂的业务
主干分支模式的灵活性高,但随着项目的发展可能会导致分支管理混乱。它适用于小型的、实验性的项目或者项目的早期探索阶段。
分支模式:Git-Flow
在Git-Flow模式中,根据项目的不同需求创建对应开发分支,多个需求可以并行开发。比如,在敏捷开发中,每个迭代在开始时都从主干分支拉出一个功能分支,所有关于这个需求的开发都在功能分支上进行,开发完成后把功能分支合并回主干分支,测试通过后进行发布。
在Git-Flow模式中,根据使用方式不同,可以将分支模型划分为以下几种:
master
:稳定分支,保存最新的已发布代码feature
:功能开发分支develop
:开发主干分支,迭代中的feature分支的集合,包含所有特性功能release
:版本发布分支hotfix
:热修复分支,修复线上问题的临时分支
它的工作流程为:
- 从
develop
分支切出一个feature
分支,进行功能开发 - 功能开发完成后,
feature
分支合入develop
分支。这里一般采用git merge --no-ff
,保留功能分支历史,方便查看merge历史和分支状态 develop
分支为只读唯一分支,只能从其他分支合并。从develop
分支拉取release
分支然后进行测试- 对
release
分支进行功能测试,并且在此分支上修复缺陷 release
分支上线后,将release
分支合并到develop
和master
,并打上tag
- 对于线上缺陷,需要从
master
拉取hotfix
进行缺陷修复 - 在
hotfix
分支通过测试上线后,将hotfix
分支合并到develop
和master
,并打上tag
它的优点:
- 特性并行开发,效率高,代码独立
- 适合复杂业务、大团队协同开发
- 支持多版本发布
它的缺点:
- 分支结构复杂,对小型团队和项目负担太重
- 功能分支的生命周期过长会导致合并冲突
Git-Flow模式能够很好地支持多需求并行开发。它相对复杂,适用于大型的、长期维护的软件项目,尤其是有严格发布流程和需要同时维护多个版本的项目。
分支模式:Github-Flow
Github-Flow是简化版的Git-Flow,它更轻量,只保留了master
和feature
分支,是一种以部署为中心的分支管理模式。Github-Flow不涉及发布分支,只要准备好发布一个版本,就可以对其进行部署。并且它认为hotfix
和feature
在发布流程上应该统一,可以用feature
代替hotfix
。
Github-Flow的分支模型可以分为:
master
:保存最新的已发布代码,随时处于可部署状态feature
:进行开发的分支,包括功能开发、缺陷修复
Github-Flow的工作流程规定如下:
master
分支上的代码都是可部署的最新版本- 从
master
分支创建feature
分支进行开发 - 代码应该尽可能频繁地提交到本地开发分支并同步到远端,从而减少合并时的冲突
- 将
feature
分支合入master
分支时,需要发起PR(pull-request
),并确保CR(Code review)通过 feature
分支进行CR的同时,必须将其部署到测试环境以进行验证feature
分支通过了代码审查和测试后,必须尽快合入master
分支,并确定master
处于随时可部署状态
相比Git-Flow模式,Github-Flow更加轻巧简便。它强调持续集成和快速反馈,是精益软件开发(Lean Software Development)和持续交付(Continuous Delivery)所倡导的最佳实践。适用于以快速迭代为主要特点的小型到中型项目,尤其是在 GitHub 平台上进行开发的项目。
分支模式:Gitlab-Flow
Gitlab-Flow 结合了 Git-Flow 和 Github-Flow 的一些特点。它也以master
分支为中心,支持feature
分支的创建和合并,同时引入了环境分支(如production
、staging
等)来更好地管理发布和部署。例如,production
分支用于实际的生产环境,staging
分支用于测试环境,开发完成的feature
分支可以先合并到staging
分支进行测试,然后再推送到production
分支。
Gitlab-Flow的分支模型可以分为:
master
:部署在集成环境下的代码分支feature
:功能开发的分支,包括功能开发、缺陷修复staging
:部署在预发布环境下的代码分支production
:部署在生产环境下的代码分支
它的工作流程如下:
- 功能开发完成后,提交
feature
合入master
的MR(merge request) - CR通过后,将
feature
分支的代码合入master
,并部署到集成环境进行验证 - 验证通过后,提交
master
合入staging
的MR - 将
master
合入staging
,部署到预发布环境进行验证 - 预发布环境验证通过后,提交MR将
staging
合入production
- 将
production
部署到正式环境中。
Gitlab-Flow模式适用于需要在不同环境中进行测试和部署的项目,无论是小型还是大型项目都可以采用。特别是对于使用 Gitlab 作为代码托管平台并且注重开发、测试和部署流程管理的团队,这种模式能够提供很好的支持。
在实际的项目开发过程中,通常需要根据项目自身的业务特点和团队规模来选择合适的分支模式。
4. Git Hook
Git可以在特定动作发生时触发自定义脚本,它提供了两组钩子:客户端和服务端。
客户端常用的钩子:
-
pre-commit
:在执行git commit
命令之前运行,主要用于检查即将提交的快照。如果该钩子以非零值退出,则Git将放弃此次提交。可以利用这个钩子做一些代码提交前的准备工作,比如检查代码是否符合项目的规范、代码格式是否正确、是否通过了单元测试等。 -
prepare-commit-msg
:在启动提交信息编辑器之前触发。可以结合提交模板使用,动态插入信息。比如,添加一些固定的文本(项目名称、当前日期)到提交消息的开头,或者根据提交的文件内容自动生成部分提交消息。 -
commit-msg
:在开发者输入提交消息并关闭编辑器之后运行。如果该钩子以非零值退出,则Git将放弃此次提交。在一个采用约定式提交规范(如 Conventional Commits)的项目中,commit-msg
钩子可以检查提交消息是否符合<type>[optional scope]: <description>
的格式。如果不符合,就阻止提交并提示开发者修改提交消息。 -
post-commit
:在git commit
操作成功完成后运行。可以用于执行一些后续的操作,比如通知相关人员提交已经完成,或者将提交信息记录到其他系统中。 -
pre-push
:在执行git push
操作之前运行。在代码被推送到远程仓库之前,对本地的提交进行最后的检查。例如检查所有的提交是否都已经通过了必要的测试、是否符合项目的代码规范、是否会与远程仓库的内容产生冲突等。如果它以非零值退出,则终止推送过程。
服务端常用的钩子:
-
pre-receive
:在服务端接收推送(git push
)的提交之前运行。如果它以非零值退出,则所有推送的内容都不会被接受。它可以用于检查推送的内容是否符合项目的要求,比如检查是否有不符合规范的提交、是否会导致合并冲突等。比如,它可以用于阻止non-fast-forward的更新。 -
update
:与pre-receive
类似,但它会对推送中的每个引用(如分支)进行单独检查。如果它以非零值退出,则只有相应的应用会被拒绝。它可以用于更精细地控制每个分支的更新,比如检查每个分支的推送是否符合分支的特定规则。比如,在一个采用 Git-Flow 分支模式的项目中,update
钩子可以检查master
分支的推送是否只包含发布相关的提交,develop
分支的推送是否包含经过测试的新功能开发提交等。 -
post-receive
:在服务端成功接收推送的提交之后运行,可以用来更新其他系统或者通知用户。比如,在一个CI/CD
环境中,可以触发自动构建系统(如 Jenkins)来构建和测试刚刚推送的代码,或者更新项目的在线文档。值得注意的是,客户端在post-receive
脚本结束运行之前都会保持连接状态,如果脚本执行时间过长会导致客户端长时间等待,影响用户体验并且可能占用服务器资源。最好避免长时间运行操作,采用异步处理等方式来处理复杂任务。
5. 相关工具
结合相关的工具可以在开发流程中对质量管控进行卡点。
commitizen
commitizen
是一个格式化commit message的工具,帮助开发者编写符合规范的提交消息。它通过适配器(Adapters)来支持不同的提交消息规范,并且提供了一个交互式的命令行界面,引导开发者按照一定的规范(如 Conventional Commits 规范)来填写提交消息。
比如使用cz-conventional-changelog
适配器用于 Conventional Commits 规范。
commitizen init cz-conventional-changelog --save-dev
设置适配器后,使用 git cz
代替 git commit
,命令行就会提供格式选项,通过流程化的引导自动生成符合格式的commit message。
husky
husky
是一个开源社区的Git Hook工具,它允许开发者在 Git 钩子(如pre-commit
)中轻松地添加自定义脚本,它是前端工程化搭建中必不可少的部分。
只需在package.json
中配置自定义脚本:
{
"husky": {
"hooks": {
"pre-commit": "npm run lint && npm run test",
"pre-push": "npm run lint"
}
}
}
commitlint
commitlint
是一个用于检查提交消息是否符合指定规范的开源工具,可以帮助开发人员有效管理commit message。
commitlint
通过读取配置文件(如.commitlintrc.js
)中的规则来检查提交消息,并且通过命令行工具(如commitlint-cli
)来解析提交消息,并根据配置的规则返回检查结果,如是否通过检查、有哪些不符合规则的地方等。
可以结合husky
工具,在commit-msg
钩子进行检查。配置如下:
{
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
}
conventional-changelog-cli
conventional-changelog-cli
是一个用于根据提交历史生成变更日志(Changelog)的工具。它遵循 Conventional Commits 规范或者其他自定义的规范,从 Git 提交历史中提取信息,生成清晰、有条理的变更日志。
它通过解析 Git 提交历史,根据提交消息中的类型(如feat
、fix
等)和其他相关信息(如范围、描述)来构建变更日志的各个部分。例如,它可以将feat
类型的提交归类为新功能部分,fix
类型的提交归类为修复问题部分等,在变更日志中展示每个版本的更新内容。可以通过命令行参数指定版本范围、输出格式等,然后将生成的变更日志输出到文件或者控制台。
在发布软件新版本时,生成变更日志是一个重要的环节。conventional-changelog-cli
可以自动化这个过程,节省时间并且保证变更日志的质量。