2020年、BLM運動に伴い、Git界隈でもデフォルトブランチをmasterからmainに変更しようという動きが起こりました。Gitのデフォルトブランチ名変更は、リモートリポジトリがなければ非常に簡単です。リモートリポジトリと連携している場合も、自分が主導権を握っている(リモートリポジトリに書き込める)場合は比較的単純で、ドキュメントも多数見つかります。しかし、書き込めず、基本的にread-onlyで使っているリモートリポジトリの場合、少し面倒ですので、こちらにまとめておきます。
「read-onlyなのだから、単にcloneしなおせばよい」という意見もあると思います。しかし、巨大リポジトリの場合、cloneしなおすにはそれなりの資源の浪費を伴います。また、問題が起きたときこそ学びの機会なのに、cloneしなおしただけでは何の学びも得られません。折角なので、問題解決しつつ、その過程でGitの内部構造について少しでも多くの方々に理解を深めていただけることを目指します。
結論
最終的に一番シンプルと考えた手順は以下のとおりです。「ここだけ知りたい!」というかたはこちらをどうぞ。
# Step 1: ローカルブランチ名を変更する
git branch -m master main
# Step 2: リモートリポジトリのmainブランチを認識させる
git remote set-branches origin main
# Step 3: リモートリポジトリのmainを取得し、ローカルブランチmainの上流ブランチに設定する
git fetch --set-upstream origin main
# Step 4: リモートリポジトリのデフォルトブランチをmainに変更する
## リモートリポジトリの情報をもとに自動的に設定する方法(ネットワークアクセス必要)
git remote set-head -a origin
## 明示する方法(ネットワークアクセス不要)
git remote set-head origin main
git pullできない!
git pullしていたリモートリポジトリでデフォルトブランチ名を変更してしまうと、git pullが失敗します。私の場合、GitHubのmicrosoft/vscode-docsというリポジトリをpullしようとしたら、エラーになりました。
% git pull
fatal: couldn't find remote ref refs/heads/master
git pullの中で行われているgit fetchで失敗します。なお、git pullは、git fetchした上でgit merge FETCH_HEADするのと等価です。
% git fetch
fatal: couldn't find remote ref refs/heads/master
GitHub上のWeb UIでリポジトリを眺めてみれば、一目瞭然。masterからmainへの名前の変更が原因です。単なるブランチ名前の変更なので、cloneしなおさずに対応していきます。
なお、git remote updateでもできそうですが、これもエラーになります。
% git remote update
Fetching origin
fatal: couldn't find remote ref refs/heads/master
error: Could not fetch origin
何が起きているのか?
内部構造を確認しながら対応していきましょう。
まず、エラーが起きたときの状態を確認します。
% git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
ローカルブランチはmasterのみで、リモートリポジトリは1つだけ(表示されていませんが、GitHub上のmicrosoft/vscode-docsリポジトリ)がoriginという名前で登録されていることがわかります。リモート追跡ブランチ (remote-tracking branch) として、originの参照するリポジトリのmasterを追跡するorigin/masterブランチのみがあります。また、origin/HEADがorigin/masterを指しているので、originのデフォルトブランチはmasterである、とこのリポジトリは認識しています。origin/HEADについては、後で解説します。
実際には、origin側にはすでにmasterはなく、mainをデフォルトブランチとしており、それに合わせてローカルでもmainを使いたいので、これらのmasterはすべてmainに変更されなくてはなりません。
リポジトリ内部を見ていきます。まずはconfigです。
% cat .git/config
[core]
repositoryformatversion = 0
(snip)
[remote "origin"]
url = https://github.com/microsoft/vscode-docs.git
fetch = +refs/heads/master:refs/remotes/origin/master
[branch "master"]
remote = origin
merge = refs/heads/master
まず、masterのremoteの設定がoriginになっているので、ローカルのmasterブランチのリモートとしてoriginを参照しています。つまり、masterで何も引数をつけずにgit fetchを実行したときにはoriginをチェックしに行きます。
その際に、そのoriginのどこのブランチを見に行くかというと、originのfetchの設定にあるとおり、masterブランチのみを見に行きます(すべてのブランチを見に行くような挙動にも、この設定で変えられます)。すでにmasterはありませんので、これはエラーになりますね。
最後に、masterのremoteがorigin、mergeがrefs/heads/masterになっているので、masterで何も引数をつけずにgit mergeを実行するとorigin/masterをマージします。要は、masterはorigin/masterを上流ブランチ (upstream branch) としています
これらのmasterはすべてmainに変える必要があります。
最後に、上のgit branch -aの結果やconfigに登場した各種ブランチやHEADの実体です。
% ls .git/refs/heads .git/refs/remotes/origin
.git/refs/heads:
master
.git/refs/remotes/origin:
HEAD master
% cat .git/refs/heads/master
b301ccfa39274d648a654fe69f32b47e6d0577c4
% cat .git/refs/remotes/origin/master
b301ccfa39274d648a654fe69f32b47e6d0577c4
% cat .git/refs/remotes/origin/HEAD
ref: refs/remotes/origin/master
% cat .git/HEAD
ref: refs/heads/master
% cat .git/FETCH_HEAD
b301ccfa39274d648a654fe69f32b47e6d0577c4 branch 'master' of https://github.com/microsoft/vscode-docs
最後の.git/FETCH_HEADはgit fetchの操作で取得したブランチの情報を一時的に(git pullでマージするために)記録するもので、git fetchの結果次第では空にもなります。そんなに重要ではないのですが、一応載せています。
実体がわかっているので、すべて手で書き換えたりファイル名を変更したりしても対応できます。とはいえ、それは推奨される方法ではないので、ここではgitコマンドを使いながら対応します。
Step 1: ローカルブランチ名を変更する
まず最初にすべきことは、ローカルのmasterブランチをmainにすることです。ローカルブランチ名の変更はたまに発生することで、通常運用の範囲内なので簡単ですね。
% git branch -m master main
これにより、ローカルのmasterがmainに変わりました。
% git branch -a
* main
remotes/origin/HEAD -> origin/master
remotes/origin/master
configもそこだけ変わっており、他はまったく何も変わっていません。リモートリポジトリの処理に関しては何の問題解決にもなっていないとも言えます。
% cat .git/config
[core]
repositoryformatversion = 0
(snip)
[remote "origin"]
url = https://github.com/microsoft/vscode-docs.git
fetch = +refs/heads/master:refs/remotes/origin/master
[branch "main"]
remote = origin
merge = refs/heads/master
実体です。変化がある部分のみ記載します。特に説明は不要でしょう。
% ls .git/refs/heads .git/refs/remotes/origin
.git/refs/heads:
main
.git/refs/remotes/origin:
HEAD master
% cat .git/refs/heads/main
b301ccfa39274d648a654fe69f32b47e6d0577c4
% cat .git/HEAD
ref: refs/heads/main
Step 2: リモートリポジトリのmainブランチを認識させる
git pullやgit fetchを実行したときにエラーになる直接の原因は、リモートリポジトリのmasterを見に行っているのにそのmasterがないという点にありました。そこで、まずはmainを見に行くようにする必要があります。現在、ローカルではリモートのmainブランチの存在を一切認識していないので、認識させます。
% git remote set-branches origin main
これをしておかないと、たとえgit fetch origin mainとmainを明示して取りに行っても、origin/mainのデータの取得には成功するものの、origin/mainがリモート追跡ブランチとして登録されません。
このgit remote set-branchesはconfigをいじるだけなので、ネットワークアクセスがない状態で実行できます。また、ブランチのリストにも影響を与えません。
% git branch -a
* main
remotes/origin/HEAD -> origin/master
remotes/origin/master
configは、originのfetchのみが変わっており、取得元も取得先もmainになっていることがわかります。
% cat .git/config
[core]
repositoryformatversion = 0
(snip)
[remote "origin"]
url = https://github.com/microsoft/vscode-docs.git
fetch = +refs/heads/main:refs/remotes/origin/main
[branch "main"]
remote = origin
merge = refs/heads/master
なお、git remote set-branches --add origin mainとしてやると、こんな感じでmasterブランチをfetchする設定を残せます。今回は不要なので置き換えています。
[remote "origin"]
url = https://github.com/microsoft/vscode-docs.git
fetch = +refs/heads/master:refs/remotes/origin/master
fetch = +refs/heads/main:refs/remotes/origin/main
実体です。一切変化はありません。まず、git branch -aの結果からわかるとおり、ブランチには特に変化はありません。また、実際にリモートリポジトリに最新データを取りに行ったわけでもないので、.git/FETCH_HEADにも変化はありません。
% ls .git/refs/heads .git/refs/remotes/origin
.git/refs/heads:
main
.git/refs/remotes/origin:
HEAD master
% cat .git/FETCH_HEAD
b301ccfa39274d648a654fe69f32b47e6d0577c4 branch 'master' of https://github.com/microsoft/vscode-docs
Step 3: リモートリポジトリのmainを取得し、ローカルブランチmainの上流ブランチに設定する
Step 2で準備したので、実際にリモートリポジトリのmainブランチをリモート追跡ブランチorigin/mainとしてローカルに取得します。その上で、それをmainの上流ブランチに設定します。
% git fetch --set-upstream origin main
From https://github.com/microsoft/vscode-docs
* branch main -> FETCH_HEAD
* [new branch] main -> origin/main
この操作で行われる3つの処理
ここでやっていることは3点あります。
- リモートリポジトリの
mainを構成するデータ(コミットやblobなどのオブジェクト)をネットワーク経由で取得 - リモート追跡ブランチ
origin/mainを登録 - ローカルブランチ
mainの上流ブランチをリモート追跡ブランチorigin/mainに変更
順に見ていきましょう。
1は、特に説明は不要でしょう。コミットやblobなどのオブジェクトがなければそもそもリモートリポジトリのmainブランチをローカルで表現できないので必須です。わからなければPro Gitの『10.2 Gitの内側 - Gitオブジェクト』を読むとイメージが掴めるはずです。
この1だけであれば、実は最初の段階からgit fetch origin mainで可能でした。しかしそれだと2以降ができないので、Step 2を実施しました。
2では、origin/mainをリモート追跡ブランチとして登録します。何度も書いているように、Step 2でorigin/mainを認識させたことで、この追跡が可能になりました。
2までであれば、実は、git fetchというシンプルなコマンドで可能でした。Step 2で、mainでgit fetchするとorigin/mainを更新しに行くように設定したためです。しかし、これだけだと、リモート追跡ブランチorigin/mainはローカルブランチmainの上流ブランチになっていません。なので、少し中途半端な状態です。
3は、現在のmainのマージ対象がorigin/masterになっているのを、origin/mainに変えてやる作業です。2でorigin/mainを更新しても、現在のmainのマージ元がorigin/masterになっている以上、3をやらないと、origin/mainの更新内容がmainに入ってこないのです。
3は、--set-upstreamオプションによって実現されています。このオプションをつけないと2止まりです。そしてこのオプションをつけるときはブランチの指定が必要なので、git fetch --set-upstreamではダメで、git fetch --set-upstream origin mainとする必要があります。
操作の結果
説明が長くなりました。先程の操作の結果を載せます。
まず、ブランチについては、リモート追跡ブランチorigin/mainが加わっています。これは上の説明の2にあたります。
% git branch -a
* main
remotes/origin/HEAD -> origin/master
remotes/origin/main
remotes/origin/master
configではmainのmergeがrefs/heads/mainに書き換わっています。これは上の説明の3にあたります。
% cat .git/config
[core]
repositoryformatversion = 0
(snip)
[remote "origin"]
url = https://github.com/microsoft/vscode-docs.git
fetch = +refs/heads/main:refs/remotes/origin/main
[branch "main"]
remote = origin
merge = refs/heads/main
実体です。
% ls .git/refs/heads .git/refs/remotes/origin
.git/refs/heads:
main
.git/refs/remotes/origin:
HEAD main master
% cat .git/refs/heads/main
ed61526ebb39ec00809fce8a75fa4189d0d51ad6
% cat .git/refs/remotes/origin/main
ed61526ebb39ec00809fce8a75fa4189d0d51ad6
% cat .git/refs/remotes/origin/master
b301ccfa39274d648a654fe69f32b47e6d0577c4
% cat .git/refs/remotes/origin/HEAD
ref: refs/remotes/origin/master
% cat .git/FETCH_HEAD
ed61526ebb39ec00809fce8a75fa4189d0d51ad6 branch 'main' of https://github.com/microsoft/vscode-docs
origin/main用の参照ができています。mainの参照の内容も、origin/mainの参照と同じSHA1ハッシュ値に変わっています。一方で、origin/masterの参照はfetchの指定から外れたので更新されていません。
git fetchに伴って.git/FETCH_HEADが更新されています。
git branch -aと.git/refs/remotes/origin/HEADの内容から、origin/HEADはorigin/masterを指したままだとわかります。
Step 4: リモートリポジトリのデフォルトブランチをmainに変更する
概ね問題なくなりました。ただ、デフォルトブランチを表すorigin/HEADが引き続きorigin/masterを指してしまっているので、最後にこれを直します。以下のコマンドのどちらかになります。
リモートリポジトリの情報をもとに自動的に設定する方法(ネットワークアクセス必要):
% git remote set-head -a origin
origin/HEAD set to main
明示する方法(ネットワークアクセス不要):
git remote set-head origin main
前者は、実際にリモートリポジトリにアクセスして、どのブランチがデフォルトブランチになっているのかの情報を取得します。そのため、(リモートリポジトリがネットワークの向こうにある場合は)ネットワークアクセスが必要ですし、ローカルアクセスのみの後者に比べると遅いです(といっても、通常のインターネット回線なら、かかっても1秒程度かと思いますが)。
origin/HEADの役割
origin/HEADの役割は、「ブランチ名を指定するところでoriginと指定したときに、それをoriginのデフォルトブランチ(たとえばorigin/main)のことだと解釈できるようにする」というものです。たとえば、git log originと実行したときにgit log origin/mainのことだと解釈してくれるのは、このorigin/HEADが正しく設定されているからです。
したがって、上のコマンドでorigin/HEADの向く先をorigin/masterからorigin/mainに直さないと、originと指定したときにorigin/masterのほうを見に行ってしまい、混乱のもとです。
操作の結果
origin/HEADが正しく設定されました。
% git branch -a
* main
remotes/origin/HEAD -> origin/main
remotes/origin/main
remotes/origin/master
もちろん、実体も適切です。
% cat .git/refs/remotes/origin/HEAD
ref: refs/remotes/origin/main
完成形
これで、リモートリポジトリのデフォルトブランチ名変更への追従は終わりました。あとは今までどおりgit pullなどで使えます。
% git branch -a
* main
remotes/origin/HEAD -> origin/main
remotes/origin/main
remotes/origin/master
% cat .git/config
[core]
repositoryformatversion = 0
(snip)
[remote "origin"]
url = https://github.com/microsoft/vscode-docs.git
fetch = +refs/heads/main:refs/remotes/origin/main
[branch "main"]
remote = origin
merge = refs/heads/main
origin/masterが残っているのが少し気になりますが、害はありませんし消そうと思えば消せるので、ひとまずこれで完成とします(git fetch --pruneやgit remote prune originやgit remote update --pruneで消えるかと思ったのですが、消えませんでした)。
最後に
「git pullできない」という問題をきっかけに、リモートリポジトリのデフォルトブランチ名変更(master → main)に追従する方法を扱いながら、Gitの内部構造の解説を少ししました。少し冗長だったかもしれません。
Gitのいいところは、そのパフォーマンスとは裏腹に、シンプルなディレクトリやテキストファイルでデータが表現されているので、いざとなれば自分でも簡単なUnixコマンドなどである程度調査や問題解決ができるところです。ぜひ触って親しんでいただければと思います。
用語・バージョン
用語:
-
デフォルトブランチ:
masterやmainのような、すべての履歴の中心となるブランチ。何もブランチを指定しない場合に参照されるブランチ。 - デフォルトブランチ名のデフォルト:様々なリポジトリの「デフォルトブランチ」の名前に使う文字列のデフォルト。バラバラだと操作しにくいのでふつうは組織などで統一する。
- ローカルブランチ:ローカルリポジトリで作成したブランチ。リポジトリ作成時に自動的に作られるもの以外は、基本的に自分で作る。
-
リモート追跡ブランチ:ローカルリポジトリのブランチのうち、リモートリポジトリのブランチがそのまま反映される(リモートリポジトリのブランチを追跡する)もの。
origin/mainなど。 - 上流ブランチ:あるローカルブランチがあるリモート追跡ブランチの上に作られている場合、そのリモート追跡ブランチはそのローカルブランチの上流ブランチであると言う。
確認に用いたバージョン:
% git --version
git version 2.28.0
参考資料:デフォルトブランチ関連の動き
-
2020-06-23: Gitのデフォルトブランチ名の変更に関する声明
- Software Freedom ConservancyとGitプロジェクトが、Gitのデフォルトブランチ名を攻撃的(offensive)だと感じる人がいるので変更に向けて動く旨を、声明として発表しました。
- 参考:Regarding Git and Branch Naming (Software Freedom Conservancy, June 23, 2020)
-
2020-07-27: Git 2.28での
init.defaultBranchの導入-
init.defaultBranchという設定で、git initによる新規リポジトリ作成時のデフォルトブランチを設定可能になりました。それまでもリポジトリ作成後の変更(運用)は自由でしたが、作成時はmasterにハードコードされていました。 - 参考:Highlights from Git 2.28 (The GitHub Blog, July 27, 2020)
-
-
2020-08-26: GitHubで新規リポジトリのデフォルトブランチ名のデフォルトを設定可能に
- デフォルトブランチ名のデフォルトをアカウントごとに個人設定で設定可能になりました。
- 参考:Set the default branch for newly-created repositories (The GitHub Blog, August 26, 2020)
-
2020-10-01: GitHubで新規リポジトリのデフォルトブランチ名のデフォルトがmainに
- デフォルトブランチ名のデフォルトを設定で変更していない場合、masterでなくmainが使われるようになりました。8月26日の変更をさらに一歩推し進めたものです。
- 参考:The default branch for newly-created repositories is now main (The GitHub Blog, October 1, 2020)