Git入門:バージョン管理のきほん

チームでのコンフリクトを減らす

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • コンフリクトが起きる条件と、確率を上げてしまう習慣
  • ブランチを短命にする、こまめに main を取り込むという 2 つの予防
  • 改行コードやフォーマッタを揃えて、無意味な差分を消す方法

解決するより起こさないほうが速い

コンフリクトを解決する では、起きてしまったコンフリクトの直し方を扱いました。このレッスンは別の話です。そもそも起こさないための習慣を扱います。

コンフリクトの解決は、慣れれば数分で終わります。しかし本当のコストは時間ではありません。両方の変更の意図を読み解いて正しい形を作る作業なので、判断を間違えると他人の修正を無言で消してしまいます。しかも消えたことに誰も気づきません。回数が増えるほど事故の確率が上がります。

だから目標は「速く直せるようになる」ではなく「起きる回数を減らす」です。

コンフリクトが起きる条件

Git がコンフリクトを出すのは、次の条件が同時に成立したときだけです。

  1. 2 つのブランチが、共通の分岐点から別々に進んだ
  2. 同じファイルの、近い行を両方が変更した
  3. それを 1 つにまとめようとした

裏を返すと、次の 3 つのどれかを崩せば起きません。

条件崩し方
分岐が長いブランチを短命にする
差が開いているこまめに main を取り込む
同じ行に触る触る範囲を分ける、道具で差分を減らす

Git は「近い行」の判定が少し広めです。同じ行を直接編集していなくても、すぐ上下の行が両方で変わっていればコンフリクトになります。1 つのファイルを 2 人で同時に触っている時点で、確率はかなり高いと考えてください。

ブランチを長生きさせない

最も効く対策がこれです。ブランチが存在する期間が長いほど、その間に main へ入る他人の変更が増え、差が開きます。

3 日で終わるブランチと 3 週間かかるブランチでは、コンフリクトの起きやすさが単純な比例では済みません。main に入るコミット数が増えるうえ、自分の変更も大きくなるので、衝突する組み合わせが掛け算で増えていきます。

短く終わらせるには、作業の切り方を変えます。

  • 動く最小単位に割る — 画面全体ではなく、まず表示だけ、次に入力、次に保存
  • 見た目と中身を分ける — 整形やリネームは機能の変更と別 PR にする
  • 未完成でも出す — 使われていないコードを先に入れておき、あとから呼び出しをつなぐ

3 つ目は少し意外に見えますが、実務ではよく使われます。新しい関数やコンポーネントを追加するだけの PR は、既存コードにほとんど触らないのでコンフリクトしません。呼び出し側の書き換えだけを別 PR にすれば、差分が小さくなりレビューも速くなります。

「完成してからまとめて出す」は、レビューが遅くなるだけでなく、コンフリクトの温床になります。プルリクエストの粒度は プルリクエストを出す でも扱いました。

こまめに main を取り込む

ブランチを切ったら、そこから先は他人の変更を自動では受け取りません。放っておくと、自分の手元だけが古い main を基準にした世界になります。

これを防ぐには、1 日 1 回を目安に main を自分のブランチへ取り込みます。

ターミナル

$ git switch main $ git pull $ git switch feature/login-form $ git merge main

git pull は、リモートの main の最新を取ってきて手元の main に反映するコマンドです。そのうえで作業ブランチに戻り、main の内容をマージします。

1 行で済ませたいときは、作業ブランチにいるまま次のようにも書けます。

ターミナル

$ git fetch origin $ git merge origin/main

origin/main は、最後に取得した時点でのリモート側の main を指す名前です。git fetch はリモートの状態を取ってくるだけで、手元のブランチは変えません。

この作業を毎日やると何が変わるのか。コンフリクトが起きなくなるわけではありません。起きるコンフリクトが小さくなり、記憶が新しいうちに解決できるようになります。昨日の自分と昨日の同僚の変更なら、どちらの意図も覚えています。3 週間分がまとめて来ると、そうはいきません。

取り込みは必ず、自分の作業がコミット済みの状態でやってください。編集途中のファイルが残っているとマージが止まります。中断したくないときは 作業を一時退避する(stash) の方法で退避します。

rebase を使う場合

チームが履歴を一直線に保つ方針なら、merge の代わりに rebase を使います。

ターミナル

$ git fetch origin $ git rebase origin/main

自分のコミットを、最新の main の先端に置き直す操作です。詳しくは rebase で履歴を整える を見てください。

注意点が 1 つあります。rebase はコミットを作り直すので、すでに push 済みのブランチでやると、リモートと食い違って通常の push が拒否されます。自分しか触っていないブランチなら git push --force-with-lease で更新できますが、他人が同じブランチで作業しているときは絶対にやってはいけません。相手の手元の作業が行方不明になります。

無意味な差分を作らない

コンフリクトの中には、内容の対立がまったくないものがあります。改行コードの違いや、自動整形の設定が人によって違うせいで起きるものです。これは道具をそろえるだけで消えます。

改行コードをそろえる

