维护层,核查于 2026-09-01。 文末逐字保留了 2019 年的简短命令笔记。本文区分“同一个逻辑仓库的备用推送地址”和“两个真正独立的托管仓库”,并把双目标推送视为复制流程,而不是一个原子事务。
Table of Contents
2026 维护指南
Git 可以把一次推送发送到某个远程仓库配置的每一个 pushurl。因此原文的方法确实可用,但“同时”容易让人误解:Git 会分别连接各目标,一个目标可能已经成功,另一个才失败。若使用两个独立的托管服务,分别命名远程仓库通常更清晰、更安全。
以下示例使用 main 分支。请替换成真正需要发布的分支,并显式写出分支名,避免仓库内的 push.default 或上游设置改变结果。
选择正确的模型
| 场景 | 推荐模型 | 原因 |
|---|---|---|
| 两个经过认证、且管理员保证指向同一逻辑仓库的端点 | 在一个远程仓库上配置多个 pushurl | 一个远程名称可以使用全部写入端点,同时保留一个 fetch URL。 |
| 两个独立仓库、托管商、凭据或策略集合 | 使用 primary、mirror 等独立远程名称 | 每个目标都能独立检查、推送、重试和校验。 |
这一区别直接来自官方的 `git remote` 文档:同一远程仓库的 push URL 与 fetch URL 应指向同一位置;若从一个位置获取、向另一个位置发布,应使用两个远程仓库。`git push` 文档也确认,一次推送会作用于所有已定义的 push URL。
方案 A:一个远程仓库配置多个 Push URL
假设 origin 已存在。先设置它的 fetch URL,再添加每一个预期的推送端点,包括主端点:
git remote set-url origin ssh://git@read.example/team/project.git
git remote set-url --add --push origin
ssh://git@write-a.example/team/project.git
git remote set-url --add --push origin
ssh://git@write-b.example/team/project.git
只要存在任意 pushurl,Git 就会使用这份 push URL 列表,不再回退到 fetch URL。因此若只添加第二个端点,后续推送反而会漏掉主端点。
分别检查 fetch 与 push 配置:
git remote get-url --all origin
git remote get-url --all --push origin
删除一个 push URL 时要注意,最后一个参数是正则表达式。使用首尾锚点可以避免意外匹配过宽:
git remote set-url --delete --push origin
'^ssh://git@write-b[.]example/team/project[.]git$'
如果有意清除全部自定义 push URL,Git 会重新使用该远程仓库的 fetch URL 来推送:
git config --unset-all remote.origin.pushurl
不要为了修改 URL 而使用 git remote rm origin:它会删除整个远程仓库配置及相关的跟踪设置。
方案 B:不同主机使用独立远程仓库
对于独立仓库,为每个目标取一个有意义的名字:
git remote add primary ssh://git@code-a.example/team/project.git
git remote add mirror ssh://git@code-b.example/team/project.git
git remote get-url --all --push primary
git remote get-url --all --push mirror
如果某个名称已经存在,请先检查,再用 git remote set-url <name> <url> 修改,而不是重复添加。独立名称能明确呈现不同的凭据、分支规则、错误日志和重试,不必把这些差异藏在 origin 后面。
预演、推送与校验
针对每个独立目标预演确切的分支:
git push --dry-run --porcelain primary main
git push --dry-run --porcelain mirror main
--dry-run 会执行除真正发送 ref 更新之外的步骤;--porcelain 则提供稳定、便于机器读取的状态行。预演很有用,但它既不是成功名额的预留,也不能证明稍后的真实推送会通过每一项服务器端规则。
按明确的顺序推送。使用 && 后,如果主仓库失败,镜像推送不会执行:
git push primary main &&
git push mirror main
如果第二条命令失败,第一次推送不会回滚。应记录每个目标的状态,只重试落后的目标。
推送后,对比本地提交 ID 与各服务器公开的分支 ID:
git rev-parse main
git ls-remote --exit-code --branches primary refs/heads/main
git ls-remote --exit-code --branches mirror refs/heads/main
两条 ls-remote 输出的第一列都应等于 git rev-parse main 的结果。在自动化流程中应精确比较这些值;只要任一 ref 不存在或不同,就让任务失败。
原子性与部分失败
git push --atomic 请求同一台接收服务器要么更新此次推送中的所有 ref,要么全部不更新;如果该服务器不支持原子推送,操作会失败。它无法把两条传输连接或两台主机合成一个事务。无论目标用多个 push URL 表示,还是用独立远程仓库表示,这一点都不变。
在 Git 2.43.0 上使用一次性本地仓库测试,复现了实际的失败模式:
git push --dry-run访问了两个本地 push URL,但没有更新任何一个裸仓库。- 真实推送把两个仓库更新到了同一提交。
- 随后保留第一个有效 URL、把第二个改成不存在的仓库;第一个仓库已更新,第二个失败,
git push以状态码 128 退出。
不同传输方式的具体错误与退出码可能不同,但部分成功的风险始终存在。稳健的复制任务必须发现并消除分歧;添加 --atomic 并不会产生跨主机回滚。
分支、标签与 Mirror 模式
明确指定 ref 范围:
git push <remote> main只发布main。git push <remote> --all发布所有本地分支,但不包含标签。git push <remote> --tags发布所有标签,以及命令中显式列出的 refspec。git push <remote> --mirror镜像refs/下的所有 ref,强制更新已变化的 ref,并删除远端存在而本地不存在的 ref。
--mirror 是具有破坏性的同步,并不是“做个备份”的同义词。只有在完整 ref 范围和删除策略都经过审查、且目标确实是受控镜像时才应使用。普通发布应推送指定分支,并且只分发确实需要的标签。
凭据与服务器端策略
绝不要把密码或访问令牌写进远程 URL。URL 会出现在 .git/config、命令输出、截图和日志中。优先使用由 agent 管理的 SSH 密钥,或由安全的操作系统凭据库支持的 Git credential helper 提供 HTTPS 凭据。应分别验证每台独立主机,并只授予推送所需的仓库权限。
存档中的命令使用 git://。Git 的 `git daemon` 文档说明该协议没有身份认证,默认只启用面向读取的 upload-pack 服务,而匿名 receive-pack 默认禁用。不要把这些历史 URL 当作现代认证写入的模板。
每台服务器都会独立检查自己的受保护分支规则、权限、非快进策略、钩子、签名要求以及其他托管控制。通用 Git 服务器可以用 `receive.denyNonFastForwards` 拒绝非快进推送,`pre-receive` 与 `update` 钩子也可以拒绝拟议的 ref 更新。客户端的强制选项无法绕过这些服务器端决定。若不在两台主机上对齐策略,就应预期某个目标可能拒绝另一个目标已经接受的更新。
实用自动化检查表
- 对独立主机使用独立远程名称,并显式指定分支或 refspec。
- 不在 URL 与日志中保存秘密;分别测试每个目标的认证。
- 对每个远程仓库执行预演,但不要把预演当作保证。
- 按明确顺序推送,并保存每个目标的结果。
- 将远程公布的分支 ID 与预期本地提交进行比较。
- 只重试失败或落后的目标;分歧仍存在时发出告警。
这样,“推送两次”就成为一个可观测的复制流程,同时不会虚构分布式事务保证。
官方一手文档
- Git:`git remote` 的 URL 与 push URL 行为
- Git:`git push`、预演、ref 选择、mirror 模式与原子推送
- Git:`git ls-remote` 与 `--exit-code`
- Git:credential helper
- Git:receive 配置,包括拒绝非快进更新
- Git:接收端钩子
- Git:`git daemon` 服务与匿名推送警告
---
2019 年原文存档(逐字保留)
以下是原始 WordPress 导出中完整可见的正文,发布及最后修改日期均为 2019-04-24。仅为仓库格式规范化了不可见的行尾空白。文中的未认证 git:// 示例以及缺少失败处理的写法作为历史来源保留,并非当前建议。
git remote set-url --add --push origin git://original/repo.git
git remote set-url --add --push origin git://another/repo.git
Remove one url from origin
git remote set-url --delete --push origin git://another/repo.git
Remove origin totally
git remote rm origin
git remote set-url origin git://another/repo.git
