Gitを2つのリモートへ安全にPushする:Push URL・障害処理・検証

メンテナンス版、2026-09-01確認。 2019年の短いコマンドメモは末尾に原文のまま保存しています。ここでは、1つの論理リポジトリに対する代替Push URLと、実体が異なる2つのホストを区別し、2宛先へのPushを単一の原子的トランザクションではなくレプリケーションとして扱います。

2026年メンテナンスガイド

Gitは、1つのリモートに設定されたすべてのpushurlへPushできます。そのため元の記事の方法は動作しますが、「同時」という表現は誤解を招きます。Gitは宛先ごとに接続するので、一方が成功した後でも、もう一方が失敗し得ます。独立した2つのホスティングサービスへ送るなら、別々の名前付きリモートにする方が通常は明確で安全です。

例ではmainブランチを使います。実際に公開するブランチへ置き換え、リポジトリ固有のpush.defaultやupstream設定に結果を左右されないよう、ブランチ名を明示してください。

適切なモデルを選ぶ

状況推奨モデル理由
管理者が同一の論理リポジトリを公開すると保証している2つの認証済みエンドポイント1つのリモートに複数のpushurlを設定1つのリモート名で全書き込みエンドポイントを使い、1つのFetch URLを維持できます。
独立した2つのリポジトリ、プロバイダー、認証情報、またはポリシーprimarymirrorのような別々のリモート宛先ごとに確認、Push、再試行、検証ができます。

この区別は公式の`git remote`ドキュメントに基づきます。1つのリモートのPush URLとFetch URLは同じ場所を指すべきであり、ある場所から取得して別の場所へ公開するなら別々のリモートを使うよう記載されています。`git push`ドキュメントも、Pushが定義済みのすべてのPush URLへ作用することを明記しています。

方法A:1つのリモートに複数のPush URL

originがすでに存在するとします。Fetch URLを設定してから、プライマリエンドポイントも含む、意図したPush先をすべて追加します。

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が1つでも存在すると、GitはFetch URLへフォールバックせず、設定されたPush URL一覧を使います。2番目のエンドポイントだけを追加すると、以後のPushからプライマリが抜けてしまいます。

FetchとPushの設定を分けて確認します。

git remote get-url --all origin
git remote get-url --all --push origin

Push URLを1つ削除するときは、最後の引数が正規表現である点に注意してください。先頭と末尾を固定すれば、意図せず広く一致することを防げます。

git remote set-url --delete --push origin 
  '^ssh://git@write-b[.]example/team/project[.]git$'

カスタムPush URLを意図的にすべて消すと、Gitは再びそのリモートのFetch URLをPushにも使います。

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の背後に隠さず扱えます。

ドライラン、Push、検証

独立した宛先ごとに、対象ブランチをドライランします。

git push --dry-run --porcelain primary main
git push --dry-run --porcelain mirror main

--dry-runはref更新の実送信以外を行い、--porcelainは機械処理しやすい安定したステータス行を出力します。ドライランは有用ですが、成功を予約するものでも、後のPushがサーバー側の全規則を通過する証明でもありません。

順序を明確にしてPushします。&&を使うと、プライマリが失敗した場合はミラーを実行しません。

git push primary main &&
git push mirror main

2つ目が失敗しても、最初のPushはロールバックされません。各宛先の状態を記録し、遅れている宛先だけを再試行します。

Push後、ローカルのコミット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

2つのls-remote出力の第1フィールドが、どちらもgit rev-parse mainの値と一致する必要があります。自動化では値を厳密に比較し、refが存在しない、または異なる場合にジョブを失敗させてください。

原子性と部分的な失敗

git push --atomicは、1台の受信サーバーに対して、1回のPushに含まれる全refを更新するか、すべて更新しないかを要求します。サーバーがatomic pushをサポートしなければ失敗します。2つの接続やホストを1つのトランザクションにまとめることはできません。宛先を複数のPush URLで表しても、別々のリモートで表しても同じです。

Git 2.43.0と使い捨てのローカルリポジトリで、実際の失敗パターンを再現しました。

  • git push --dry-runは設定した2つのローカルPush URLを確認しましたが、どちらのベアリポジトリも更新しませんでした。
  • 実際のPushでは、両方が同じコミットへ更新されました。
  • 1つ目を有効なURLのまま残し、2つ目を存在しないリポジトリへ変更すると、1つ目は更新済み、2つ目は失敗となり、git pushはステータス128で終了しました。

具体的なエラーと終了ステータスはトランスポートにより異なることがありますが、部分的成功の危険は変わりません。堅牢なレプリケーションジョブは不一致を検出して解消する必要があり、--atomicを付けてもホスト間ロールバックは実現しません。

ブランチ、タグ、Mirrorモード

対象refを明示します。

  • git push <remote> mainmainだけを公開します。
  • git push <remote> --allはローカルの全ブランチを公開しますが、タグは含みません。
  • git push <remote> --tagsは全タグと、コマンドで明示したrefspecを公開します。
  • git push <remote> --mirrorrefs/配下の全refをミラーし、変更されたrefを強制更新し、ローカルにないリモートrefを削除します。

--mirrorは破壊的な同期であり、「バックアップを作る」の同義語ではありません。全refの範囲と削除ポリシーを確認済みの、意図的に管理するミラーにだけ使ってください。通常の公開では名前を指定したブランチと、配布する意思のあるタグだけをPushします。

認証情報とサーバー側ポリシー

パスワードやアクセストークンをリモートURLへ埋め込まないでください。URLは.git/config、コマンド出力、スクリーンショット、ログに現れます。エージェントで管理するSSH鍵、またはOSの安全な資格情報ストアを使うGit credential helperからHTTPS認証情報を渡してください。独立したホストごとに認証し、Pushに必要なリポジトリ権限だけを与えます。

アーカイブのコマンドはgit://を使っています。Gitの`git daemon`ドキュメントによれば、このプロトコルには認証がなく、読み取り用のupload-packだけがデフォルトで有効で、匿名のreceive-packはデフォルトで無効です。歴史的なURLを、現代の認証付き書き込みのひな型にしないでください。

各サーバーは、保護ブランチ規則、権限、non-fast-forwardポリシー、フック、署名要件などのホスティング制御を独立して評価します。一般的なGitサーバーでは`receive.denyNonFastForwards`でnon-fast-forward Pushを拒否でき、`pre-receive`と`update`フックでも提案されたref更新を拒否できます。クライアント側のforceオプションは、そのサーバー判断を上書きしません。両ホストのポリシーをそろえない限り、一方が受理した更新をもう一方が拒否する可能性があります。

実用的な自動化チェックリスト

  1. 独立したホストには別々のリモートを使い、ブランチまたはrefspecを明示します。
  2. URLやログへ秘密情報を入れず、各宛先の認証を個別に試します。
  3. リモートごとにドライランしますが、保証とは見なしません。
  4. 意図した順序でPushし、宛先ごとの結果を保存します。
  5. 公開されたリモートブランチIDを、意図したローカルコミットと比較します。
  6. 失敗または遅延した宛先だけを再試行し、不一致が残れば通知します。

これにより「2回Pushする」を観測可能なレプリケーション処理にでき、存在しない分散トランザクション保証を装うこともありません。

公式一次資料

---

2019年の原文アーカイブ(逐語保存)

以下は、2019-04-24に公開・最終更新されたWordPressエクスポートの表示可能な本文全文です。リポジトリの整形に合わせ、見えない行末空白だけを正規化しました。認証のない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

Leave a Reply