Windows は改行を CRLF、Mac と Linux は LF で表します。何もしないと、Windows の人がファイルを保存しただけで全行が変更扱いになります。

リポジトリの直下に .gitattributes を置いて、扱いを固定します。

プレーンテキスト

* text=auto eol=lf *.png binary *.jpg binary

1 行目は「テキストファイルはリポジトリ内では LF で持つ」という指定です。この設定はリポジトリに入るので、チーム全員に同じルールが適用されます。個人設定の core.autocrlf に頼るより確実です。

すでに CRLF で入ってしまったファイルがあるときは、設定を入れたあとに一度正規化します。

ターミナル

$ git add --renormalize . $ git commit -m "改行コードを LF に統一"

フォーマッタと lint をそろえる

もう 1 つの原因は自動整形です。A さんのエディタはセミコロンを付け、B さんのは付けない。この状態で同じファイルを触ると、実質的な変更が 1 行でも差分は数十行になり、そのすべてがコンフリクトの候補になります。

対策は、設定ファイルをリポジトリに入れて全員が同じ結果になるようにすることです。JavaScript や TypeScript なら Prettier と ESLint の設定を置きます。

プレーンテキスト

プロジェクト直下 .prettierrc .eslintrc.json .editorconfig

.editorconfig はエディタの種類を問わず読まれる設定で、インデント幅や末尾の改行を指定できます。

そのうえで、整形が実行されたかを機械的に確かめる仕組みを入れておくと確実です。この確認を自動でやる方法は GitHub Actions で自動化のさわり で扱います。

生成物や依存ライブラリをリポジトリに入れているなら、それも見直してください。node_modules やビルド後のファイルは中身が頻繁に変わるうえに巨大で、コンフリクトの発生源になります。追跡しないファイルを決める(.gitignore) の考え方で除外します。

コンフリクトしやすいファイルを知っておく

経験上、衝突が集中するファイルはだいたい決まっています。心当たりがあれば、扱いを先に決めておいてください。

ファイルなぜ衝突するか対処
package-lock.json依存を足すたびに広い範囲が変わる手で直さず、マージ後に再生成する
ルーティング定義全員が末尾に 1 行足す追加位置を分野ごとに分ける
定数や設定の一覧同じ配列の末尾に追記が集中するファイルを分割する
翻訳ファイル同じキー付近に全員が触る機能ごとにファイルを分ける

package-lock.json は特に厄介です。中身を人が読んで正しい形を作るのは現実的ではないので、コンフリクトしたら再生成します。

ターミナル

$ git checkout --theirs package-lock.json $ npm install $ git add package-lock.json

末尾への追記が集中するファイルは、ファイルを分けるだけで問題が消えます。1 つの巨大な配列を 5 つのファイルに割れば、5 人が同時に触っても衝突しません。「コンフリクトが多い」は、設計を見直す合図でもあります。

よくある失敗

ここで実際にありがちな失敗を 1 つ挙げます。2 週間かけた大きなブランチを、最後に main へマージしようとして 30 ファイルがコンフリクトするケースです。

このとき最もやりがちなのが、片側を丸ごと採用して片付けようとすることです。

ターミナル

$ git checkout --ours src/api.js

--ours は自分側の内容をそのまま採用するという意味です。一見すると解決したように見えますが、この 2 週間に他の人が src/api.js に入れた修正は、すべて消えます。しかもコンフリクトは解消されているので、Git は何も警告しません。バグとして現れるのは数週間後です。

正しい手順は、コンフリクトしたファイルを 1 つずつ開いて、両方の意図を残す形に手で直すことです。時間はかかりますが、それが 30 ファイル分かかるという事実こそが問題です。

そしてこの状況は、2 週間の間に 1 日 1 回 main を取り込んでいれば、そもそも起きませんでした。毎日 1〜2 ファイルずつ、記憶の新しいうちに片付いていたはずです。予防が効くのはこういう場面です。

--ours--theirs は、生成物のように「片方を採ればよい」と確信できるファイルにだけ使ってください。人が書いたコードには使いません。

人の側の調整

最後は技術ではない部分です。同じファイルを同時に触ることが分かっているなら、事前にひとこと言うだけで避けられます。

  • 大きなリファクタに入る前に、対象ファイルをチームに共有する
  • 誰かが触っている最中のファイルは、急ぎでなければ待つ
  • 触らざるを得ないときは、先に相手のブランチをマージしてから始める

チームによっては、朝会で「今日どこを触るか」を一言ずつ共有しています。地味ですが、.gitattributes より効きます。

この章のポイント
  • コンフリクトは「分岐が長い」「差が開く」「同じ行を触る」の 3 条件で起きる。どれかを崩せば起きない
  • ブランチは 1〜3 日で終わらせる。大きい作業は動く最小単位に割って複数の PR にする
  • 1 日 1 回 main を取り込む。起きるコンフリクトが小さくなり、記憶が新しいうちに解決できる
  • .gitattributes で改行コードを、フォーマッタ設定で書式をそろえ、内容のない差分をなくす