场景

最近团队基于 Snowy 这个开源项目做二次开发,在它上面加了很多自己的业务功能。但 Snowy 也在持续更新——修 Bug、加新特性、升级依赖版本。

问题来了:自己的代码和上游代码混在一起,怎么才能既自由修改,又能定期拉取上游的最新更新?

这就是我写这篇文章要分享的。

思路

我的核心思路是双远程 + 上游镜像分支:

  • origin → 团队自己的远程仓库(日常开发用)
  • upstream → 开源项目的远程仓库(只读,仅用来拉取更新)
  • upstream-master 分支 → 上游代码的纯净镜像,绝不直接提交
  • main 分支 → 团队开发主分支,包含所有自定义代码
gitGraph commit id: "A" commit id: "B" branch upstream-master checkout upstream-master commit id: "C" commit id: "D" checkout main commit id: "E: 上游v3.6.5" branch feature/xxx checkout feature/xxx commit id: "F" checkout main merge feature/xxx

初始化仓库

进入项目目录

bash
1cd /your/project

初始化 Git

bash
1git init

配置两个远程仓库(以codeup和gitee为例)

bash
1git remote add origin git@codeup.aliyun.com:your-team/your-project.git
2git remote add upstream git@gitee.com:xiaonuobase/snowy.git

origin 是团队日常用的仓库,upstream 是 Snowy 的仓库(只拉取,不推送)。

拉取上游代码

bash
1git fetch upstream

这样就把 Snowy 的所有分支和标签拉到本地了。

sequenceDiagram participant Local as 本地仓库 participant Upstream as upstream (Gitee) Local->>Upstream: git fetch upstream Upstream-->>Local: branches: master, dev<br/>tags: v3.6.5, v3.6.4 ... Note over Local: remotes/upstream/master<br/>remotes/upstream/dev

创建上游镜像分支

我基于 upstream/master 创建了一个本地分支,用来纯净地跟踪上游代码:

bash
1git checkout -b upstream-master upstream/master

这个分支的作用:

  • upstream-master 是本地分支名
  • 这个分支永远只做 fast-forward 更新,我绝不在上面直接提交代码
  • 它是后续合并到 main 的桥梁

创建主开发分支

main 不能带上上游的 commit 历史,否则将来 merge 会把上游的 commit 全部混进团队仓库。我用 orphan 分支创建一个干净的起点:

bash
1git checkout --orphan main upstream-master

这样 main 和 upstream-master 虽然文件内容一样,但没有共同的 commit 历史。main 只包含一个独立的根提交,积累团队的修改;upstream-master 保持纯净。

然后 squash 合并上游代码作为一个提交:

bash
1git merge upstream-master --squash --allow-unrelated-histories
2git commit -m "chore: squash merge upstream v3.6.5"

第一步初始化完成后,本地仓库应该是这样的:

切回上游镜像分支

提交完切回 upstream-master,保持它在纯净状态:

bash
1git checkout upstream-master

日常开发

gitGraph commit id: "feat: 基础功能" branch feature/my-feature checkout feature/my-feature commit id: "feat: 审计日志" checkout main merge feature/my-feature commit id: "feat: 其他功能"

日常开发我直接在 main 上或从 main 切出功能分支:

bash
 1git checkout main
 2git pull origin main
 3git checkout -b feature/my-feature
 4
 5# 开发、提交
 6git add .
 7git commit -m "feat: 增加用户操作审计日志"
 8
 9# 推送到 origin
10git push -u origin feature/my-feature

在 Codeup / Gitee / GitHub 上创建合并请求(MR/PR),合入 main(或者本地合并提交也可,看团队情况)。

定期从上游同步(核心流程)

这是最关键的操作——把 Snowy 的最新代码合并到我的项目里。

flowchart TD A[git fetch upstream] --> B[checkout upstream-master] B --> C[git merge upstream/master --ff-only] C --> D[checkout main] D --> E[git merge upstream-master --squash] E --> F{有冲突?} F -->|是| G[解决冲突] G --> H[git commit] F -->|否| H H --> I[测试: mvn clean test] I --> J[git push origin main] J --> K[checkout upstream-master]

拉取上游最新代码

bash
1git fetch upstream

更新上游镜像分支

