git mergeに強くなったCargo.lock V2を先取りして試す

RustのパッケージマネージャCargoを使っていると、自動生成ファイルである `Cargo.lock` のマージで困ることがあります。この問題を緩和するための実験的なフォーマットが最近実装されたため、本稿ではそれを試してみます。

**注意: 本稿で説明されている機能は2019年10月時点では実験的なものであり、今後のサポートが保証されているものではありません。**その点を理解し、問題ないか確認した上で試してください。

## Cargo.lockとは

`Cargo.lock` は `Gemfile.lock` や `yarn.lock` と同様、依存解決後の依存グラフのスナップショットを保存するためのものです。各パッケージのもつ情報 (`Cargo.toml`) には直接の依存関係に関する制約 (例: `>=1.7.2 <2.0.0`)しか書いていないので、再現性のあるビルドのためには不十分です。そこで、ビルド時に生成される `Cargo.lock` を使うことで、間接依存を含めた依存パッケージの更新タイミングをプログラマが明示的にコントロールできます。

最終成果物であるバイナリクレートでは `Cargo.lock` をgit(等のVCS)の管理下に置くのが推奨されています。ライブラリの場合はVCSの管理下に置かないほうが一般的です。

## Cargo.lock V2

一般論として、lockfileのような自動生成されたファイルはVCSのマージ機能で壊れやすく、手動マージか再生成が必要になることが多いです。とはいえ、出力フォーマットを工夫することによってマージしやすさを上げることは可能なはずです。

そこで最近Alex Crichton氏が[Cargo.lockのフォーマットを改善する実験的な機能](https://github.com/rust-lang/cargo/pull/7070)を実装しました。

具体的には以下のような変更点があります。

- チェックサムを集約していた `[metadata]` セクションを廃止し、チェックサムを各パッケージのセクションで記述するようにする。
- 各パッケージの直接依存関係の情報を簡略化する。曖昧性がない場合にはバージョンや取得元の情報を省略する。

これにより全体としての情報量は維持しつつ、生成される差分が関係するパッケージに集約されやすくなります。

## Cargoにパッチを当ててビルドする

現在のCargoにはCargo.lock V2のサポートが入っていますが、Cargo.lock V2を生成するための機能は(おそらく意図的に)用意されていません。

そこでまずCargoにパッチを当てた独自バージョンを作ります。

### 依存関係

まず[README](https://github.com/rust-lang/cargo/blob/0.39.0/README.md)に書いてある依存関係を入れます。(RustとCargoは入ってる前提で進めます)

```console
$ sudo apt install git python3-minimal curl libssl-dev
```

### Cargoソースの取得

cargoをクローンします。

```console
$ git clone http://github.com/rust-lang/cargo.git
$ cd cargo
```

masterをビルドしてもいいですが、今回は念のため手元のcargoと同じバージョンで作業します。手元のcargoのバージョンを確認します。

```console
$ cargo --version
cargo 1.38.0 (23ef9a4ef 2019-08-20)
```

ここで `1.38.0` と記載されているのはRustのバージョンですが、隣の `23ef9a4e` はCargoのバージョンです。これをcheckoutします。

```console
$ git checkout 23ef9a4e
```

### パッチを当てる

以下のパッチを当てます。

```diff:cargo-lock-v2.patch
diff --git a/src/cargo/core/resolver/resolve.rs b/src/cargo/core/resolver/resolve.rs
index 9ced48f4d..72619c61e 100644
--- a/src/cargo/core/resolver/resolve.rs
+++ b/src/cargo/core/resolver/resolve.rs
@@ -378,7 +378,7 @@ impl ResolveVersion {
     /// previous `Cargo.lock` files, and generally matches with what we want to
     /// encode.
     pub fn default() -> ResolveVersion {
-        ResolveVersion::V1
+        ResolveVersion::V2
     }
 
     /// Returns whether this encoding version is "from the future".
@@ -387,7 +387,7 @@ impl ResolveVersion {
     /// intended to become the default "soon".
     pub fn from_the_future(&self) -> bool {
         match self {
-            ResolveVersion::V2 => true,
+            ResolveVersion::V2 => false,
             ResolveVersion::V1 => false,
         }
     }
```

```console
$ patch -p1 < cargo-lock-v2.patch
```

### ビルドする

```console
$ cargo build --release
```

これで `./target/release/cargo` にV2出力可能なcargoが生成されます。間違えて使ってしまわないためにも、installはしないほうがいいと思います。

## 新しいCargo.lockを生成する

さっそく今いるディレクトリで試してみましょう。CargoのソースコードはCargo.lockをVCSに入れてないので、比較用に元のCargo.lockをバックアップしておきましょう。

```console
$ cp Cargo.lock Cargo.lock.old
$ target/release/cargo fetch
$ diff Cargo.lock.old Cargo.lock
```

## マージの挙動を確認する

ripgrepで試してみます。「serdeのバージョンを上げたブランチ」と「serde_deriveのバージョンを上げたブランチ」を作成し、マージをしてみます。

### ソースを取得

```console
$ git clone https://github.com/BurntSushi/ripgrep.git
$ cd ripgrep
$ git checkout 11.0.2
```

### Cargo.lock V1でマージしてみる

```console
$ git checkout -b branch1
$ git branch branch2
$ cargo update -p serde && git add Cargo.lock && git commit -m "Bump serde"
$ git checkout branch2
$ cargo update -p serde_derive && git add Cargo.lock && git commit -m "Bump serde_derive."
$ git merge branch1  # FAIL
$ git merge --abort
```

### Cargo.lock V2でマージしてみる

まずCargo.lockをV2に上げて、それをbranch3とbranch4に分岐させてみます。

```console
$ git checkout 11.0.2
$ git checkout -b branch3
$ PATH/TO/cargo/target/release/cargo fetch && git add Cargo.lock && git commit -m "Use Cargo.lock V2"
$ git branch branch4
$ cargo update -p serde && git add Cargo.lock && git commit -m "Bump serde"
$ git checkout branch4
$ cargo update -p serde_derive && git add Cargo.lock && git commit -m "Bump serde_derive."
$ git merge branch3  # SUCCESS
```

## 手元で試すのが面倒な人向け

rust-lang/rustでは既に使われていて、以下で差分を眺められます。

- [rust-lang/rustでの例](https://github.com/rust-lang/rust/commit/093ede240ab8bb4ec6720301619dda831b5690fd)

## まとめ

- Cargo.lockは自動生成されるファイルで、その性質上git mergeのコンフリクトで困る場合がある。
- 新フォーマットが正式採用されれば、コンフリクトする確率が減ることが期待される。
- すでに実装自体は入っているので、頑張れば試せる。ただし、実験的なものなので注意が必要。