Skip to content

同步一个Fork

约 2009 字大约 7 分钟

Git

2023-08-04

    1. 给 fork 配置一个 remote
#使用 `git remote -v` 查看仓库状态
git remote -v
# origin  https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch)
# origin  https://github.com/YOUR_USERNAME/YOUR_FORK.git (push)

# 添加上游仓库地址
git remote add upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git


# 再次查看状态确认是否配置成功。
git remote -v
# origin    https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch)
# origin    https://github.com/YOUR_USERNAME/YOUR_FORK.git (push)
# upstream  https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git (fetch)
# upstream  https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git (push)
    1. 拉取上游仓库 fetch 分支和提交点,传送到本地,并会被存储在一个本地分支 upstream/master
git fetch upstream
git fetch upstream <分支> # 拉取指定分支
# remote: Counting objects: 75, done.
# remote: Compressing objects: 100% (53/53), done.
# remote: Total 62 (delta 27), reused 44 (delta 9)
# Unpacking objects: 100% (62/62), done.
# From https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY
#  * [new branch]      master     -> upstream/master
    1. 把 upstream/master 分支合并到本地 master 上,这样就完成了同步,并且不会丢掉本地修改的内容。有冲突的话合并一下
git merge --squash upstream/master # 在主分支时合并,不合并上游分支的提交历史
# Updating a422352..5fdff0f
# Fast-forward
#  README                    |    9 -------
#  README.md                 |    7 ++++++
#  2 files changed, 7 insertions(+), 9 deletions(-)
#  delete mode 100644 README
#  create mode 100644 README.md
# 有可能会出现fatal: refusing to merge unrelated histories的问题
git merge forkmerge
# 参数:
	--allow-unrelated # 解决fatal: refusing to merge unrelated histories,分支已经分叉并且没有共同的提交或历史记录
	--squash # 不保留commit历史


#简单点的 无冲突的情况下合并之后直接push 不commit不会产生合并记录
git merge upstream/master
git push
  • 如果想更新到 GitHub 的 fork 上,直接 git push origin master 就好了。

说明

1. 不会产生新提交记录的情况(Fast-Forward 合并)

git checkout master
git fetch upstream/v2
git merge upstream/v2
git push --set-upstream origin master
  • 前提:如果你的本地 main 分支和 upstream/main 分支之间没有分叉(即你的本地 main 分支的提交历史完全是 upstream/main 的子集),那么 git merge upstream/main 会执行 Fast-Forward 合并

  • 结果

    • 本地 main 分支的 HEAD 指针会直接移动到 upstream/main 的最新提交。
    • 不会生成新的合并提交(Merge Commit)。
    • 推送 git push origin main 时,origin/main 只是更新到与 upstream/main 相同的提交,没有额外的提交记录。
  • 提交历史示例

    A --- B --- C (upstream/main, origin/main, local main)

    在这种情况下,历史保持线性,没有新提交。

2. 会产生新提交记录的情况(Merge Commit)

git checkout master
git fetch upstream/v2
git merge upstream/v2
git commit -m '有不同分支的时候,会被创建新的提交记录'
git push --set-upstream origin master
  • 前提:如果你的本地 main 分支上有自己的提交(例如你在 main 上修改并提交了内容),而这些提交不在 upstream/main 中,那么 git merge upstream/main 会创建一个 合并提交(Merge Commit)。

  • 结果

    • Git 会将 upstream/main 的更改合并到本地 main,生成一个新的合并提交。
    • 这个新提交会包含两个父提交:一个指向本地 main 的最新提交,一个指向 upstream/main 的最新提交。
    • 推送 git push origin main 时,这个合并提交会被推送到 origin/main,从而产生新的提交记录。
  • 提交历史示例

    A --- B (upstream/main) \ C --- D (local main 前状态) \ M (local main 合并后,M 是合并提交)

    这里 M 是新的合并提交。


3. 如何判断是否会产生新提交?

在执行 git merge upstream/main 之前,可以通过以下方式预判:

  • 查看分支状态:
    git fetch upstream 
    git log main..upstream/main
    • 如果输出为空,说明 main 已包含 upstream/main 的所有提交,合并将是 Fast-Forward。
    • 如果有输出,说明 upstream/main 有新提交需要合并。

新方案

会!这是最常见的问题。当你用 --orphan 创建干净分支后,你的分支和上游分支没有共同祖先,Git 会拒绝合并。

为什么会出现

你的历史:    A (clean-main, 你的第一个commit)
上游历史:    o---o---o---...---Z (upstream/main, 几万个commit)

            完全不同的起点,无共同祖先

Git 默认拒绝合并没有关联的历史,防止意外合并错误的内容。


解决方案

方案 1:强制允许无关历史(推荐,最简单)

git fetch upstream

# 关键:添加 --allow-unrelated-histories
git merge upstream/main --squash --allow-unrelated-histories -m "Sync: Upstream update"

