Bundle update *gems で何が起きる？

## はじめに

ここ数日になって、Ruby project に関わり始めた新米が 「``Bundle update ***`` すると実際に何が起きるんだ？」 という疑問について調べてまとめてみました。

> 間違ってるよ、とか、公式のここにちゃんと書いてあるよ、等あればやさしく教えて下さい m(_ _)m

## (そもそも) Bundle update *gems とは？

``bundle update rails`` の様に、指定した GEM をアップデートする際に使用します。
実際にはアップデートされるのは指定した GEM だけではなく以下の2点になります。

1. 指定した GEM (↑のケースでは ``rails``)
2. 指定した GEM の 全ての依存 ([rails の場合 actioncable, railties, ...](https://rubygems.org/gems/rails/versions/6.1.4.1) ) と、その子孫

https://bundler.io/man/bundle-update.1.html#UPDATING-A-LIST-OF-GEMS

> For instance, in the scenario above, imagine that nokogiri releases version 1.4.4, and you want to update it without updating Rails and all of its dependencies. To do this, run bundle update nokogiri.
>
> Bundler will update nokogiri and any of its dependencies, but leave alone Rails and its dependencies.

指定した GEM のみを上げたい (不必要に依存は上げたくない) 場合は ``--conservative`` オプションを使用します。

```sh
bundle update --conservative rails
```

## 説明の前提

以降、以下のような GEM, プロジェクトがある〜という前提で話を進めます。

### Gems

![Bunder.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/8f3e77f0-550f-ec1d-92bb-570fa30f5ee7.png)

| GEM Name | Version | 依存 GEM |
|:----|:---:|:----|
| gem-common | 1.0 | -- |
|            | 1.1 | -- |
|            | 2.0 | -- |
| gem-a      | 1.0 | gem-common (= 1.0) |
|            | 1.1 | gem-common (= 1.1) |
|            | 2.0 | gem-common (= 2.0) |
| gem-b      | 1.0 | gem-common (~> 1.0) |
|            | 1.1 | gem-common (~> 1.0) |
|            | 2.0 | gem-common (~> 2.0) |

### Gemfile (.lock)

既にプロジェクトに ↓ の Gemfile (.lock) があるものとします。

Gemfile

```yaml:Gemfile
source 'https://rubygems.org'

gem 'gem-a'
gem 'gem-b'
```

Gemfile.lock

```Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    gem-common (1.0)
    gem-a (1.0)
      gem-common (= 1.0)
    gem-b (1.0)
      gem-common (~> 1.0)
```

> この .lock ファイルが作られた当時は、``1.0`` が最新だったのでしょう、という仮定。

## 検証 (ケーススタディ)

### 1-1. Gemfile を変更しないで bundle update gem-a

Gemfile を変更せずに ``bundle update gem-a`` をします。

```diff:Gemfile
source 'https://rubygems.org'

gem 'gem-a'
gem 'gem-b'
```

結果、 ``gem-a`` は ``1.1`` に、``gem-common`` も ``1.1`` にアップデートされます。

> ``2.0`` にはなりません！ ``gem-b`` の依存 ``gem-common (~> 1.0)`` を満たさないからです。

```diff:Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
-   gem-common (1.0)
-   gem-a (1.0)
-     gem-common (= 1.1)
+   gem-common (1.1)
+   gem-a (1.1)
+     gem-common (= 1.1)
    gem-b (1.0)
      gem-common (~> 1.0)
```

### 1-2. Gemfile を変更しないで bundle update gem-b

Gemfile を変更せずに ``bundle update gem-b`` をします。

```diff:Gemfile
source 'https://rubygems.org'

gem 'gem-a'
gem 'gem-b'
```

結果、 ``gem-b`` は ``1.1`` にアップデートされます。

> ``gem-common`` は元のバージョンのままです。
> ``gem-common`` がアップデートされると ``gem-a`` の依存  ``gem-common (= 1.0)``  に違反するからです。

```diff:Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    gem-common (1.0)
    gem-a (1.0)
      gem-common (= 1.0)
-   gem-b (1.0)
+   gem-b (1.1)
      gem-common (~> 1.0)
```

### 2-1. バージョン ``2.0`` を指定して bundle update gem-a

バージョン ``2.0`` を指定して ``bundle update gem-a`` をします。

```diff:Gemfile
   source 'https://rubygems.org'
   #
-  gem 'gem-a'
+  gem 'gem-a', '= 2.0'
   gem 'gem-b'
```

結果、以下のエラーが発生して、アップデート (.lock ファイルの更新) はされません。

``gem-a`` の依存 ``gem-common`` を ``2.0`` にしたいが、それは ``gem-b`` (**※アップデート対象ではない！**) の依存 ``gem-common (~> 1.0)`` に違反するからです。

```
Bundler could not find compatible versions for gem "gem-common":
  In Gemfile:
    gem-a (= 2.0) was resolved to 2.0, which depends on
      gem-common (= 2.0)

    gem-b was resolved to 1.0, which depends on
      gem-common (~> 1.0)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
```

この場合は、コマンドを ``bundle update gem-a gem-b`` に変えて、``gem-a``, ``gem-b`` 両方ともアップデート対象にします。

```diff:Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
-   gem-common (1.0)
-   gem-a (1.0)
-     gem-common (= 1.0)
-   gem-b (1.0)
-     gem-common (~> 1.0)
+   gem-common (2.0)
+   gem-a (2.0)
+     gem-common (= 2.0)
+   gem-b (2.0)
+     gem-common (~> 2.0)
```

無事に両方とも ``2.0`` にアップデートされました。

> つまり、Gemfile の ``gem 'gem-a', '= 2.0'`` バージョン指定は不必要だった (``gem 'gem-a'`` のままでいい) という事になります。