さらに優れた プルリクエスト

Git を使っているならば、恐らく プルリクエスト を使用していることでしょう。プルリクエストは、DVCS の黎明期から、何かしらの形で存在してきました。Bitbucket と GitHub がオシャレな Web UI を構築する以前、プルリクエストと言えば、自分のリポを少し変更してほしいという友人のアリスからのメールによるプルリクエストを指したかもしれません。そうして、問題なさそうであればいくつかのコマンドを実行して、master ブランチに変更内容をプルします:

Screen Shot 2015-02-16 at 12.14.10 PM

もちろん、アリスの変更点をランダムに master にプルするだけでは、 あまり良い アイディアとは言えません。master は、お客様に出荷予定のコードを表しているため、マージ内容については注意する必要があるでしょう。master にプルするよりは、別途のブランチにプルダウンして、マージする前に変更内容を詳しく調べる方がお勧めです:

rsz_screen_shot_2015-02-16_at_121707_pmgit diff の「トリプルドット」構文を使用すると、alice/master の先端とそのマージ元 (または共通の先祖) と、私たちのローカルの master ブランチとの変更点を示してくれます。こうして、アリスが私たちにプルしてほしい、全ての変更内容を効果的に示すことができます。

git diff master...alice/master は、git diff A B と同等
git diff master…alice/master は、git diff A B と同等

一見、これはプルリクエストにまつわる変更内容をレビューできる、合理的な方法のように思えます。現に書込み時において、大半の git ホスティングツールはこの方法を利用して、そのプルリクエストの文書比較アルゴリズムを実装しているようです。

しかし、プルリクエスト用に diff を生成する場合、「トリプルドット」diff アプローチを使用するといくつかの問題が生じます。実際のプロジェクトでは、master ブランチは任意の feature ブランチから著しく分岐します。他のデベロッパーも、自分たちのブランチを作業して、それを master にマージするわけです。一旦 master が進展すると、feature ブランチの先端からマージ元までの単純な git diff では、もはや両者の間にある実際の差分を適切に示したものにはなりません。ここで見ているのは、単にブランチの先端と master の古いバージョンの差分に過ぎないのです。

triple-dot-diff-missing (1)
「トリプルドット」 git diff master…alice/master は、master への変更内容を考慮しない

 

これらの変更内容をプルリクエスト diff で見れない事が、なぜ問題なのでしょうか。その理由は、2つあります。

マージのコンフリクト

1つ目の問題は、比較的普段から遭遇するもの、即ちマージのコンフリクトです。master 上でも修正されたファイルを、フィーチャーブランチ上でも修正した場合、git diff はあなたのフィーチャーブランチ上で行われた変更内容を引き続き示してくれます。他方、git merge はエラーを吐き出して、ワーキングコピー上のあらゆる箇所にコンフリクトマーカーをまき散らし、あなたのブランチに一致不能な差分が存在すると表示します。これは、少なくとも git の洗練されたマージ戦略の許容範囲を超えたものだと言えるでしょう。
merge-conflict
マージコンフリクトの解決を 楽しむ 人は存在しませんが、これはもはや全てのバージョン管理システムにとって日常的な現実となっています。この場合のバージョン管理システムというのは、少なくともファイルレベルのロック (これには固有の問題があり) に対応していないものを指します。

ただし、プルリクエスト用に「トリプルドット」git diff を使用した際に直面するかもしれない、もう一つのコンフリクトと比べれば、マージコンフリクトの方が遥かに好まれる傾向にあります。この特殊な類いの論理コンフリクトは綺麗にマージするものの、あなたのコードベースに微妙なバグを取り込む場合があります。

綺麗にマージする論理コンフリクト

デベロッパーが、同一ファイルの異なる箇所を異なるブランチで修正した場合、問題を抱えることになるかもしれません。独立して動作し、コンフリクト無し に上手くマージしたように見える異なる変更点が、実は組み合わさると論理バグを作り出すというケースがあります。

これはいくつかの方法をとりますが、二人以上のデベロッパーが、異なるブランチ上で偶然にも同じバグに気付いてこれを修正するというのはよくあるケースです。
航空券の価格を計算するための以下の JavaScript を考察してみましょう。

rsz_screen_shot_2015-02-16_at_123602_pm

ここには、明らかにバグが存在します。作成者は、計算式に通関手数料を含めるのを忘れていますね!

ここで、二人のデベロッパー、アリスとボブが、それぞれこのバグに気付いて、2つの異なるブランチ上で個別に修正したとしましょう。

アリスは、immigrationFee の前に customsFee を加えます:

rsz_screen_shot_2015-02-16_at_123820_pm

そして、ボブも似たような修正を行いますが、immigrationFee の行の後に加えます:

rsz_screen_shot_2015-02-16_at_124040_pm

