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

2020年、[BLM運動](https://en.wikipedia.org/wiki/Black_Lives_Matter)に伴い、Git界隈でもデフォルトブランチを`master`から`main`に変更しようという動きが起こりました。Gitのデフォルトブランチ名変更は、リモートリポジトリがなければ非常に簡単です。リモートリポジトリと連携している場合も、自分が主導権を握っている（リモートリポジトリに書き込める）場合は比較的単純で、ドキュメントも多数見つかります。しかし、書き込めず、基本的にread-onlyで使っているリモートリポジトリの場合、少し面倒ですので、こちらにまとめておきます。

「read-onlyなのだから、単にcloneしなおせばよい」という意見もあると思います。しかし、巨大リポジトリの場合、cloneしなおすにはそれなりの資源の浪費を伴います。また、問題が起きたときこそ学びの機会なのに、cloneしなおしただけでは何の学びも得られません。折角なので、問題解決しつつ、その過程でGitの内部構造について少しでも多くの方々に理解を深めていただけることを目指します。


## 結論

最終的に一番シンプルと考えた手順は以下のとおりです。「ここだけ知りたい！」というかたはこちらをどうぞ。

```shell
# 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](https://github.com/microsoft/vscode-docs)というリポジトリをpullしようとしたら、エラーになりました。

```shell
% git pull
fatal: couldn't find remote ref refs/heads/master
```

`git pull`の中で行われている`git fetch`で失敗します。なお、`git pull`は、`git fetch`した上で`git merge FETCH_HEAD`するのと[等価](https://git-scm.com/docs/git-pull)です。

```shell
% git fetch
fatal: couldn't find remote ref refs/heads/master
```

GitHub上のWeb UIでリポジトリを眺めてみれば、一目瞭然。`master`から`main`への名前の変更が原因です。単なるブランチ名前の変更なので、cloneしなおさずに対応していきます。

なお、`git remote update`でもできそうですが、これもエラーになります。

```shell
% git remote update
Fetching origin
fatal: couldn't find remote ref refs/heads/master
error: Could not fetch origin
```


## 何が起きているのか？

内部構造を確認しながら対応していきましょう。

まず、エラーが起きたときの状態を確認します。

```shell
% 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です。

```shell
% 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の実体です。

```shell
% 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`にすることです。ローカルブランチ名の変更はたまに発生することで、通常運用の範囲内なので簡単ですね。

```shell
% git branch -m master main
```

これにより、ローカルの`master`が`main`に変わりました。

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

configもそこだけ変わっており、他はまったく何も変わっていません。リモートリポジトリの処理に関しては何の問題解決にもなっていないとも言えます。

```shell
% 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
```

実体です。変化がある部分のみ記載します。特に説明は不要でしょう。

```shell
% 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`ブランチの存在を一切認識していないので、認識させます。

```shell
% git remote set-branches origin main
```

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

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

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

configは、`origin`の`fetch`のみが変わっており、取得元も取得先も`main`になっていることがわかります。

```shell
% 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`する設定を残せます。今回は不要なので置き換えています。

```shell
[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`にも変化はありません。

```shell
% 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`の上流ブランチに設定します。

```shell
% 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オブジェクト』](https://git-scm.com/book/ja/v2/Git%E3%81%AE%E5%86%85%E5%81%B4-Git%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88)を読むとイメージが掴めるはずです。

この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にあたります。

```shell
% 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
```

実体です。

```shell
% 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`を指してしまっているので、最後にこれを直します。以下のコマンドのどちらかになります。

リモートリポジトリの情報をもとに自動的に設定する方法（ネットワークアクセス必要）：

```shell
% 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`が正しく設定されました。

```shell
% 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`などで使えます。

```shell
% 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`など。
* **上流ブランチ**：あるローカルブランチがあるリモート追跡ブランチの上に作られている場合、そのリモート追跡ブランチはそのローカルブランチの上流ブランチであると言う。

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

```shell
% 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)](https://sfconservancy.org/news/2020/jun/23/gitbranchname/)
* **2020-07-27: Git 2.28での`init.defaultBranch`の導入**
    * `init.defaultBranch`という設定で、`git init`による新規リポジトリ作成時のデフォルトブランチを設定可能になりました。それまでもリポジトリ作成後の変更（運用）は自由でしたが、作成時はmasterにハードコードされていました。
    * 参考：[Highlights from Git 2.28 (The GitHub Blog, July 27, 2020)](https://github.blog/2020-07-27-highlights-from-git-2-28/)
* **2020-08-26: GitHubで新規リポジトリのデフォルトブランチ名のデフォルトを設定可能に**
    * デフォルトブランチ名のデフォルトをアカウントごとに個人設定で設定可能になりました。
    * 参考：[Set the default branch for newly-created repositories (The GitHub Blog, August 26, 2020)](https://github.blog/changelog/2020-08-26-set-the-default-branch-for-newly-created-repositories/)
* **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)](https://github.blog/changelog/2020-10-01-the-default-branch-for-newly-created-repositories-is-now-main/)


## あわせて読みたい

* [Pro Gitの『10.3 Gitの内側 - Gitの参照』](https://git-scm.com/book/ja/v2/Git%E3%81%AE%E5%86%85%E5%81%B4-Git%E3%81%AE%E5%8F%82%E7%85%A7)
* [Pro Gitの『10.5 Gitの内側 - Refspec』](https://git-scm.com/book/ja/v2/Git%E3%81%AE%E5%86%85%E5%81%B4-Refspec)
* [Understanding .git/config's 'remote' and 'branch' sections](https://stackoverflow.com/questions/43317872/understanding-git-configs-remote-and-branch-sections)
* [Git で「追跡ブランチ」って言うのやめましょう](https://qiita.com/uasi/items/69368c17c79e99aaddbf)
* [5 steps to change GitHub default branch from master to main](https://stevenmortimer.com/5-steps-to-change-github-default-branch-from-master-to-main/)