mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
5137 字
14 分钟
谈代码历史与版本控制系统
2024-02-06

接触一门完全陌生的技术时,你是否也曾困惑于它从何而来、如今怎样、未来又将往何处?不得不从维基百科、官方文档、代码仓库的 issue 和历史中拼凑信息,才能还原一段完整的技术脉络。这种体验,正是版本控制系统试图解决的核心问题的缩影——如何记录历史、如何从历史中获取所需。

一、活着的软件#

世间常言,学史以明智,鉴往而知来。从历史中,不仅仅能看到陈年往事,更能从中汲取出一些经验和规律。软件的迭代也是如此,信息时代造成的信息爆炸现象使得某些事物的历史无法书写在纸张上,那么充分利用电子存储设备记录和整理历史便成为了一大要务。那么应该如何记录这些历史,又该怎样从这些历史中获取想要的东西呢?

我想大家可能遇到过这样一种场景:接触一门完全陌生的技术时想知道它是从何而来,如今又是怎样的现状,未来又会往哪个方向发展。此时便不得不查阅资料,从维基百科,从该技术相关的权威站点或者相关技术的官网,从代码仓库的 issue 和历史等等……不得不从这些分布在互联网各处的资料进行整合和学习,以求窥得真理,还原历史。从中可能会看到同类型软件之间的竞争与博弈,可能会看到互补类型软件之间的相互促进与融合,可能会看到一个软件从创建到繁荣,从繁荣到冻结的一生。这不正是一副波澜壮阔的历史画卷吗?就像人类历史一样,软件在某些意义上大概也算是有生命的,是活着的吧。

二、需求来源于生活#

在互联网普及之前(20 世纪 90 年代),开发者们就一直存在版本管理的需求。最早期的版本管理是通过手动复制文件并重命名的形式进行的,这是一种十分朴素且直接的使用方法,即使是现在在某些场景也会用这种方式去管理文件版本。随后分别出现了例如 RCS 这种基于锁的修订管理系统(Revision Control System);以及例如 SVN 这种基于服务器的集中式版本控制系统(Centralized Version Control System);还有目前使用最广泛的例如 Git 这类分布式版本控制系统(Distributed Version Control System)

版本控制迭代史

从以上几个版本控制系统的官方网站能看到,它们主要用于管理程序源代码,因为程序源代码也属于一种修改频繁且需要保留历史版本的一类文档。特别是大量开发者共同参与的开源类项目,若使用 RCS 这类版本控制系统则无法做到共同开发。若使用 SVN 这类版本控制系统则不够灵活,不支持离线开发以及对开源社区开发者不够友好。所以使用分布式版本控制系统是最为理智的决定。

当用户使用开发的软件或者库时,由于用户使用的版本、环境以及操作方式都是随机的,所以在传统环境下遇到的问题即使反馈给开发者,开发者大概率会甩你一脸《提问的智慧》,或者 issue 提问指南,比如提问须知。作为一个想尽力维护好自己作品的开发者,有必要花时间写 FAQ(Frequently Asked Questions)提问指南以及格式规范等文档。甚至,为了降低用户门槛,可以在客户端封装一些子命令,用于获取程序的构建时和运行时信息。

Info

使用「不可变基础设施」发布和管理软件能更容易地进行场景复现。

三、在正确的地方求经问道#

那么怎样正确地使用 Git 进行版本管理呢?从 madnight.github.io 中能发现 JavaScript 是使用百分比最高的语言,那么简单推理可得 JavaScript 也是迭代最快、对版本控制需求最大的语言。接着从 JavaScript 的相关社区中就能发现,他们使用下面的工具进行版本管理与发布:

  • conventionalcommits:从项目的提交消息和元数据生成变更日志和发布说明的工具。
  • release-it:自动化版本控制和包发布。

即使不使用这些工具,从它们朝气蓬勃的社区发展就不难看出人们对它们的热衷。同时,也能从它们的用途开始研究怎样更标准、更正确地使用 Git 进行项目的版本管理:

  • 为提交消息制定一组统一的简单规则,这有利于编写自动化工具,对项目的变更历史进行审计。
  • 使用语义化版本控制的规则定义版本号和标签。
  • 提交前使用程序自动生成简单的 CHANGELOG.md 文档用于说明版本差异。
  • 集成 CI/CD(Continuous Integration/Continuous Deployment) 工具自动化版本控制和软件包的发布。
Info

目前软件包的发布可以集成相关插件到 GitHub Actions,比如 Golang 项目使用 goreleaser 能轻松自动生成不同架构的可执行文件、容器镜像和 CHANGELOG。

四、小试牛刀#

