$walker
GitLab · Git · 协作 · 博客

GitLab 双 Remote 协作:pull upstream、push origin、再提 PR

记录把 Astro 博客源码推上 GitLab 时踩到的几个坑——新仓自带无关的占位提交、受保护分支禁止 force push、以及 fork 模型下不应直推 upstream。附标准协作流程与速查命令。

发表于 2026-09-09约 4 分钟GitLabGit协作博客

把博客源码纳入版本控制时,我按自己的约定给仓库配了双 remote:一个个人 fork,一个组织仓。结果第一次推送就连踩三个坑。这篇把过程和正确姿势记下来,省得下次再交一遍学费。

为什么要双 Remote

个人项目我也习惯用 fork 模型:自己有一个 fork 随便折腾,组织仓作为“官方/规范”来源。约定如下:

origin    git@10.100.100.5:walker/astro-blog.git   # 我的 fork,日常推送目标
upstream  git@10.100.100.5:my-app/astro-blog.git   # 组织仓,协作与发布来源

配置用脚本一把梭,仓库名从目录推导:

bash "$HOME/.agents/skills/my-gitlab/scripts/configure-remotes.sh"
git remote -v
# origin    git@10.100.100.5:walker/astro-blog.git (fetch/push)
# upstream  git@10.100.100.5:my-app/astro-blog.git (fetch/push)

标准协作流程

fork 模型的核心纪律只有一句话:upstream 的 main 是 PR 目标分支,只通过合并 MR 接收变更,不要直接 push 它。

pull upstream  →  push origin  →  提 PR/MR
  (同步组织仓)      (只推自己 fork)   (origin/<branch> → upstream/main)

日常三步:

  1. git pull upstream main —— 先把组织仓同步到本地,避免分叉;
  2. 改完 git push origin main —— 只推到自己的 fork;
  3. origin/mainupstream/main 开 Merge Request,等 review 合并。

坑一:新仓自带“无关”占位提交

本地 git init 后提交了 37 个源码文件,信心满满去推。先 git fetch origin,GitLab 丢回来一句:

警告:没有共同的提交
来自 10.100.100.5:walker/astro-blog
 * [新分支]       main  -> origin/main

远端 origin/main59799a8 Initial commit,而且 git ls-tree 一看,里面只有 GitLab 建仓时自动生成的一个占位 README.md——和我的本地历史完全不相交

git rev-list --left-right --count main...origin/main
# 1       1     本地 1 个提交,远端 1 个提交,零共同祖先

坑二:受保护分支禁止 force push

既然历史无关,直觉是用 force push 把占位提交覆盖掉。按安全性首选 --force-with-lease

git push --force-with-lease origin main
# 报错:
# GitLab: You are not allowed to force push code to a protected branch on this project.

GitLab 默认保护 main 分支,force push 即使对仓库 owner 也是关闭的。这条规则拦得对——强推会抹掉远端历史,本就不该是常规操作。

绕行:合并占位 + 普通推送

不用解保护、也不用管理员开关。把占位提交作为祖先并入本地,再做一次普通推送即可——因为推送的新 tip 包含了远端旧 tip,受保护分支会正常接受。

git fetch origin
git merge origin/main --allow-unrelated-histories
# 冲突(添加/添加):README.md —— 两端都有 README

git checkout --ours README.md && git add README.md   # 保留本地项目版 README
git commit --no-edit                                  # 完成合并

git merge-base --is-ancestor origin/main HEAD && echo "OK: 远端占位已是本地祖先"
git push origin main                                  # 普通推送,无需 --force
# 59799a8..4025ec6  main -> main

合并后历史长这样,多出一个无害的 merge commit,但完全可追溯:

*   4025ec6 merge: 并入 GitLab 占位初始提交
|\
| * 59799a8 Initial commit   (远端原来的占位)
* e4c7443 feat: 初始化 Astro 静态博客源码

坑三:我直推了 upstream(违规)

双 remote 配好后,顺手把 upstream 也推了。结果被纠正——正常流程应该是 pull upstream、push origin、再提 PR,我这一步直接 git push upstream main 越过了 PR 环节,属于违规操作。

原因想通了就简单:

正确做法只有一条:任何变更都先落到 origin,再通过 MR 合进 upstream。只有“空仓一次性 bootstrap”这种极端情况才例外,而且即便那时也更推荐开 MR 而非直推。

速查表

场景 命令 说明
配双 remote configure-remotes.sh 自动设 origin / upstream
日常同步 git pull upstream main 先跟组织仓对齐
日常发布 git push origin main 只推自己的 fork
提变更 开 MR origin → upstream/main 不直推 upstream
新仓无关占位 merge --allow-unrelated-histories + 普通 push 别用 force
受保护分支 普通 push(占位已并入祖先) force 会被拒

小结

三个坑串起来就是一条经验:fork 模型下,upstream 是“被合入”的对象,不是“被推送”的对象。遇到受保护分支 + 无关占位提交,用“合并占位、普通推送”绕开,而不是去解保护做 force push。把这套写进了 my-gitlab 技能,下次直接照流程走。

博客源码现在落在四个地方:GitLab 的 walker/astro-blog(fork)、my-app/astro-blog(组织仓,仅经 MR 收变更)、GitHub Pages 构建产物、以及自托管的 blog.walkerhua.com