切到 upstream-master,用 fast-forward 方式追赶上源:

bash
1git checkout upstream-master
2git merge upstream/master --ff-only

--ff-only 确保只做快进合并——如果这个分支上有任何直接提交,合并会失败(这正是我想要的保护)。

gitGraph commit id: "A" commit id: "B" commit id: "C" branch upstream-master checkout upstream-master commit id: "D" commit id: "E" checkout main commit id: "F: squash upstream v3.7.0"

将上游更新合并到主分支

关键区别在这里:我用 --squash 把上游的更新压成一个提交,而非逐条合并上游的 commit 历史。

bash
1git checkout main
2git merge upstream-master --squash

为什么不直接用 git merge upstream-master?

默认 merge 会把上游仓库的所有 commit 历史都带到 main 上。几次同步之后,main 的提交图上就混杂着上游的几百个 commit 和团队的 commit,分不清哪些是团队写的。使用 --squash 后,每一次上游同步在 main 上只产生一个提交:

text
commit 2f3a1b8 (HEAD -> main)
     feat: squash merge upstream (同步上游 v3.7.0 的更新)

commit 8938cf4
     chore: initial team snapshot based on snowy v3.6.5

这里有可能产生冲突。 有冲突说明上游和团队改了同一个文件的同一段代码。需要逐文件手动确认,不能自动偏向某一方,才能确保合并后的代码逻辑正确。

解决冲突

用 git status 查看冲突文件:

bash
1git diff --name-only --diff-filter=U

对每个冲突文件:

  • 打开文件,找到 <<<<<<<、=======、>>>>>>> 标记
  • 理解双方改了什么,手动合并
  • 保存文件,git add 标记已解决

所有冲突解决完后:

bash
1git commit -m "feat: squash merge upstream (同步上游 v3.7.0 的更新)"

squash merge 不会自动生成合并提交,需要手动 commit。建议在提交信息里注明上游版本号,方便日后排查。

测试并推送

bash
1# 运行测试确保一切正常
2mvn clean test
3
4# 推送到 origin
5git push origin main

切回上游镜像分支

bash
1git checkout upstream-master

同步完成后,提交历史看起来像这样:

gitGraph commit id: "A" commit id: "B" branch upstream-master checkout upstream-master commit id: "C" commit id: "D" commit id: "E" checkout main commit id: "F: squash v3.7.0" commit id: "G"

完整命令速查

场景命令
初始化
初始化仓库git init
添加 origingit remote add origin <团队仓库地址>
添加 upstreamgit remote add upstream <开源仓库地址>
拉取上游git fetch upstream
创建上游镜像git checkout -b upstream-master upstream/master
创建主分支(orphan)git checkout --orphan main upstream-master
首次 squash 上游git merge upstream-master --squash --allow-unrelated-histories && git commit
日常开发
切功能分支git checkout -b feature/xxx main
推送功能分支git push -u origin feature/xxx
定期同步上游
拉取上游git fetch upstream
更新镜像git checkout upstream-master && git merge upstream/master --ff-only
合并到主分支git checkout main && git merge upstream-master --squash && git commit
推送到 origingit push origin main

提交时间线

每次同步就是:

  1. 把 upstream-master 快进到 upstream/master 的最新提交
  2. 用 --squash 把 upstream-master 合并到 main
gitGraph commit id: "A" commit id: "B" commit id: "C" branch upstream-master checkout upstream-master commit id: "D" commit id: "E" checkout main commit id: "F" commit id: "G" checkout upstream-master commit id: "H" commit id: "I" checkout main commit id: "J: squash upstream"

需要注意的事

  • upstream-master 上绝对不要提交,这是整个方案的基础。一旦提交了,--ff-only 会阻止合并,你就需要处理本不该出现的冲突
  • main 是 orphan 分支(无共同祖先),首次 merge 必须加 --allow-unrelated-histories
  • 合并冲突时,逐文件手动审查,不要偷懒用 -X ours 或 -X theirs 自动解决——自动策略不了解业务逻辑,可能引入隐性 Bug
  • 同步后务必跑一遍测试(mvn test),确认功能正常再推送
  • 多人协作时,同步上游前先确保 main 分支是干净的,没有未合并的 MR