【Git】エラー、コンフリクトのお悩み解決 - Gitの気持ちになって理解する checkout/merge/pull

# よく見るエラーメッセージ

Gitを使っていると頻繁に目にするエラーメッセージ……
例えば次のメッセージが表示されたとき、あなたはどのように対応するでしょうか？

```shell
error: Your local changes to the following files would be overwritten by checkout:
	test.txt
Please commit your changes or stash them before you switch branches.
Aborting
```

メッセージの意味、その対処法や背景までわかる人は、おそらく先を読む必要はないでしょう。
そうでない人はこの記事を読むことで、いままでより**自信をもってgitを使うことができる**ようになるかもしれません。

# この記事で説明すること

- チェックアウトするときに、なぜエラーが発生するのか
- マージやリベースするときに、なぜコンフリクトが発生するのか
- チェックアウト時のエラーやコンフリクトを理解するための**gitのキホン**
- チェックアウトやマージ、プルが具体的になにをやっているのか

## この記事が対象とする人

- 冒頭のエラーメッセージの意味がわからない人
- 冒頭のエラーメッセージへの対処法はわかるけど、その意味がいまいちわかっていない人
- 検索で出たページのコマンドをコピペして、痛い目にあったことのある人

## この記事が対象としない人

- 冒頭のエラーメッセージの意味や対処法が完璧にわかる人
- Gitで困ったことのない人
- Gitに触ったことのない人、これから勉強する人

# Gitのキホン

Gitの気持ちを理解する上で大切なことは以下の2点です。

1. コミットはその時点におけるすべてのファイルのスナップショットである[^1]
2. ブランチはコミットを**指し示しているだけ**

これは本当に大事なので、いまここで目を閉じて復唱してください。

## コミットはスナップショット

各コミットは、コミット作成時点でのgitの管理下にあるファイルすべてのスナップショット（その時点でのファイルの状態）を保存しています。

gitの管理下にあるというのは、コミットを作成したときに`git add`コマンドやGUIツールでインデックスに登録されていたファイルのことです。過去（＝ツリーの上流）のコミット作成時に登録されていたファイルも含みます。

### 「コミットする」という言葉の意味

**注意**
この節は飛ばしても構いません。以降を読み進めるにあたって、用語に引っ掛かりを感じたら戻って来てみてください。

「ファイルをコミットする」という言葉がよく使われますが、これには二つの動作が含まれています。

1. ファイルAをインデックスに登録する
2. ファイルAがインデックスに登録された状態で、コミットを**作成する**

また「コミットされていないファイル」という言葉もよく使われますが、これは以下のような意味です。

- すでにgitの管理下にあるファイルAに（いまのコミットから）なんらかの変更が加えられている
- ファイルAがインデックスに登録されていない、または、インデックスに登録されているが新たなコミットがまだ作成されていない

## ブランチはコミットに貼り付けた付箋

masterやorigin/master, developなど色々なブランチを使用したことがあると思います。

ブランチは変更履歴のツリー（コミットの連なり）ではありません。ただ単にひとつのコミットを指し示しているだけの存在です。たとえるなら、区別しやすいようにコミットに付箋を貼り付けているようなものです。

Gitはコミットを作るときに、自動的にいまのブランチ（＝付箋）を新しく作ったコミットに移動してくれているのです。

# なぜチェックアウト時にエラーが発生するのか？

ここまでのキホン知識があると、チェックアウト時にエラーが発生する理由がわかります。

## そもそもチェックアウトとは？

**いまいる場所（HEAD）を指定したコミットに移動する**コマンドです。
指定先としてブランチ（＝付箋）を指定することもできて、その場合はブランチを切り替えるとともに、ブランチが指し示しているコミットに移動します。

ターミナルであれば以下のようなコマンドで実行できます。

```shell
git checkout 1a104f1 # コミットハッシュを指定して移動
git checkout develop # developブランチに移動
```

## コミットを移動するとなにが起こるのか？

思い出してください。コミットはファイルのスナップショットを保存しているのでした。

コミットを移動すると、gitの管理下にあるファイルが、移動先のコミット作成時のスナップショットと同じ状態になります。
言い換えると、**ファイルの内容が移動前の状態から移動先のコミット作成時の状態に変更されます**。

ここにチェックアウト時にエラーが発生する理由があります。

## チェックアウト時に発生したエラーをよく読んでみる

冒頭のエラーをもう一度読んでみましょう。

