LoginSignup
12
9

More than 3 years have passed since last update.

リモートリポジトリのデフォルトブランチ名変更(master → main)に追従する 〜Gitの内部構造の理解を深めながら〜

Posted at

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/HEADorigin/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

まず、masterremoteの設定がoriginになっているので、ローカルのmasterブランチのリモートとしてoriginを参照しています。つまり、masterで何も引数をつけずにgit fetchを実行したときにはoriginをチェックしに行きます。

その際に、そのoriginのどこのブランチを見に行くかというと、originfetchの設定にあるとおり、masterブランチのみを見に行きます(すべてのブランチを見に行くような挙動にも、この設定で変えられます)。すでにmasterはありませんので、これはエラーになりますね。

最後に、masterremoteoriginmergerefs/heads/masterになっているので、masterで何も引数をつけずにgit mergeを実行するとorigin/masterをマージします。要は、masterorigin/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_HEADgit fetchの操作で取得したブランチの情報を一時的に(git pullでマージするために)記録するもので、git fetchの結果次第では空にもなります。そんなに重要ではないのですが、一応載せています。

実体がわかっているので、すべて手で書き換えたりファイル名を変更したりしても対応できます。とはいえ、それは推奨される方法ではないので、ここではgitコマンドを使いながら対応します。

Step 1: ローカルブランチ名を変更する

まず最初にすべきことは、ローカルのmasterブランチをmainにすることです。ローカルブランチ名の変更はたまに発生することで、通常運用の範囲内なので簡単ですね。

% git branch -m master main

これにより、ローカルのmastermainに変わりました。

%  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 pullgit fetchを実行したときにエラーになる直接の原因は、リモートリポジトリのmasterを見に行っているのにそのmasterがないという点にありました。そこで、まずはmainを見に行くようにする必要があります。現在、ローカルではリモートのmainブランチの存在を一切認識していないので、認識させます。

% git remote set-branches origin main

これをしておかないと、たとえgit fetch origin mainmainを明示して取りに行っても、origin/mainのデータの取得には成功するものの、origin/mainがリモート追跡ブランチとして登録されません。

このgit remote set-branchesはconfigをいじるだけなので、ネットワークアクセスがない状態で実行できます。また、ブランチのリストにも影響を与えません。

% git branch -a
* main
  remotes/origin/HEAD -> origin/master
  remotes/origin/master

configは、originfetchのみが変わっており、取得元も取得先も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点あります。

  1. リモートリポジトリのmainを構成するデータ(コミットやblobなどのオブジェクト)をネットワーク経由で取得
  2. リモート追跡ブランチorigin/mainを登録
  3. ローカルブランチ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で、maingit 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ではmainmergerefs/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/HEADorigin/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 --prunegit remote prune origingit remote update --pruneで消えるかと思ったのですが、消えませんでした)。

最後に

git pullできない」という問題をきっかけに、リモートリポジトリのデフォルトブランチ名変更(master → main)に追従する方法を扱いながら、Gitの内部構造の解説を少ししました。少し冗長だったかもしれません。

Gitのいいところは、そのパフォーマンスとは裏腹に、シンプルなディレクトリやテキストファイルでデータが表現されているので、いざとなれば自分でも簡単なUnixコマンドなどである程度調査や問題解決ができるところです。ぜひ触って親しんでいただければと思います。

用語・バージョン

用語:

  • デフォルトブランチmastermainのような、すべての履歴の中心となるブランチ。何もブランチを指定しない場合に参照されるブランチ。
  • デフォルトブランチ名のデフォルト:様々なリポジトリの「デフォルトブランチ」の名前に使う文字列のデフォルト。バラバラだと操作しにくいのでふつうは組織などで統一する。
  • ローカルブランチ:ローカルリポジトリで作成したブランチ。リポジトリ作成時に自動的に作られるもの以外は、基本的に自分で作る。
  • リモート追跡ブランチ:ローカルリポジトリのブランチのうち、リモートリポジトリのブランチがそのまま反映される(リモートリポジトリのブランチを追跡する)もの。origin/mainなど。
  • 上流ブランチ:あるローカルブランチがあるリモート追跡ブランチの上に作られている場合、そのリモート追跡ブランチはそのローカルブランチの上流ブランチであると言う。

確認に用いたバージョン:

% git --version
git version 2.28.0

参考資料:デフォルトブランチ関連の動き

あわせて読みたい

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9