维护开源 repo 的 local patches 有哪些最佳实践?
建议日常开发使用完整分支,升级时在独立分支重放补丁,按需生成 patch files。这样可以直接查看、编译修改后的源码,也能保留清晰的本地修改清单。
团队正在使用的共享分支应保持稳定,需要整理或 rebase 的补丁序列则放在升级分支中维护。如果现有分支已积累大量 merge,可以继续通过 merge 升级,不必为了让历史整齐而重写已发布的分支。
1. 用 Git commits 维护补丁
对于长期定制的项目,一组组织良好的 commits 就是一份 patch list。团队成员照常修改源码、提交代码,由维护者整理补丁边界,无须每个人都手工编辑 .patch 文件。
每个补丁应尽量只做一件事,例如:
- 增加客户需要的配置项。
- 适配内部认证接口。
- backport 某个 upstream 修复。
- 为上游行为增加扩展接口。
提交说明至少要写清修改原因、依赖关系和保留条件。安全修复还应记录 upstream commit、相关公告和本地适配内容。
补丁并非拆得越小越好。如果一个功能拆成大量相互修补的 commits,升级时可能反复在同一位置遇到冲突。每个提交的范围应便于理解,最好能独立编译,并有对应测试。
如果构建系统需要 patch files,可以从提交序列生成:
git format-patch --output-directory ../patch-export \
upstream-base-old..product/current
这里假设该范围内是一组线性的本地 commits。git format-patch 导出的是非 merge commits,无法直接完整表达包含 merge 冲突处理的复杂历史。Git 文档
应确定唯一的维护来源。如果以 commits 为准,就用脚本生成 patch files,避免两边分别修改,再靠人工同步。
2. 用 worktree 同时保留日常开发和升级环境
升级无须删除、重新添加 repo。同一个 Git repository 可以使用多个 worktree,在不同目录检出不同分支。它们共享对象库,各自保留工作区。Git 文档
例如:
repo/ 日常开发
repo-upgrade/ 升级、解决冲突、运行验证
以下命令假设:
upstream已配置为上游 remote。upstream-base-old、upstream-base-new是已获取并核验的两个固定基线引用。product/current从旧基线出发,只包含线性的本地补丁。- 示例中的快照 tag、升级分支和目标目录尚不存在。
git fetch upstream
# 固定本次迁移的起点
git tag snapshot/before-upgrade product/current
# 在另一个目录准备升级分支
git worktree add -b upgrade/next \
../repo-upgrade snapshot/before-upgrade
cd ../repo-upgrade
# 将旧基线之上的补丁重放到新基线
git -c rebase.updateRefs=false rebase \
--onto upstream-base-new upstream-base-old
--onto 指定新基线和待迁移补丁的起点。命令中还关闭了自动更新其他分支引用的功能,避免升级时连带移动旧分支。rebase 会重建提交,因此 commit SHA 也会改变。Git 文档
原来的开发目录仍可正常使用。如果升级期间产生了新提交,应在切换前补迁,并核对快照之后的改动。
3. 逐个解决冲突,再比较补丁序列
rebase 遇到冲突时,可以查看当前正在迁移的补丁和工作区状态:
git rebase --show-current-patch
git status
git diff
修改冲突文件后,暂存实际处理过的文件,再继续:
git add path/to/resolved-file
git rebase --continue
如果需要撤销本次 rebase,可以执行以下命令。Git 文档
git rebase --abort
这样,冲突处理后的调整会保留在正在重放的补丁中,便于检查某项定制在新版本中发生了什么变化。如果多个补丁反复修改同一处代码,仍可能需要多次处理冲突。
迁移完成后,比较升级前后的两组补丁:
git range-diff \
upstream-base-old..snapshot/before-upgrade \
upstream-base-new..upgrade/next
git diff --stat upstream-base-new upgrade/next
git range-diff 用于比较两版提交序列,可以帮助检查补丁是否丢失、被重写或新增,但不能证明迁移后的行为正确。Git 文档
还需要执行项目的编译、测试和客户场景验证。尤其要检查没有文本冲突的补丁:接口语义、默认值或调用顺序的变化,也可能让旧补丁失效。
4. 用 rerere 复用冲突处理结果
经常升级或反复尝试迁移时,可以在仓库中启用:
git config rerere.enabled true
git config rerere.autoupdate false
rerere 会记录冲突及其人工解决结果,遇到匹配的冲突时尝试复用。关闭自动暂存后,可以先检查结果,再执行 git add。Git 文档
复用成功后仍要审查,因为相似的冲突文本可能有不同的业务语义。rerere 的记录也不会随普通的 push、clone 自动分发给团队,不能代替补丁说明和测试。
5. 已有大量共享历史时,可以继续 merge
如果分支已被多人长期使用,或依赖大量 merge 历史,继续 merge upstream 往往更方便协作。此时可以约定:
- upstream 升级使用专门的 PR。
- 升级提交不夹带无关功能和格式化修改。
- 冲突处理说明要写清涉及哪些定制,以及保留或调整的原因。
- 发布版本固定到不可变的 commit 或 tag。
merge commit 记录了两条历史的整合结果,其中包含多个补丁的冲突调整很正常。这些调整需要能解释清楚,也能通过验证。
如果选择重放补丁,建议验证后发布新的产品分支,例如 product/next,并更新多仓库清单,使其指向新分支。旧分支保留用于维护和回溯,避免直接 force-push 团队正在使用的分支。Git 文档也说明,对他人已经依赖的分支执行 rebase,会给下游带来历史修复工作。Git 文档
已有历史比较混乱时,可以先保留旧分支,在一次计划好的升级中整理出新的补丁序列。整理时要同时核对 commits 和最终源码差异,因为部分定制可能只存在于历史 merge 的冲突处理里。
6. 单独跟踪安全修复,升级时逐项核对
安全修复不应等到整轮版本升级时再处理。需要 backport 单个 upstream commit 时,可以使用:
# 将占位内容替换为已获取并核验的 upstream commit SHA
git cherry-pick -x <upstream-security-commit>
-x 用于在提交说明中记录来源。如果发生冲突,应检查最终说明是否保留了来源信息,并补充本地适配原因。Git 文档
应用前要确认修复是否依赖其他 commits,以及 upstream 是否为当前维护版本提供了 backport。一个 CL 可能只是完整修复的一部分。
后续升级时,应逐项检查这些 backports。如果新基线已包含等效修复,就移除重复补丁并验证结果。不能只凭 commit SHA 判断,因为 cherry-pick 和 rebase 都可能产生新的 SHA。
7. 50+ repos 要统一升级记录和交付清单
维护 50 多个 repo 时,建议用纳入版本控制的 manifest 或 lock 文件,记录每个 repo 的:
- upstream URL 和精确基线 SHA。
- 产品使用的 fork、精确交付 SHA。
- 维护负责人和验证入口。
- 尚未被基线吸收的安全 backports。
如果继续使用 patch files,还需记录补丁顺序和校验信息。构建应使用固定输入,避免每次拉取浮动分支后,再临时决定如何应用补丁。
可以把常规操作封装成统一脚本:获取源码、创建升级工作区、生成补丁差异报告,再调用各 repo 的验证任务。CI 需要检查补丁能否应用,以及组合后的产品是否正常。
要长期减少冲突,还需缩小对 upstream 内部实现的修改范围。能放进配置、插件或外围适配层的定制,可以逐步移出去。客户专有逻辑无法提交给 upstream 时,也可以考虑单独提交通用扩展接口。如果某个 repo 已长期重写核心模块,就应按内部 fork 安排负责人和升级预算;单靠更换 Git 工作流,无法消除这部分维护成本。
备注:内容仅供参考。