各ブランチ上で異なる行が修正されたため、この 2 つのブランチは両方とも、1 つまた 1 つと master に綺麗にマージされていきます。しかし、これでは master両方の行 を有することになります。以下では、深刻なバグがお客様から関税手数料を 2 度チャージしてしまいます:

rsz_screen_shot_2015-02-16_at_124207_pm

(これは明らかに大げさな例ですが、コードや論理の複製は、かなり深刻な問題をもたらし得るのです: goto fail; の話、聞いた事はありませんか?)

最初にアリスのプルリクエストを master にマージしたものと仮定して、ブランチの先端から共通の先祖へと「トリプルドット」git diff を使用した場合、ボブのプルリクエストは次のように表示されます:

rsz_screen_shot_2015-02-16_at_124323_pm

先祖に対する diff をレビューしているため、マージボタンを押した時に生じる、差し迫った悪夢に関して一切の警告がありません。

プルリクエストで本当に見たいのは、ボブのブランチをマージした際に master がどのように変更するのかという点です:

rsz_screen_shot_2015-02-16_at_124438_pm

この diff では、問題が明確に表されています。プルリクエストのレビュワーは複製された行を見つけることができ (そうでないと困りますね)、コードにはまだ作業が必要であることをボブに伝え、深刻なバグが master、ひいては本番環境に到達してしまう事態を防げるのです。

Bitbucket と Stash において、私たちはこのような方法でプルリクエストの diff を実装することに決めました。プルリクエストを見る時は、その結果として起こるマージコミットがどのように表れるかを想定しているのです。これを、私たちは舞台裏でマージコミットを作成し、それとターゲットブランチの先端の差分を示すことで行っているのです:

D が マージコミット である時、git diff C D は 2 つのブランチの 全ての 差分を示す
Dマージコミット である時、git diff C D は 2 つのブランチの 全ての 差分を示す

同じリポジトリを他のいくつかのホスティングプロバイダにプッシュしてあるので、興味がある人は動作中の異なる diff アルゴリズムを確認できます:

Bitbucket と Stash で使われている「マージコミット」diff は、マージした際に適用される 実際の変更内容 を示しています。これには裏があって、実装がより難しく、また実行にもより多くのコストがかかるということです。

移動するターゲット

1つ目の問題はマージコミット D がまだ実際には存在しないという点です。その上、マージコミットの作成は結構コストのかかる行程なのです。2つ目の問題は、単に D を作成してハイ終了、とはいかない点にあります。私たちのマージコミットの親である BC は、いつ変わってもおかしくありません。これら親への変更を、私たちはプルリクエストの リスコーピング (rescoping) と呼んでいますが、それはプルリクエストがマージされた際に適用される diff を効果的に変更できるからです。あなたのプルリクエストが master のような変更の多いブランチをターゲットにしている場合、それは非常に頻繁にリスコーピングされていると考えてよいでしょう。

マージコミットは、いずれかの ブランチが変更された際、いつでも作成されます。
マージコミットは、いずれかの ブランチが変更された際、いつでも作成されます。

実際、誰かが master もしくは あなたのフィーチャーブランチへとブランチのプッシュやマージを行う度に、Bitbucket か Stash は正確な diff を示そうとして、新たなマージを計算しなくてはならない可能性が高いのです。

マージコンフリクトの取扱い

プルリクエスト diff を生成するためにマージすることのもう1つの問題は、何かとマージコンフリクトを処理する必要が出てくるということです。あなたの git サーバーは非対話型で実行中のため、これを解決できる人が誰もいません。こうなると話はややこしくなりそうなものですが、これが実際には利点になるのです。私たちは Bitbucket と Stash において、実際にコンフリクトマーカーをマージコミット D の一部としてコミットした後、diff で記してプルリクエストがどのようにコンフリクトしているかを示します:

Bitbucket と Stash diff では: 緑 の行が追加、 赤 の行が削除、そして オレンジ の行がコンフリクトを意味します。
Bitbucket と Stash diff では: の行が追加、 の行が削除、そして オレンジ の行がコンフリクトを意味します。

これは、あなたのプルリクエストがコンフリクトしていることを事前に検知できるだけでなく、コンフリクトの解決方法についてレビュワーが話し合えることも意味します。コンフリクトには常に少なくとも 2 当事者が関わっているため、私たちは適切な解決策の判断にはプルリクエストが最適だと考えています。

複雑性とコストが増すものの、Stash と Bitbucket でとった方法は最も正確かつ便利なプルリクエスト diff だと思います。質問あるいはフィードバックのある方は、コメントもしくは Twitter でお知らせください。よろしければ (@kannonboy) で私をフォローして、Git や Bitbucket のアップデート、そしてその他の素晴らしい話をゲットしてください。


*本ブログは Atlassian Developers の翻訳です。本文中の日時などは投稿当時のものですのでご了承ください。
*原文 : 2015 年 1 月 22 日投稿 "A better pull request"