纸上得来终觉浅,绝知此事要躬行。好与不好仅靠学习和观察是得不出结论的,只有设身处地进行实践才能得到最真实的感受。在前面我们已经了解了版本控制的重要性和较为标准的用法,接下来我打算使用 pypssh 项目练手,因为 Python 不像 Golang 那样有干净的构建环境,为了使特定环境、特定版本的构建产物能够复现,它还需要记录一些额外信息。

4.1 版本号相关实践#

首先根据语义化版本规范,规定版本格式为主版本号.次版本号.修订号。在初始开发阶段,以 0.1.0 为初始版本号,且后续每次发行都递增修订号,在 1.0.0 之前不需要对 API 的兼容性负责。以项目成熟、已经被多人依赖、并且开始考虑 API 的兼容性时作为正式发布阶段,以 1.0.0 作为起点,每次发行都递增版本号。

然后再考虑怎样让用户感知程序的版本。桌面程序一般是通过「关于」按钮,命令行程序一般是通过 --version/-v 选项获取版本。可以先看看自己所用到程序是怎样做的,再考虑自己怎样实现。

首先看看 VSCode 桌面程序的版本号是怎样体现的,单击帮助中的关于按钮后显示了如下信息:

Version: 1.56.2 (user setup)
Commit: 054a9295330880ed74ceaedda236253b4f39a335
Date: 2021-05-12T17:13:13.157Z
Electron: 12.0.4
Chrome: 89.0.4389.114
Node.js: 14.16.0
V8: 8.9.255.24-electron.0
OS: Windows_NT x64 10.0.19041

从以上可以很清晰地看到这个 VSCode 的软件版本号、提交 hash 值、构建日期以及上游软件/构建软件的版本号。通过这些信息我们基本就能重新构建出一个几乎一模一样的软件。

然后,再挑一个命令行程序 pypssh 查看它的版本号是怎样体现的,通过执行 pypssh version 后显示了如下信息:

地址: https://github.com/witchc/pypssh
版本号: v0.2.2
解释器版本: Python 3.7.4 (default, Jun 9 2021, 15:16:23) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
发行版: Linux-4.19.104-microsoft-standard-x86_64-with-debian-10.9

从以上信息可以看出,它的输出和 VSCode 的「关于」菜单项输出的内容很是相似,都显示了构建环境和构建上游程序的版本。

其中 pypssh 的实现也比较简单:

@cli.command()
def version():
"""
print version
"""
addr = "https://github.com/witchc/pypssh"
vno = "v0.2.2"
interrupt_version = "Python " + ' '.join(sys.version.split('\n'))
print(
"\n".join
([
f"Github: {addr}",
f"Version: {vno}",
f"Interrupt-Version: {interrupt_version}",
f"Running-Platfrom: {platform.platform()}"
])
)

基于以上实现,我们只需要保证每次发行时手动确保局部变量 vno 和 GitHub 中 tag 一一对应,即可实现一个简单的 --version 代码。通过特定发行版打印的版本信息,我们便能获取到对应的源代码以及构造一个一样的构建环境,从而进行发行版的重构建或者调试。

如果想在 --version 中携带更多的构建时信息,则可以采用构建时生成临时文件带入程序中的形式去展现,具体可以参考这个示例仓库

4.2 提交消息最佳实践#

使用 VSCode 开发的项目,推荐使用 Conventional Commits 插件,该插件提供了标准的提交消息规范。若不使用 VSCode 开发,也可以使用最流行的提交消息规范工具 cz 提交消息。

CHANGELOG 的自动生成需要和方案多种多样,只要项目为提交消息制定了规范,那么自己也很容易能开发一个 CHANGELOG 生成工具,因为所需要的信息都在 git log 中了。

当完成当前版本所有开发计划后,便可以在 GitHub 上建立版本标签,将代码归档,并且开始新的版本计划。版本标签的命名规则需要遵循 semver

4.3 持续集成实践#

目前比较流行容器化构建,因为容器化环境能确保构建的一致性。所以目前大多数代码管理平台都支持管道构建(PIPE BUILD),比如开源项目就可以选择 GitHub Actions 集成如 github-actions-golang 插件,实现较为标准的持续集成方案。由于不同平台都有不同的插件生态和语法,所以这里就不细聊了,具体参考平台文档和示例即可。比如 GitHub Actions 就可以参考博客 GitHub Actions 入门教程 - 阮一峰

五、Git 工作流对比#

选择了 Git 作为版本控制工具后,团队面临的下一个问题是:怎样组织分支和合并?不同的工作流适合不同的团队规模和发布节奏。下面是三种最常见的工作流。

5.1 GitFlow#

