把博客源码纳入版本控制时,我按自己的约定给仓库配了双 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)
日常三步:
git pull upstream main—— 先把组织仓同步到本地,避免分叉;- 改完
git push origin main—— 只推到自己的 fork; - 从
origin/main向upstream/main开 Merge Request,等 review 合并。
坑一:新仓自带“无关”占位提交
本地 git init 后提交了 37 个源码文件,信心满满去推。先 git fetch origin,GitLab 丢回来一句:
警告:没有共同的提交
来自 10.100.100.5:walker/astro-blog
* [新分支] main -> origin/main
远端 origin/main 是 59799a8 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 环节,属于违规操作。
原因想通了就简单:
upstream/main是别人(或未来的你)发起 MR 的目标分支,受 review 保护;- 直接强推/直推它,等于绕过了整个协作闸门;
- 即使是个人项目、两个仓都是自己所有,也不该养成这个习惯。
正确做法只有一条:任何变更都先落到 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。