Squash + --allow-unrelated-histories 的组合效果:

  • ✅ 允许合并没有共同祖先的历史
  • ✅ 但结果仍然是 1 个干净的 commit(因为 --squash 不保留上游的提交历史)
  • ✅ 下次同步时,你的 main 和 upstream/main 仍然没有共同祖先,需要继续用 --allow-unrelated-histories

方案 2:建立"假"共同祖先(更优雅,长期更好)

如果你不想每次都用 --allow-unrelated-histories,可以人为制造一个连接点:

# 1. 先创建你的干净分支
git checkout --orphan clean-main
git add -A
git commit -m "Initial: Based on upstream v1.0"

# 2. 获取上游的第一次 commit(作为"假祖先")
git fetch upstream

# 找到上游的第一个 commit
FIRST_COMMIT=$(git rev-list --max-parents=0 upstream/main)
echo "上游第一个 commit: $FIRST_COMMIT"

# 3. 变基到上游第一个 commit 上(制造关联)
git rebase --onto $FIRST_COMMIT --root

# 现在你的第一个 commit 的 parent 是上游的第一个 commit
# 历史看起来: 上游第一个 commit → 你的干净 commit

效果:

历史现在:    [上游第一个commit] --- A (你的干净commit)

             也是 upstream/main 的祖先之一

这样之后 git merge upstream/main 就不需要 --allow-unrelated-histories 了。

方案 3:使用 git replacegit commit-tree(高级)

完全伪造一个合并关系,但过于复杂,一般不推荐。


推荐的工作流(整合解决方案)

如果你选择方案 1(每次用 --allow-unrelated-histories)

#!/bin/bash
# sync-upstream.sh

set -e
cd /path/to/your/repo

echo ">>> Fetching upstream..."
git fetch upstream

echo ">>> Squash merging upstream changes..."
# 必须加 --allow-unrelated-histories
git merge upstream/main --squash --allow-unrelated-histories \
    -m "Sync: Merge upstream changes ($(date +%Y-%m-%d))"

echo ">>> Done. Check status and commit:"
git status

特点:

  • 简单直接,每次同步都是 1 个命令
  • 需要记住加 --allow-unrelated-histories
  • 你的历史永远只有你自己的 commits + 定期的 squash 合并 commits

如果你选择方案 2(建立假祖先,推荐长期使用)

# ===== 初始化(只做一次)=====

git clone https://github.com/YOUR_USERNAME/repo.git
cd repo
git remote add upstream https://github.com/ORIGINAL/repo.git

# 获取上游完整历史
git fetch upstream

# 找到上游第一个 commit
FIRST=$(git rev-list --max-parents=0 upstream/main)

# 创建 orphan 分支
git checkout --orphan temp-clean

# 添加你的初始代码
git add -A
git commit -m "Initial: Clean base from upstream"

# 变基到上游第一个 commit 上,建立关联
git rebase --onto $FIRST --root

# 强制推送到 main
git branch -D main
git branch -m main
git push -f origin main

# ===== 以后同步(不需要 --allow-unrelated-histories 了)=====

git fetch upstream
git merge upstream/main --squash -m "Sync: Upstream update $(date +%Y-%m-%d)"

常见问题排查

Q: 用了 --allow-unrelated-histories 但还是报错?

# 检查是否真的 fetch 了上游
git remote -v
git branch -r  # 应该能看到 upstream/main

# 如果没有,重新添加 remote
git remote remove upstream
git remote add upstream <正确的URL>
git fetch upstream

Q: Squash 合并后,下次同步有冲突?

这是正常的。因为每次 squash 合并后,你的 main 和 upstream/main 的内容虽然相同,但提交历史不同。

# 解决冲突的标准流程
git merge upstream/main --squash --allow-unrelated-histories

# 会有冲突提示,解决后
git add -A
git commit -m "Sync: Upstream update with conflict resolution"

Q: 想要更激进的方案 - 完全替换历史但保留同步?

# 使用 git worktree 双轨制
# 你的干净仓库:只放你的代码
# 另一个目录:完整上游仓库,用于查看和 cherry-pick

# 创建上游工作区
git worktree add ../upstream-work upstream/main

# 需要某个上游功能时,手动 cherry-pick 或复制文件
cd ../upstream-work
git log --oneline -20  # 找想要的 commit
cd ../your-repo
git cherry-pick <commit-hash>  # 或手动合并文件

最终建议

场景推荐方案
快速开始,不介意每次打长命令方案 1 --allow-unrelated-histories
长期维护,想要标准 Git 工作流方案 2 建立假祖先
极少同步上游,主要独立开发双工作区手动 cherry-pick

最实用的快速命令:

# 初始化
git checkout --orphan clean-main && git add -A && git commit -m "init" && git push -f origin clean-main:main

# 每次同步(记住这个就行)
git fetch upstream && git merge upstream/main --squash --allow-unrelated-histories -m "sync: $(date +%Y-%m-%d)"