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个文件,结果回滚时把新功能也带没了,折腾了一下午。

我的建议:提交信息用「动词+名词」的格式。比如「fix: 修复UART接收超时问题」或「feat: 添加STM32F4的SPI驱动」。这样在CI流水线里,可以自动生成变更日志。

2.1.2 分支(Branch)—— 并行开发的命脉

分支的本质就是一个可移动的指针。但在嵌入式项目里,分支的意义远不止于此。你想想看,硬件版本A在用v1.0的驱动,硬件版本B需要v2.0的驱动,这两个版本可能同时维护。没有分支,你根本玩不转。

这里有个坑:不要长期在同一个分支上开发。我见过一个团队,所有人都在master上直接提交,结果发布前一周,发现一个驱动改动影响了另一个模块,回滚都回滚不干净。说白了,分支就是你的「后悔药」。

2.1.3 合并(Merge)与变基(Rebase)—— 怎么选?

这个问题几乎每个工程师都会问。我的答案是:

  • 合并(Merge):保留完整的历史脉络。适合公共分支,比如master、release。因为你需要知道「这个功能是什么时候合进来的」。
  • 变基(Rebase):让历史变成一条直线。适合个人开发分支。比如你在feature分支上开发,每天拉取master的最新代码,用rebase可以让你的提交历史干干净净。
注意:绝对不要对已经推送到远程的分支执行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)来隔离风险。分支通常只存活几天,最长不超过一周。

为什么适合嵌入式?因为嵌入式开发中,硬件和软件经常需要同步调试。如果分支存在太久,等合回来时,硬件可能已经改版了,代码冲突会让人崩溃。

我的经验:如果你的团队CI/CD自动化程度高,每次提交都能自动跑编译、单元测试、静态检查,那么Trunk-Based是更好的选择。反之,如果测试全靠人工,那还是老老实实用GitFlow吧。

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流水线的搭建。到时候你会发现,如果版本控制这步没做好,流水线写起来会处处掣肘。所以,先把地基打牢吧。