```shell
error: Your local changes to the following files would be overwritten by checkout:
    test.txt
Please commit your changes or stash them before you switch branches.
Aborting
```

>エラー：以下のファイルのローカルの変更がチェックアウトによって上書きされます。
>    test.txt
>ブランチを切り替える前に、変更をコミットするかスタッシュしてください。
>中断されました。

メッセージの通り、このエラーはファイルにコミットされていない変更がある場合に発生します。

## Gitの気持ちになって考える

ここでgitの気持ちになってみましょう。

Git「お、masterブランチにチェックアウトしろっていう命令がきたよ」
Git「じゃあ、masterブランチが指し示しているこのコミットAに移動しなきゃ！」
Git「管理してるファイルたちをコミットAの状態に変更しないとね。」
Git「……あれ？このtext.txtってファイル、いまのコミット（の状態）から変更されてるよ。」
Git「このtext.txtってファイルはまだコミットされてないんだね。」
Git「でも、コミットAに移動しちゃったら、text.txtはコミットAのときの状態に戻さなきゃ。」
Git「（*勝手に戻しちゃったら怒られそう…………どうしたらいいの……？*）」
Git「」
Git「」
Git「」
Git「error: Your local changes to the following files would be overwritten by checkout……Aborting」

あたなの気持ちにもなってみましょう。

別のブランチをちょっと確認しようと、うっかりチェックアウトしただけで、（コミットするのを忘れていた）**1時間かけて編集した**ファイルの変更がなかったことになったら嫌ですよね？

そんな悲劇を避けるために、gitは丁寧にエラーを返してくれているのです。

## チェックアウトができる条件

チェックアウトができる条件は

- **Gitの管理下にあるファイルがすべてコミット済みであること**[^2]

です。これが満たされていない場合、gitはファイルを移動先のコミットの状態に変更することができない（変更していいかどうかわからない）のでエラーを返します。

Gitの管理下にないファイルが変更されたり新規に作成されている場合は、gitの管理下にあるファイルの状態とは関係ないため、チェックアウト可能です。

### チェックアウト時にエラーがでたときの対処法

ここまでの説明でおわかりかと思いますが、エラーが発生した場合は、メッセージに従って、

- **コミットされていないファイルをコミットするかスタッシュする**
- または、コミットされていないファイルの変更を元に戻す

ことで、チェックアウトができる条件が満たされます。

### 例外的な状況で発生するエラー - スタッシュが効かない！

Gitの管理下にないファイルが新規作成・変更されていても、チェックアウト可能と書きました。しかし、この状況でも例外的にエラーが発生する場合があります。それは、

- 移動先のコミットで同名のファイルがgitの管理下にある

場合です。このときも、gitは「いまのコミット下では管理下にないファイルA」を、移動先のコミットでの状態に変更していいかどうかわからないため、エラーが発生します。

Gitの管理下にないファイルはスタッシュの対象になりません。そのため、この状況ではスタッシュではチェックアウトできない状況が解消できないので、注意してください。

# なぜマージするとコンフリクトが発生するのか？

次はマージすると発生するこのメッセージについて考えていきます。

```
CONFLICT (content): Merge conflict in test.txt
Automatic merge failed; fix conflicts and then commit the result.
```

## そもそもマージとは？

二つの変更履歴を合流させるコマンドです。

典型的には「masterブランチにdevelopブランチをマージする」などの使い方ができます。

注：ブランチは変更履歴そのものではなく、コミットを指し示す付箋です。そのため、この言葉遣いは正確ではありません[^3]。しかし、便宜上このような表現のほうが表面的にはわかりやすく、また通じやすいでしょう。

## マージは具体的になにをやっているのか？

