PAYATHON 2026

维护开源 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 工作流,无法消除这部分维护成本。

备注:内容仅供参考。