1. 一句话

上游源码是以压缩包/整份拷贝导入进自己仓库时(而不是先 clone 上游再改),这个 fork 与上游没有共同祖先(unrelated histories),git merge upstream/main 必然失败。同步只能走内容差异补丁,不能走 merge。

2. 触发条件

  • 仓库历史里有一次性导入提交,如 Import Gemini Web2API Go source、baseline。
  • 本地在此基础上加了自有特性,现在想跟上上游的后续版本。

3. 症状与报错

$ git merge v4.20.1
fatal: refusing to merge unrelated histories

加 --allow-unrelated-histories 后不再报错,但问题更大:merge base(共同祖先)为空,凡双方内容有差异的文件全部冲突,等于逐文件手工"二选一"。

4. 根因

Git 的三方合并(three-way merge)依赖 merge base 来判定"谁改了什么"。没有共同祖先时,双方所有差异都被视为"各自新增",自动合并的判定依据根本不存在。

5. 正确做法:补丁同步

前提:能拿到上游当前基线版本与目标版本两份源码。

  1. 确定基线版本:把本地树与上游各 tag 逐一二进制 diff,差异最小者即基线。
    git diff --no-index --stat <tag目录> <本地目录>
  2. 两份源码解压到同一父目录下的同级目录(保证 a/、b/ 两侧路径深度一致)。
  3. 在父目录用相对路径生成补丁:
    git diff --no-index --binary -- "up415/<v4.15.0目录>" "upstream/<v4.20.1目录>" > up.patch
    用绝对路径会让 patch 的 a/、b/ 前缀变成绝对路径,后续 git apply 直接找不到文件。
  4. 先空跑:git apply --check -pN up.patch
    N = a/ + 父目录名 + 解压目录名 的层数(两侧层数相同,本例各 1 段,故 -p3)。
  5. 真打补丁 → 编译 → 跑测试 → 提交前人工核对 diff,确认自有改动没被冲掉。

6. 边界(补丁法何时不成立)

  • 本地改动与上游改动落在同一文件:hunk 会 reject(Hunk #N FAILED)。改用 git apply -3 借 index 里的 blob 做三方合并,仍失败再手工合。
  • 上游重命名/移动了目录:--no-index 的 rename 检测弱,会表现为"删一个 + 加一个",需人工判断。
  • 上游动了依赖清单:同步后必须重新 go build / go mod tidy(或等价命令)验证。

7. 实例(2026-09-18,google-search-mcp-gemini-web2api)

  • 本地 fork 基线为上游 zexadev/gemini-web2api-go 的 v4.15.0,上游已发到 v4.20.1(落后 5 个版本)。
  • 本地自有改动集中在 apikey.go / mcp.go / mcp_test.go / app.go;上游 4.15→4.20.1 改的是 rotate.go / cookie_pool.go / gemini.go / admin_ui 等。两侧文件集不相交,因此补丁干净应用。
  • 验证链:git apply --check -p3 通过 → 真打 → go build 产出 25.5 MB 单二进制 → go test ./... 全绿。
  • 结论:此类 fork 的"升级"是打补丁,且改动文件集是否相交决定要不要人工介入。

8. 下次如何快速识别

  • 历史里见到一次性导入提交,就默认它是 unrelated histories,不要先试 merge。
  • 判断落后多少:GitHub compare v<当前版本>...main,返回 status: identical 说明该 tag 就是 main 最新;再用 git diff --no-index --stat 看具体落后内容。
  • 判断"本地是不是最新"最快的办法是直接比 const Version / pyproject.toml 等版本常量与上游最新 release,而不是读 CHANGELOG。

9. 相关


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Programming