コンフリクトしたschema.rbをきれいにマージする手順


## はじめに

複数のメンバーで同じRailsアプリケーションを開発しているとコードのコンフリクトがときどき発生します。
普通のファイルは人間が目で見てどうマージすべきか判断すればいいですが、schema.rbのようにRailsによって自動更新されるファイルは手で修正しない方が良いです。

じゃあschema.rbがコンフリクトしたらどうしたらいいの！？という人のためにマージする手順を説明します。

### 対象となるバージョン

本記事は以下のバージョンを対象としています。

- Rails 5.0.2
- Git 2.12.2

### サンプルコード

この手順で使ったRailsアプリケーション（マージがすべて終わった状態）はGitHubにアップしてあります。

https://github.com/JunichiIto/schema-rb-sandbox

## 想定するシナリオ

ここでは以下のようにアリスとボブが同じテーブル（同じモデル）に対して異なるカラムを追加しようとする状況を想定します。

- アリス = Blogモデルに`author_name`カラムを追加
- ボブ = Blogモデルに`published_at`カラムを追加

二人は次のような順番（時間軸）で作業を実施しました。

| アリス                        | ボブ                           |
|-------------------------------|--------------------------------|
| add-author-nameブランチを作成 |                                |
| author_nameカラムを追加       |                                |
|                               | add-published-atブランチを作成 |
|                               | published_atカラムを追加       |
| GitHubにpush                  |                                |
| pull request作成                  |                                |
| masterにマージ                |                                |
|                               | GitHubにpush                   |
|                               | pull request作成                   |
|                               | <<コンフリクトしてると言われた！>> |

ご覧のとおり、二人が同じテーブルに対してマイグレーションを実行してしまったため、後からpull requestを作成したボブは「schema.rbがコンフリクトしてるから自動マージできないよ！」と怒られました。

ではボブはここからどうすればいいでしょうか？
次からその手順を説明します。

## 手順

### masterブランチをチェックアウトしてgit pullする

add-published-atブランチからmasterブランチに切り替え、GitHubから最新のコードをpullします。

```
$ git checkout master
$ git pull
```

### マイグレーションのステータスを確認

`rails db:migrate:status`コマンドを実行してマイグレーションのステータスを確認します。

```
$ rails db:migrate:status

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20170426032205  Create blogs
  down    20170426032325  Add author name to blogs
   up     20170426032438  ********** NO FILE **********
```

Statusの列がupになっていれば、すでに`rails db:migrate`で実行されたマイグレーションであることを表しています。
downであれば逆にまだ実行していないマイグレーションであることを表しています。

また、Migration Nameの列が"NO FILE"になっているものは、「db/migrateディレクトリに該当するマイグレーションファイルがないこと」を表しています。

よって、現在の状況は以下のようになっています。

- 2つ目のマイグレーションはまだローカルでは実行されていない
- 3つ目のマイグレーションは実行されているが該当するマイグレーションファイルが（masterブランチには）ない

これが1件目のマイグレーションのようにすべて「upかつMigration Nameあり」にするのがこの手順のゴールです。

### 開発用ブランチに切り替えてmasterブランチをマージする

次に自分の開発用ブランチ（今回はadd-published-atブランチ）に切り替えます。
ついでにマイグレーションのステータスも確認しておきましょう。

```
$ git checkout add-published-at
$ rails db:migrate:status

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20170426032205  Create blogs
  down    20170426032325  Add author name to blogs
   up     20170426032438  Add published at to blogs
```

ブランチを切り替えたので、3件目のマイグレーションでMigration Nameが表示されるようになりました。
ただし、2件目のマイグレーションは未実行なのでdownのままです。

続いて、masterブランチをマージします。
ですが、当然コンフリクトが発生します。

```
$ git merge master

Auto-merging db/schema.rb
CONFLICT (content): Merge conflict in db/schema.rb
Automatic merge failed; fix conflicts and then commit the result.
```

ちなみにマージを試みたあとのschema.rbはこんなふうになっています。

```
<<<<<<< HEAD
ActiveRecord::Schema.define(version: 20170426032438) do
=======
ActiveRecord::Schema.define(version: 20170426032325) do
>>>>>>> master

  create_table "blogs", force: :cascade do |t|
    t.string   "title"
    t.text     "content"
<<<<<<< HEAD
    t.datetime "created_at",   null: false
    t.datetime "updated_at",   null: false
    t.datetime "published_at"
=======
    t.datetime "created_at",  null: false
    t.datetime "updated_at",  null: false
    t.string   "author_name"
>>>>>>> master
  end

end
```

### schema.rbの変更を取り消して元に戻す

schema.rbがコンフリクトして内容がおかしくなってしまったので、次の手順でいったん変更をリセットして元に戻します。