この節は[Gitのmergeコマンドのドキュメント](https://git-scm.com/docs/git-merge)からの引用です。

以下のような変更履歴をもつ二つのブランチ（masterとtopic）があるとします。

```
      A---B---C topic # topicブランチはコミットCを指し示している
     /
D---E---F---G master # masterブランチはコミットGを指し示している
```

このときに、masterブランチ側からtopicブランチをマージします。ターミナルからなら、次のコマンドです。

```shell
git checkout master
git merge topic
```

マージコマンドを受け取ると、gitはそれぞれのブランチが指し示すコミット（CとG）の共通の祖先であるコミットEまで遡ります。
そして、masterが指し示すコミットGに対して、コミットEからtopicブランチの指し示すコミットCまでの変更を、以下のように順番に再現していきます。

コミットGに対して・・・

- ＜E → A＞の変更を追加する
- ＜A → B＞の変更を追加する
- ＜B → C＞の変更を追加する

これらの変更を追加し終わったあと、コミットHを新しく作成して、マージ完了です（下図）。

```
      A---B---C topic
     /         \
D---E---F---G---H master
```

## マージでコンフリクトが発生する理由

これもgitの気持ちになって考えてみれば、理由がおわかりいただけると思います。

masterブランチにtopicブランチをマージするとき、コミットEからコミットCへと向かう変更を、コミットGに対して順番に適用していきます。
しかし、コミットGそれ自体もコミットEからいくつかの変更を経て出来上がっているわけです。
たとえば、

1. ＜E → A＞への変更
2. ＜F → G＞への変更

これら2つの変更の両方で、同じ test.txt というファイルが編集されていたらどうでしょう？
マージコマンドが実行されて、コミットGに＜E → A＞の変更が適用されるとき、1の変更を使うのか2の変更を使うのか、gitには自分で判断することはできません。

このためコンフリクトを発生させて、どちらの変更を使うかの選択はユーザーに委ねるのです。

```
<<<<<<< HEAD
＜F → G＞への変更
=======
＜E → A＞への変更
>>>>>>> topic
```

このようなコンフリクトが発生した場合は、いずれか一方の変更を残したり、二つの変更のいいとこ取りをするなりしてファイルを保存します。そして、改めてインデックスに登録したあと、新しいコミットを作成します。

```shell
 # ＜E → A＞への変更だけを残して、他の部分は削除した
＜E → A＞への変更
```

# プルとは

プルとは主に、リモートリポジトリのブランチをローカル（手元）のリポジトリのブランチにマージするときに使うコマンドです。

このコマンドは、リモートリポジトリのブランチをローカルに取得してくるフェッチ（fetch）コマンドを使ったあとに、対象のブランチをマージする動作を組み合わせたものに過ぎません。

```shell
# リモートリポジトリのorigin/masterブランチを手元のmasterにマージするコマンド
git pull origin master 

# フェッチしたあとマージしても同様のことが実現できる
git fetch # ローカルのorigin/masterがリモートのorigin/masterと同じ位置に同期される
git merge origin/master # origin/masterをmasterにマージする
```

そのため、プルで発生するコンフリクトはマージと同じように扱うことができます。

# おすすめのgitの解説サイト

ぼくがgitを使い始めたときは、次のサイトで勉強しました。
それ以降は、少し高度なことをやりたくなったときに、都度検索して調べる方式でやっていますが、業務上gitで困ることはほとんどありません。

- [Gitをはじめからていねいに](https://github.com/Shinpeim/introduction-to-git)

このサイトで解説されていなくて、いざというときに知っておいた方がいいコマンドとして、`git reflog`があります。このコマンドは、直前のgitでの操作（失敗）をほとんどの場合、取り消すことができます。
ただし、gitの基礎がわかっていないと難しいコマンドなので、[Gitをはじめからていねいに](https://github.com/Shinpeim/introduction-to-git)などで、基礎的な知識をしっかり固めたあとで挑戦するほうがよいです。次のページは比較的丁寧に解説してくれています。

- [git reflog](https://www.atlassian.com/ja/git/tutorials/rewriting-history/git-reflog)

# 謝辞

この記事の内容や言い回しの多くは、[@akio0911](https://twitter.com/akio0911)さんが[アプリ道場サロン](https://community.camp-fire.jp/projects/view/281055)（iOSアプリ開発をテーマとしたサロン）内で行った「git講座」を参考に書かせていただきました。
また、サロン内でのgitに関するやりとりを参考に書かせていただいた節もあります。

akioさんはもちろんのこと、サロンの皆様にはいつもモチベーションをいただいています。ありがとうございます。
iOSアプリを勉強中で仲間が欲しい人は是非覗いてみてください！

この記事がgitで困っている人の助けになれば嬉しいです。

# 参考

- [git-merge Documentation](https://git-scm.com/docs/git-merge)

[^1]: 内部的にはデータ容量を減らすためにgitが保存方法を工夫していますが、gitの気持ちを理解する上ではこう理解するのがよいです。
[^2]: チェックアウトには「コミットを移動せずブランチだけを切り替える」場合もあります。この場合は、この条件が満たされている必要はありません。
[^3]: 正確に書くならば、「masterブランチが指し示すコミットαとdevelopブランチが指し示すコミットβの共通の祖先であるコミットγからの、それぞれの変更履歴をマージする」という感じになるでしょうか。