搞 Git 久了你会发现,单远程仓库很多时候根本不够用。特别是这两年做开源项目、公司内部代码镜像,或者是自己需要把一份代码同时维护到 GitHub 和国内代码托管平台,最头疼的就是每次提交完代码都要 push 两遍,稍不留神就漏推一个,两边代码不同步,后面在另一台机器上拉下来一跑,直接一脸懵。这篇教程就是把“多个远程仓库怎么推”这件事彻底讲透,我整理了两种主流方案,从原理到命令到踩坑记录都会写到,不管你是刚接触 Git 的新手,还是已经写过不少仓库的熟人,都能直接用上。
1. 需求拆解与方案选型
1.1 为什么你需要同时推送到两个远程仓库
先说说我遇到这个需求的真实场景。去年我要维护一个开源的小组件,GitHub 上放一份给海外用户提 issue 用,国内另一个托管平台上也要放一份,方便国内同事拉代码不卡。一开始我还老老实实git push一次,再git push另一次,结果有一次我在晚上十一点半提交完代码只推了 GitHub,第二天同事从国内平台拉代码发现还是旧版本,排查了好久才发现是我漏推了。
除了开源分发,双远程最常见的用途还有这么几类:
- 备份安全:代码托管平台偶尔也会出状况,把仓库镜像到两个不同平台,相当于给代码上了一道保险。比如主平台挂了,另一个平台还能顶上继续发布流程。
- 访问加速:公司团队分布在国内外,不同地区访问不同托管平台的延迟差异很大,双远程可以让大家各拉各的,谁都不耽误。
- CI/CD 解耦:有些平台构建速度快,有些平台支持特定的部署插件,把代码推给两边,可以在两个平台分别跑各自的流水线,哪个成功用哪个。
- 协作隔离:技术部和业务部可能分别用自己的内部 Git 服务,但代码库是同一个,开发者提交一次,两个团队都能收到更新。
所以说,双远程不是一个冷门的奇技淫巧,它是真实协作场景里相当刚需的操作,用的人越多,越需要把方法讲明白。
1.2 两条实现路线:多 remote 与多 pushurl
实现“一次本地仓库推送到两个远程仓库”,最根本的办法是在.git/config里做文章。Git 本身允许一个本地仓库关联多个远程仓库,这个大家可能都知道,git remote 可以 add 很多个。但是,每次 git push 默认只会推给当前分支追踪的那个 remote,也就是 origin,你还得手动指定第二个 remote 再推一次,命令是两条,漏推的风险就在这里。
另一条路是把多个 push 地址挂到同一个 remote 下面,让一次git push origin main自动同时推给两个地址。这种做法的原理是 Git 支持一个 remote 有多条 pushurl,只要配置好,push 时 Git 会挨个把代码推到这些地址上,不需要你多敲一条命令,完全避免了漏推。
选哪条路取决于你的使用习惯。如果你希望推 GitHub 和推国内平台是两件事,可以分开控制,那多 remote 更直观;如果你希望“推送”这个动作本身就是幂等的、一次完成的,那么多 pushurl 明显更爽。我实际用了大半年之后,坚定地站在多 pushurl 这边,理由下文详细说。
1.3 约定本文的演示环境
为了让后面的操作步骤可复现,我先约定一下演示环境。本地仓库名就叫my-project,远程托管平台用 GitHub 和 Gitee(你也可以换成 GitLab、公司内网服务器,命令完全一样)。假设你已经把项目 clone 到本地,远程仓库的 HTTPS、SSH 地址分别是:
https://github.com/yourname/my-project.git https://gitee.com/yourname/my-project.git下面所有命令里的yourname,请替换成你自己的用户名或组织名。如果不太确定自己的远程地址,可以在托管平台仓库首页找到“克隆/下载”按钮,那里会显示完整地址。从 GitHub 或 Gitee 克隆项目时,默认 remote 名都叫 origin,这个后面会用到。
2. 方案一:多 remote 分次推送,简单但容易漏
2.1 添加远程仓库的标准操作
不管你现在是刚git clone出来的仓库,还是本地git init创建的仓库,思路都一样。先看一下当前有哪些远程:
git remote -v如果你的仓库是从 GitHub 克隆下来的,输出应该长这样:
origin https://github.com/yourname/my-project.git (fetch) origin https://github.com/yourname/my-project.git (push)现在我们把国内平台的地址加进来:
git remote add gitee https://gitee.com/yourname/my-project.git这里我给第二个远程起名gitee,纯粹是图好记。叫什么完全看自己,比如backup、mirror、internal都行。起完名之后再看一次:
git remote -v输出会变成:
origin https://github.com/yourname/my-project.git (fetch) origin https://github.com/yourname/my-project.git (push) gitee https://gitee.com/yourname/my-project.git (fetch) gitee https://gitee.com/yourname/my-project.git (push)这时你的本地仓库里已经有了两个远程仓库,各自独立。
2.2 日常推送时怎么做
接下来推送的时候,有几种写法:
# 只推给第一个远程 git push origin main # 只推给第二个远程 git push gitee main # 一次推给所有远程(逐个写) git push origin main git push gitee main如果你图省事,也可以利用 Git 的--all和--tags:
git push --all origin git push --all gitee注意,--all推的是所有本地分支,不是所有远程仓库。Git 从设计上就没有“一条命令推给所有 remote”的默认动作,多 remote 方案下你始终需要为每个 remote 执行一次 push。这就是这种方案最明显的短板:命令多,而且两条命令之间没有任何原子性。第一条成功了、第二条因为网络或者权限失败,是非常常见的事。
不过多 remote 方案也有它独特的价值。比如你想刻意让国内平台只接收release分支、不让它收到你日常的dev提交,那你可以在推gitee时精确指定分支,达到“两个远程仓库内容不完全一致”的效果。这个能力是多 pushurl 方案不太好实现的。所以我的建议是:如果你确实需要两边内容不同、或者推送节奏不同,用多 remote 没问题;如果你的诉求是“两边一模一样,只是多一份镜像”,继续往后看多 pushurl。
2.3 改名、删远程仓库的常见操作
多 remote 方案里,远程仓库的管理频率还挺高的。常用的操作我列一下:
# 给远程仓库改名 git remote rename gitee backup # 删除一个远程仓库(不会影响本地和已推过的远端内容) git remote remove backup # 修改某个远程仓库的地址 git remote set-url gitee https://new-address.git尤其要注意,git remote remove只是删除本地对某个远程的引用,不会去线上删除那个仓库,所以别怕。真正的风险反而是反向的:你本地的git remote -v看起来只有一个 origin,但托管平台上其实已经建了好几个仓库,push 的时候选错地址,内容就进错仓库了。所以每次 clone 完新仓库,第一步最好先git remote -v确认来源。
3. 方案二:多 pushurl 一条命令推送,同步双发不遗漏
3.1 核心原理:pushurl 是怎么生效的
多 pushurl 之所以能一条命令推两个地址,关键在于 Git 的 remote 配置支持pushurl字段。我们先回想一下,一个 standard remote 配置里最常见的是这样:
[remote "origin"] url = https://github.com/yourname/my-project.git fetch = +refs/heads/*:refs/remotes/origin/*这个url字段同时承担两个作用:拉取时从它这里 fetch,推送时也推给它。但 Git 同时允许你单独定义pushurl,一旦定义了pushurl,推送时就不再看url,而是只看pushurl。更关键的是,pushurl可以写多个,每个都代表一个推送目标。你执行一次git push origin main,Git 会逐个把分支推到所有pushurl上去。
这个设计初看会觉得很绕,但实际非常好用。拉取永远只走url那一个地址,推送却可以自由定义多个出口,对“我想让所有 push 都双发、但 pull 始终从主仓库拉”这种需求来说,简直是量身定做的。
3.2 具体配置步骤,照着抄就行
假设你本地已经 clone 了 GitHub 上的仓库,origin 指向的是 GitHub。现在执行以下命令,把 Gitee 地址追加到 origin 的推送目标里:
git remote set-url --add --push origin https://gitee.com/yourname/my-project.git如果你是从零开始配置一个本地仓库,还没克隆,那么先手动添加主远程:
git remote add origin https://github.com/yourname/my-project.git然后再执行上面那条set-url --add --push。
配置完之后,用git remote -v验证一下,输出应该是这样:
origin https://github.com/yourname/my-project.git (fetch) origin https://github.com/yourname/my-project.git (push) origin https://gitee.com/yourname/my-project.git (push)注意看,fetch 只有一行,push 有两行。这说明拉取还是从 GitHub 拉,推送则会同时发往 GitHub 和 Gitee。到这一步,日常推送只需要执行:
git push origin main代码就会同时进两个远程仓库了。如果你的主分支不叫main而是master,把命令里的分支名替换掉就行。
3.3 修改与删除 pushurl 的正确姿势
如果你想换掉其中一个推送地址,不要直接再执行一次set-url --add,那样会新增第三个地址。正确的做法是重设整个 pushurl 列表。Git 1.7.10 之后可以用--push来操作 pushurl,但如果你想清掉列表,最干净的方式是直接编辑.git/config文件。
用你熟悉的文本编辑器打开.git/config,找到 origin 那一节,内容多半是:
[remote "origin"] url = https://github.com/yourname/my-project.git fetch = +refs/heads/*:refs/remotes/origin/* pushurl = https://github.com/yourname/my-project.git pushurl = https://gitee.com/yourname/my-project.git想要改地址,就手动改对应的 pushurl 行;想要删除一个推送目标,删掉那一行就行。改完之后,git remote -v再看一眼,确认状态符合预期。我个人更推荐直接编辑 config 文件,因为 Git 命令行的参数组合太容易记混了,而且配置文件里一目了然,出问题也方便排查。
如果你不想用笨办法,也可以用命令重设:
# 先用 --delete 删掉某个 pushurl git remote set-url --delete --push origin https://gitee.com/yourname/my-project.git # 再重新添加 git remote set-url --add --push origin https://new-address.git3.4 push 时常用的分支参数
多 pushurl 配置好之后,最简单的git push origin main能覆盖掉绝大多数场景。但如果你工作在多个分支上,建议掌握下面几个写法:
# 推送当前分支到所有远程,并设置与 origin 的追踪关系 git push -u origin HEAD # 推送所有本地分支 git push --all origin # 推送所有标签 git push --tags origin还有一点容易被忽略:当本地分支和远程分支名不一致时,最好显式指定:
git push origin HEAD:release/v1.0这条命令的意思是把当前 HEAD 推送到远程的release/v1.0分支。因为pushurl是挂给 origin 的,所以只要你推的是 origin,所有 pushurl 都会收到这次更新。
3.5 为什么我更推荐 pushurl 方案
用久了你会发现,pushurl 方案从根本上消除了“漏推”这个人为因素。你只要养成git push origin的习惯,代码就会按约定进入所有目标仓库,不需要记住第二个仓库的名字,也不需要调整自己的肌肉记忆。
另外,配置一次之后,其他人 clone 你这个本地仓库时并不会把 pushurl 带过去,因为.git/config本来就不会被提交。所以这个配置是纯本地行为,不会意外传染给团队其他人,很安全。
当然,它也有代价:所有 push 都会双发,如果你网络到某一个平台一直很差,push 的耗时会被那个最慢的地址拖住。后面我会讲怎么在这种情况下降级处理。
4. 实操细节与扩展经验
4.1 HTTPS 与 SSH 的协议选择
双远程配置本身不挑协议,HTTPS 和 SSH 都能用。但实操中两个平台混用协议时经常会遇到“怎么这边要求输密码,那边又不需要”的困惑。
如果你用的是 HTTPS,GitHub 从 2021 年 8 月起就不再允许用账号密码直接 push,必须使用 Personal Access Token。Gitee 这边则可以在安全设置里开启私人令牌,也可以直接用账号密码,但为了安全还是建议用令牌。在 push 时如果提示输入用户名密码,密钥就是你的 token,不是登录密码,这是很多人第一次配置时最容易卡住的地方。
如果你用的是 SSH,倒不用记 token,但是你得把公钥在两边的账号设置里都添加一遍。具体操作是本地看~/.ssh/id_rsa.pub或者用下面的命令复制公钥内容:
cat ~/.ssh/id_rsa.pub然后把这段内容分别添加到 GitHub 的 SSH keys、Gitee 的 SSH 公钥里。两边都配好之后,push 就完全免密,这也是我推荐的方式。如果你两边都用了 SSH 且在同一台机器上,一般不会有冲突,因为 SSH 会按 host 自动选择对应密钥,GitHub 和 Gitee 的 host 不同,Key 可以重复或不同都行。
4.2 不同托管平台的合流与分流
双远程用得久了,很多朋友会想在“两个远程内容完全一致”和“两个远程内容不完全一致”之间灵活切换。这时我建议你给远端做不同的命名规划,比如origin始终是主远程,mirror是镜像远程。然后根据情况决定:
- 双向同步:两个远程都作为正式仓库,用多 pushurl 双发。
- 单向镜像:主远程正常开发,镜像远程只接收主远程的稳定分支或标签。这种场景下,镜像远程可以用多 remote 方案,手动按需 push,或者写个 CI 脚本定期同步。
另外要注意,如果两个托管平台的钩子、Webhook 配置不同,你推过去同一份代码,可能触发不同的构建任务。例如 GitHub 上配了 Actions,Gitee 上配了自家流水线,两边构建结果可能因为环境差异而有细微不同,这不是 Git 的锅,排查时别走偏了。
4.3 结合脚本实现推送后自动校验
多 pushurl 虽然解决了漏推,但网络原因导致其中一个推送失败时,Git 会直接报错。默认情况下,Git 在 push 时遇到一个地址失败,就会停止并返回非 0 退出码,你其实能够感知到有问题。可问题在于:有些人习惯 push 完看到一堆输出就直接切走,根本没看最后有没有error:或者failed。
我自己的做法是写一个简单的 shell 函数,push 完之后自动校验两个远程的 commit 是否一致:
function gp() { git push origin "$@" || return 1 git ls-remote origin > /dev/null || return 1 }这里的git ls-remote会向远端发一个引用查询,相当于试探远端是否还活着。如果 push 成功但某个远端掉线,这个命令马上会暴露问题。更彻底的办法是用for循环遍历两个仓库做git ls-remote对比,但大多数场景下“push 返回码 + 快速可达性检查”已经足够。
4.4 标签(tag)与子模块的特殊处理
如果你平时用git tag打版本,记住git push origin main不会把标签也推上去,标签需要单独推:
git push origin --tags在多 pushurl 配置下,git push origin --tags同样会把标签双发到两个远程。这里有一个坑:如果你本地打过同名 tag,而两个远程仓库对该 tag 的处理方式不一致,push 时就会报tag already exists或者被迫覆盖。我的建议是 tag 的命名和管理一定要统一,发布版本时先确认两个远程仓库里没有同名 tag,再执行推送。
如果是子模块项目,情况要复杂一些。子模块仓库本身有自己的远程,主仓库的多 pushurl 配置不会自动传递给子模块。你需要进入子模块目录,单独为它配置多 pushurl,或者先推送子模块,再推送主仓库。忘了这一步,同事拉下主仓库时会发现子模块引用失效,整个项目根本起不来。
5. 常见问题与排查技巧实录
5.1 怎么确定当前是否真的配好了多 pushurl
配置双远程之后,最容易出现的疑问是“我是不是配好了?”最简单的判断方式就是git remote -v,注意看 push 行的数量。如果 push 行只有一条,说明你的set-url --add并没有生效,这时候请打开.git/config文件看一下。我遇到过的典型错误是:
git remote set-url --add origin https://gitee.com/yourname/my-project.git少写了一个--push。这样不是不行,但它会把第二个地址加到url列表里,导致拉取也可能从两个仓库随机拉(实际 Git 会用第一个,但语义已经不对了)。所以你一定要养成配完就看git remote -v的习惯,确认 fetch 行只有一条、push 行有两条,才算配置正确。
5.2 push 时报错 401、403 或提示登录失败
这个真的遇到过太多次了。输入用户名密码正确,却一直提示认证失败,多半是密钥的问题。GitHub 上如果你用 HTTPS,密码位置必须填 Personal Access Token;Gitee 上如果开启了两步验证,也可能需要专用密码。两种平台的令牌都要在账号设置里自己生成,生成时记得勾选repo相关的权限。
如果是 SSH 方式报权限拒绝(Permission denied (publickey)),先去确认公钥加没加对。可以用下面命令测连通性:
ssh -T git@github.com ssh -T git@gitee.comGitHub 会回一句Hi yourname! You've successfully authenticated,Gitee 也会返回欢迎信息。如果你的密钥没有加载,执行ssh-add ~/.ssh/id_rsa再试一次。
5.3 一个远程推送成功,另一个失败,收尾怎么办
多 pushurl 场景下,如果网络抖动导致第一个成功、第二个失败,本地 Git 会返回失败状态,但第一个仓库收到更新这一点无法回滚。此时不要直接重跑git push,因为第一个仓库会提示“Everything up-to-date”或“Already up-to-date”,第二个仓库则会正常补推,结果虽然两边最终一致,但过程比较吓人。
那有没有办法快速定位“谁成功、谁失败”?Git 输出里会明确写出To https://gitee.com/...和具体的错误行,你盯一下error:关键字就行。担心自己看漏的,就再执行一次:
git push origin --porcelain--porcelain会输出机器可读状态,成功还是失败一眼就能看出来。
5.4 变基(rebase)失败后,怎么和远程保持一致
很多人在多远程场景下用git pull --rebase,结果报一堆冲突,甚至出现“变基到远程仓库失败”这种错误。这通常是本地提交和远程分支历史不一致导致的。如果你确认本地不想要之前那些提交,只想强行对齐远程分支,可以用:
git fetch origin git reset --hard origin/main这条命令会把本地分支硬重置到远程 main 分支的最新状态,所有本地未推送的提交都会被丢弃,所以执行前务必想清楚。重置之后再git push origin main --force-with-lease推上去,就能解决历史不一致的问题。切记不要用--force,因为它在多人协作时容易覆盖别人的提交,--force-with-lease至少会先检查远程有没有别人新推的内容,安全得多。
5.5 不小心把本地改动推送到了错误仓库
多 pushurl 下,只要配好了,push 就是双发,不太存在“推到错误仓库”的问题。但多 remote 方案下这种失误很常见,比如你明明想推 GitHub,结果命令写成了git push gitee dev,把 dev 分支推上来了。如果推错了分支且远程还没有人拉取,最直接的修复方法是:
git push gitee :dev这条命令会把远程的 dev 分支删除,相当于撤销。如果分支上有别人已经拉取的提交,删除分支可能造成混乱,那你就得靠git revert或者重新推送一个修正版本来处理了。
5.6 关于配置文件备份的一个小建议
.git/config是本地仓库里极易被忽略但特别重要的文件。双远程配置、branch 追踪关系、用户信息都可能写在里面。我之前有一次不小心跑了某些 IDE 的自动化操作,把 config 改坏了,重新配了半天。现在我的习惯是:在配置完双远程并验证成功之后,顺手备份一份:
cp .git/config ~/bak-my-project-git-config以后哪怕配置弄乱了,直接复制回来就行,省去重新推敲细节的时间。
6. 一点实操体会
文章写到这里,核心内容已经全部分享完了。如果让我给你一句最中肯的建议,那就是:日常开发主力仓库用多 pushurl 一条命令双发,特殊场景需要区分推送内容时临时改用多 remote 手动控制。两种方案不是互斥的,甚至可以在同一个仓库里共存,你可以让origin配成双 pushurl,再额外git remote add mirror一个只推指定分支的远程,灵活度很高。
我个人在实际操作中最受益的一点,是把双推送当成默认习惯而不是特殊情况来处理。配置一次之后,我再也不用记着“这个仓库推完 GitHub 还要推 Gitee”,也不用担心哪个同事漏推了镜像仓库导致两边代码对不上。Git 本身的设计就给这种场景留好了口子,只要顺着它的配置语义来用,整个流程会非常顺滑。希望这篇教程能帮你把多个远程仓库的安排理顺,省下以后每次 push 前都要犹豫的那几秒钟。