GitFlow 由 Vincent Driessen 在 2010 年提出,是最早被广泛采用的 Git 工作流。它定义了两种长期分支和三种短期分支:

  • 长期分支main(生产代码)和 develop(开发集成分支)
  • 短期分支feature/*(功能开发)、release/*(发布准备)、hotfix/*(线上紧急修复)
gitGraph commit id: "init" branch develop checkout develop commit id: "dev-1" branch feature/login checkout feature/login commit id: "feat-1" commit id: "feat-2" checkout develop merge feature/login branch release/1.0 checkout release/1.0 commit id: "bump-version" checkout main merge release/1.0 checkout develop merge release/1.0

GitFlow 的优点是分支职责清晰,适合有明确版本发布周期的项目。缺点是分支管理开销大,对于持续部署的项目来说过于繁琐。每次功能开发都要从 develop 拉出 feature 分支,合并回 develop,再从 develop 拉出 release 分支,最后合并到 maindevelop。对于一天发布多次的 Web 项目,这个流程太重了。

5.2 GitHub Flow#

GitHub Flow 是 GitFlow 的简化版,只有一种长期分支 main,所有开发都基于短期分支:

  1. main 拉出描述性分支(如 fix-auth-bug
  2. 在分支上开发并提交
  3. 开发过程中持续与 main 同步
  4. 通过 Pull Request(PR) 进行代码审查
  5. 审查通过后合并到 main
  6. 合并后立即部署
gitGraph commit id: "init" branch fix-auth-bug checkout fix-auth-bug commit id: "fix-1" commit id: "fix-2" checkout main merge fix-auth-bug commit id: "deploy" branch add-feature checkout add-feature commit id: "feat-1" checkout main merge add-feature commit id: "deploy-2"

GitHub Flow 的核心假设是:main 上的代码随时可以部署。这意味着每次合并前需要充分的测试和审查。它非常适合持续部署的 Web 项目和 SaaS(Software as a Service) 产品。

5.3 Trunk-Based Development#

Trunk-Based Development(主干开发) 是三种工作流中最激进的:所有开发者在 main(或 trunk)上直接提交,或者使用极短生命周期的分支(不超过一天)。

核心实践:

  • 小批量提交:每次提交尽量小,频繁集成
  • Feature Flag(功能开关):未完成的功能用开关控制,不影响主分支部署
  • 自动化测试:提交前跑完整测试套件,确保主干始终可用
  • 短生命周期分支:如果用分支,必须在一天内合并

Google 和 Facebook 都采用这种方式。它的优势在于避免了合并地狱,因为每个人都频繁与主干同步。但前提是工程基础设施足够成熟:自动化测试覆盖率要高,Feature Flag 系统要完善,CI 要快。

5.4 三种工作流的选择#

维度GitFlowGitHub FlowTrunk-Based
适合团队大型、分层团队中小型、扁平团队高成熟度工程团队
发布节奏按版本发布持续部署持续部署
分支复杂度最低
对 CI 的要求极高
学习成本
典型场景桌面软件、嵌入式Web 应用、SaaS大厂内部服务

对于刚起步的小团队,GitHub Flow 是最务实的选择。等团队和项目复杂度上来后,再考虑是否需要 GitFlow 的严格管控,或者向 Trunk-Based 演进。

六、分支命名规范#

不管用哪种工作流,一致的分支命名能让团队快速识别分支用途。推荐以下命名模式:

<类型>/<简短描述>-<Issue编号>

常见类型前缀:

前缀用途示例
feat/新功能feat/user-profile-123
fix/修复 Bugfix/login-redirect-456
refactor/代码重构refactor/auth-module
docs/文档更新docs/api-reference
test/测试相关test/integration-checkout
chore/构建/工具变更chore/update-deps
release/发布准备release/2.1.0
hotfix/线上紧急修复hotfix/payment-timeout-789

命名时注意几点:

  • 用英文短横线 - 分隔单词,不用下划线或驼峰
  • 描述部分尽量简洁,控制在 3-5 个词以内
  • 关联 Issue 编号方便追溯上下文
  • 避免在分支名中包含作者名字(git log 已经记录了)

七、提交消息最佳实践#

好的提交消息是项目历史的”注释”,它回答了三个问题:做了什么、为什么做、影响范围是什么。

7.1 Conventional Commits 规范#

Conventional Commits(规范提交) 是目前最流行的提交消息规范,格式如下:

<类型>(<范围>): <描述>
[可选的正文]
[可选的脚注]

常用类型:

类型含义是否影响版本号
feat新功能次版本号
fix修复 Bug修订号
docs文档变更
style格式调整(不影响逻辑)
refactor重构
perf性能优化
test测试相关
chore构建/工具变更
ciCI 配置变更

featfix 之外的所有类型都归类为补丁,不触发版本号变更。如果某个 featfix 包含破坏性变更,在类型后加 !,或在脚注中加 BREAKING CHANGE:

feat(api)!: 重构用户认证接口
BREAKING CHANGE: 登录接口从 /auth/login 迁移到 /api/v2/auth/login,
旧的 Authorization header 不再支持,需改用 Bearer token。

7.2 写好提交消息的技巧#

用祈使句写描述。写 “add feature” 而不是 “added feature” 或 “adds feature”。这样在 git log 中读起来更自然:“This commit will add feature”。

首行控制在 50 字符以内。首行是提交的”标题”,应该在 git log --oneline 中一目了然。详细信息放在正文中。

正文与首行空一行。Git 会把首行作为标题,正文作为详细信息,在 git log 和 GitHub 中呈现不同的样式。

解释 Why 而非 What。diff 已经展示了代码改了什么,提交消息应该解释为什么这样改。比如:

fix: 限制搜索结果最多返回 100 条
当数据量超过 10 万条时,未限制的查询会导致内存溢出。
100 条的限制覆盖了 99% 的使用场景,同时避免了 OOM。

拆分大提交。如果一个提交同时修改了功能、修复了 Bug、重构了代码,应该拆成三个提交。每个提交只做一件事,方便后续 git bisectgit revert

7.3 工具辅助#

  • commitizen/cz-cli:交互式提交消息生成器,引导你按规范填写
  • commitlint:提交消息校验工具,配合 husky 在提交时自动检查格式
  • VSCode Conventional Commits 插件:在编辑器中图形化填写提交消息

package.json 中配置 commitlint 和 husky 的示例:

{
"devDependencies": {
"@commitlint/cli": "^17.0.0",
"@commitlint/config-conventional": "^17.0.0",
"husky": "^8.0.0"
},
"commitlint": {
"extends": ["@commitlint/config-conventional"]
}
}

八、CI/CD 集成模式#

版本控制不止是保存历史,更是自动化工作流的触发器。当 Git 仓库与 CI/CD 系统打通后,提交代码就会自动触发构建、测试和部署。

8.1 持续集成(CI)#

持续集成的核心目标:让主分支始终处于可部署状态。实现这个目标需要:

  1. 提交时自动运行测试:每次 push 或 PR 都触发完整的测试套件
  2. PR 合并前必须通过 CI:在 GitHub 中通过 branch protection rules 强制执行
  3. 快速反馈:CI 跑完的时间控制在 10 分钟以内,越长开发者越容易忽略结果

GitHub Actions 的典型 CI 配置:

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build

8.2 持续部署(CD)#

持续部署在 CI 通过后自动将代码发布到生产环境。关键实践:

  • 环境分离:dev -> staging -> production,每个环境有独立的部署流水线
  • 金丝雀发布(Canary Release):先部署到一小部分实例,观察指标正常后再全量发布
  • 回滚机制(Rollback):部署出问题时能快速回滚到上一个稳定版本

8.3 自动化版本发布#

结合 Conventional Commits 和 CI/CD,可以实现全自动的版本发布流程:

  1. 开发者在 feature 分支上按规范提交
  2. PR 合并到 main 后,CI 跑测试
  3. 测试通过后,自动根据 commit 历史计算下一个版本号
  4. 自动生成 CHANGELOG
  5. 自动创建 Git Tag 和 GitHub Release
  6. 自动发布到 npm / Docker Hub / 应用商店

这套流程可以用 release-pleasesemantic-release 实现。前者由 Google 维护,通过 PR 的方式管理发布;后者完全自动化,合并即发布。

8.4 常用 CI/CD 平台对比#

平台托管方式免费额度生态适用场景
GitHub Actions云端2000 分钟/月丰富开源项目、GitHub 生态
GitLab CI自建/云端400 分钟/月完善企业私有化部署
CircleCI云端6000 分钟/月丰富中型团队
Jenkins自建无限插件极多大型企业、复杂流水线
Drone自建无限轻量Docker 化项目

九、现代史话#

计算机的历史还没有超过一百年,当代的流行软件的历史甚至没有超过三十年,但它们对世界、对社会的影响却是天翻地覆的,不关注这段历史将难以相信这仅是信息社会前几十年的发展成果。我收藏过一些电子博物馆以及技术发展史的一些站点,可供参考:

十、参考资料#

标准与规范

实用工具


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

谈代码历史与版本控制系统
https://blog.souloss.com/posts/idea/idea-version-control/
作者
Souloss
发布于
2024-02-06
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时