```
$ git reset HEAD db/schema.rb

Unstaged changes after reset:
M	db/schema.rb

$ git checkout db/schema.rb
```

するとschema.rbは「`published_at`カラムだけが追加された状態」になります。

```schema.rb
ActiveRecord::Schema.define(version: 20170426032438) do

  create_table "blogs", force: :cascade do |t|
    t.string   "title"
    t.text     "content"
    t.datetime "created_at",   null: false
    t.datetime "updated_at",   null: false
    t.datetime "published_at"
  end

end
```

### マイグレーションを実行する

schema.rbを元に戻したら、マイグレーションを実行します。
これで未実行だった2件目のマイグレーション（`author_name`カラムの追加）が実行されます。

```
$ rails db:migrate

== 20170426032325 AddAuthorNameToBlogs: migrating =============================
-- add_column(:blogs, :author_name, :string)
   -> 0.0109s
== 20170426032325 AddAuthorNameToBlogs: migrated (0.0110s) ====================
```

マイグレーションのステータスはすべてupになりました。

```
$ rails db:migrate:status

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20170426032205  Create blogs
   up     20170426032325  Add author name to blogs
   up     20170426032438  Add published at to blogs
```

schema.rbも無事に`published_at`カラムと`author_name`カラムが追加された状態になっています。

```schema.rb
ActiveRecord::Schema.define(version: 20170426032438) do

  create_table "blogs", force: :cascade do |t|
    t.string   "title"
    t.text     "content"
    t.datetime "created_at",   null: false
    t.datetime "updated_at",   null: false
    t.datetime "published_at"
    t.string   "author_name"
  end

end
```

### マージを続行する

これでコンフリクトが解消したので、schema.rbをaddして、マージを続行します。
（`git merge`の`--continue`オプションはGit 2.12から導入されたオプションです。）

```
$ git add db/schema.rb
$ git merge --continue

[add-published-at 505033f] Merge branch 'master' into add-published-at
```

これでmasterブランチからadd-published-atブランチへのマージは完了しました。

### masterブランチに切り替えて開発用ブランチをマージする

さて、いよいよ最後のステップです。
masterブランチに切り替えて開発用ブランチ（add-published-atブランチ）をマージします。

```
$ git checkout master
$ git merge --no-ff add-published-at

Merge made by the 'recursive' strategy.
 db/migrate/20170426032438_add_published_at_to_blogs.rb | 5 +++++
 db/schema.rb                                           | 7 ++++---
 2 files changed, 9 insertions(+), 3 deletions(-)
 create mode 100644 db/migrate/20170426032438_add_published_at_to_blogs.rb
```

念のためマイグレーションのステータスと、schema.rbの内容に問題がないことを確認しておきましょう。

```
$ rails db:migrate:status       

database: /Users/jit/dev/sandbox/schema-rb-sandbox/db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20170426032205  Create blogs
   up     20170426032325  Add author name to blogs
   up     20170426032438  Add published at to blogs
```

```schema.rb
ActiveRecord::Schema.define(version: 20170426032438) do

  create_table "blogs", force: :cascade do |t|
    t.string   "title"
    t.text     "content"
    t.datetime "created_at",   null: false
    t.datetime "updated_at",   null: false
    t.datetime "published_at"
    t.string   "author_name"
  end

end
```

大丈夫そうなので、GitHubにpushします。

```
$ git push origin master
```

GitHubを見に行くと、ボブが作ったpull requestは"Merged"になっているはずです。
これでボブは無事に作業を終わらせることができました。
手順は以上になります。お疲れ様でした！

## まとめ

ボブの作業内容（確認作業以外）を簡単にまとめておきましょう。

- masterブランチをpull 
  - `git checkout master`
  - `git pull`
- 開発用ブランチに切り替える 
  - `git checkout add-published-at`
- masterブランチをマージ 
  - `git merge master`
- schema.rbの変更をリセット 
  - `git reset HEAD db/schema.rb`
  - `git checkout db/schema.rb`
- マイグレーション実行 
  - `rails db:migrate`
- マージを続行 
  - `git add db/schema.rb`
  - `git merge --continue`
- masterブランチに切り替える 
  - `git checkout master`
- 開発用ブランチをマージ 
  - `git merge --no-ff add-published-at`
- GitHubにpush 
  - `git push origin master`


こんなふうにすればコンフリクトしたschema.rbをきれいにマージすることができるはずです。
ぜひ参考にしてみてください！


### 参考文献

この記事は下記ページの内容を参考にして執筆しました。
こちらのページもぜひ一度ご覧ください。

[How to Recover From Rails Database Schema Conflicts When Rebasing – dgms code](https://dgmstuart.github.io/blog/2017/04/12/how-to-recover-from-rails-database-schema-conflicts-when-rebasing/)