2、版本控制基础:Git核心概念、Git工作流(GitFlow/Trunk-Based)、嵌入式项目仓库结构设计
好,咱们直接进入正题。版本控制这件事,在嵌入式开发里,说实话,以前很多团队是不太当回事的。我记得刚入行那会儿,有的老工程师还在用「复制文件夹+改日期」的方式管理代码。嗯,现在听起来有点离谱,但当时确实存在。
但到了CI/CD时代,版本控制就是地基。地基不稳,上面的一切自动化都是空中楼阁。这一章,我就把Git的核心概念、两种主流工作流,以及嵌入式项目特有的仓库结构设计,掰开了跟你聊聊。
2.1 Git核心概念:你只需要记住这几个
Git的概念很多,什么暂存区、对象库、引用、HEAD指针……但说实话,对于落地CI/CD来说,你真正需要吃透的,就下面这几个。
2.1.1 提交(Commit)—— 最小原子单元
提交就是一次快照。我个人的习惯是:每次提交只做一件事。比如修复一个bug、添加一个驱动、更新一份文档。千万别把「修了三个bug+改了两份配置+加了一个功能」揉到一个提交里。为什么?因为将来用git bisect定位问题时,你会疯掉。我曾经在一个项目里,就因为同事一个提交改了20个文件,结果回滚时把新功能也带没了,折腾了一下午。
2.1.2 分支(Branch)—— 并行开发的命脉
分支的本质就是一个可移动的指针。但在嵌入式项目里,分支的意义远不止于此。你想想看,硬件版本A在用v1.0的驱动,硬件版本B需要v2.0的驱动,这两个版本可能同时维护。没有分支,你根本玩不转。
这里有个坑:不要长期在同一个分支上开发。我见过一个团队,所有人都在master上直接提交,结果发布前一周,发现一个驱动改动影响了另一个模块,回滚都回滚不干净。说白了,分支就是你的「后悔药」。
2.1.3 合并(Merge)与变基(Rebase)—— 怎么选?
这个问题几乎每个工程师都会问。我的答案是:
- 合并(Merge):保留完整的历史脉络。适合公共分支,比如master、release。因为你需要知道「这个功能是什么时候合进来的」。
- 变基(Rebase):让历史变成一条直线。适合个人开发分支。比如你在feature分支上开发,每天拉取master的最新代码,用rebase可以让你的提交历史干干净净。
2.2 Git工作流:GitFlow vs Trunk-Based
工作流这东西,没有绝对的好坏,只有合不合适。我分别说说这两种,以及它们在嵌入式环境下的适用场景。
2.2.1 GitFlow —— 适合复杂、多版本并行的项目
GitFlow有五个核心分支:master、develop、feature、release、hotfix。结构清晰,但说实话,有点重。
| 分支类型 | 用途 | 嵌入式场景举例 |
|---|---|---|
| master | 生产就绪的代码 | 已通过全部测试的固件版本 |
| develop | 日常开发集成分支 | 所有feature分支的合入点 |
| feature | 新功能开发 | 比如「添加CAN总线驱动」 |
| release | 发布前的准备 | 修改版本号、更新release notes |
| hotfix | 紧急修复生产问题 | 现场反馈的某个传感器读取异常 |
我在一个车载项目里用过GitFlow。当时同时维护着三个硬件平台,每个平台都有自己的release分支。GitFlow的并行管理能力确实强。但缺点也很明显:分支太多,合并频繁,CI流水线要处理的事件也多了不少。
2.2.2 Trunk-Based —— 适合快速迭代、CI/CD成熟度高的团队
Trunk-Based的核心思想很简单:所有人都在主干(trunk/main)上开发,通过短生命周期的特性分支(feature branch)来隔离风险。分支通常只存活几天,最长不超过一周。
为什么适合嵌入式?因为嵌入式开发中,硬件和软件经常需要同步调试。如果分支存在太久,等合回来时,硬件可能已经改版了,代码冲突会让人崩溃。
2.3 嵌入式项目仓库结构设计
这部分是很多嵌入式团队容易忽略的。仓库结构设计不好,CI/CD流水线写起来会非常痛苦。我见过最夸张的,一个仓库里放了三个产品的代码,编译脚本互相依赖,每次CI都要跑两个小时。
2.3.1 单仓库 vs 多仓库
- 单仓库(Monorepo):所有代码放在一个仓库里。适合中小型团队,或者产品线比较单一的项目。优点是管理简单,原子提交方便。缺点是仓库会越来越大,CI流水线需要做增量构建。
- 多仓库(Multi-repo):每个模块独立一个仓库。适合大型团队,或者多个产品共享底层库的场景。优点是模块解耦,权限控制灵活。缺点是跨仓库的变更管理复杂,需要子模块(submodule)或依赖管理工具。
2.3.2 我推荐的嵌入式仓库结构
我个人比较推荐一种「混合模式」:
project-root/
├── app/ # 应用层代码
│ ├── src/
│ └── inc/
├── bsp/ # 板级支持包
│ ├── stm32f4/
│ └── imx6ull/
├── drivers/ # 外设驱动
│ ├── uart/
│ ├── spi/
│ └── i2c/
├── os/ # 实时操作系统(FreeRTOS、RT-Thread等)
├── tests/ # 测试代码
│ ├── unit/
│ └── integration/
├── tools/ # 编译脚本、烧录工具、CI脚本
│ ├── cmake/
│ └── jenkins/
├── docs/ # 文档
└── .gitignore
这种结构的好处是:层次清晰,职责分明。CI流水线可以针对不同目录做增量编译。比如只改了drivers/uart/下的文件,那就只编译UART相关的测试,不用全量编译。
.gitignore里,一定要把编译产物(.o、.hex、.bin、.elf)和IDE的临时文件(.uvguix、.settings)排除掉。否则每次提交都会带上这些垃圾文件,仓库体积会迅速膨胀。
2.3.3 关于子模块(Submodule)的使用
嵌入式项目经常需要引用第三方库,比如FreeRTOS、lwIP、mbedTLS。我建议用子模块来管理,而不是直接把源码拷进来。为什么?因为你可以跟踪第三方库的版本更新,而且不会把自己的改动和上游代码混在一起。
但要注意:子模块的版本要锁定。我曾经在一个项目里,因为子模块指向了master分支,结果上游更新了一个API,我们的代码直接编译不过。从那以后,我要求所有子模块必须指向具体的commit hash或tag。
2.4 本章小结
版本控制是CI/CD的基石。你不需要成为Git专家,但一定要理解提交、分支、合并这些核心概念。工作流方面,GitFlow适合复杂项目,Trunk-Based适合快速迭代。仓库结构上,我建议采用分层清晰的单仓库模式,配合子模块管理第三方依赖。
下一章,我们会聊到CI/CD流水线的搭建。到时候你会发现,如果版本控制这步没做好,流水线写起来会处处掣肘。所以,先把地基打牢吧。