FlowingDev

diffとは: Gitとあらゆるコードレビューの秘伝のタレ、その正体

diffアルゴリズムが、いかにしてテキスト間の正確な追加、削除、変更を見つけ出すのか。バージョン管理とコード共同作業のコアエンジンであるその仕組みを学びましょう。

ツールを試す: 差分チェッカー

ひと言で言うと

「diff」(差分)とは、2つのファイルやテキストブロック間の正確な違いを算出した要約のことです。「変更前」から「変更後」になるために、何が追加され、何が削除され、何が変更されたのかを正確に示します。

解決する問題

1970年代初頭のコンピューティングの世界を想像してみてください。ストレージは目が飛び出るほど高価で、リモートコンピュータへの接続は眠たいカタツムリよりも遅いモデムを介して行われていました。あなたはベル研究所の開発者で、キャンパスの向こう側にあるサーバー上のソースコードファイルを更新する必要があります。ファイルは数千行の長さですが、変更したのはそのうちの3行だけです。

その糖蜜のようにトロい回線で、ファイル全体をもう一度送りますか?いやいや、そんなことはしませんよね。時間とリソースの無駄です。本当にやりたいのは、変更点だけを送ることです。

これこそが、ダグラス・マキルロイが1974年にUnixオペレーティングシステム用のオリジナルのdiffコマンドを作成するに至った、まさにその問題でした。彼の目標は、あるファイルを別のファイルに変換するために必要な、最小限の行単位の変更セットをプログラム的に見つけ出すツールを作成することでした。このdiffコマンドの出力である「パッチ」ファイルは非常に小さく、素早く送信できました。受信者は、コンパニオンプログラムであるpatchを使って、これらの変更を自分のオリジナルファイルのコピーに適用し、最新の状態に更新することができたのです。

変更そのものをデータとして分離するという、このシンプルで強力なアイデアは革命的でした。これは、Git、Subversion、Mercurialのような、あらゆるモダンなバージョン管理システムの基本的な構成要素(ビルディングブロック)です。コードレビュー、ドキュメント共同作業ツール(Googleドキュメントの「提案」モードなど)、構成管理システムの背後にあるエンジンなのです。あらゆるデジタルテキストの進化を追跡し、伝達するという中心的な問題を解決します。

舞台裏の仕組み

一見すると、diffの作成は単純に見えます。2つのテキストをスキャンして、違う部分にフラグを立てるだけ。しかし、それを効率的に行い、最小かつ最も読みやすい差分セットを生成するのは、古典的なコンピュータサイエンスの問題です。その秘訣は、何が違うかを探すのではなく、何が同じかを探すことにあります。

最長共通部分列(Longest Common Subsequence, LCS)

オリジナルのdiffを動かしていた有名なハント-マキルロイアルゴリズムを含む、ほとんどのdiffアルゴリズムは、「最長共通部分列」(LCS)問題を解くことに基づいています。

部分列(subsequence)とは、元のシーケンスと同じ順序で出現するが、必ずしも隣接している必要はないアイテムのシーケンスです。LCSは、2つのシーケンスが共通して持つ、そのような部分列の中で最も長いものです。

コードではない簡単な例を使ってみましょう。

  • オリジナル: The quick red fox
  • 新しい方: The slow red cat

アルゴリズムは、行ごと(この場合は単語ごと)に処理を行い、最長共通部分列が The red であることを見つけ出します。

LCSが見つかれば、ロジックは単純です。

  • オリジナルにあってLCSにないアイテムは、削除されたと見なされます。(quick, fox)
  • 新しいテキストにあってLCSにないアイテムは、追加されたと見なされます。(slow, cat)

共有コンテンツの最も長い岩盤(bedrock)を見つけ出すことで、アルゴリズムはその周りにある変更の島々を明確かつ簡潔に特定できるのです。この方法は、クリーンで理解しやすいdiffに必要な、最小限の差分セットを生成します。

LCSから読みやすいdiffへ

変更点を見つけるのは戦いの半分にすぎません。もう半分は、それを標準化された読みやすい形式で提示することです。GitHubのプルリクエストを見たことがあるなら、おそらくこれを目にしたことがあるでしょう。最も一般的なフォーマットは「ユニファイドdiffフォーマット」です。

少し違う例で適用してみましょう。

  • ファイルA (古い方):
    An apple a day.
    Keeps the doctor away.
    Or so they say.
    
  • ファイルB (新しい方):
    An apple a day,
    Keeps the doctor away.
    For what it's worth.
    

diffツールは、このようなものを生成します。

--- a/file_a.txt
+++ b/file_b.txt
@@ -1,3 +1,3 @@
-An apple a day.
+An apple a day,
 Keeps the doctor away.
-Or so they say.
+For what it's worth.

これを分解してみましょう。

  • --- a/file_a.txt: 「変更元」のファイル。-は削除の元を示します。
  • +++ b/file_b.txt: 「変更先」のファイル。+は追加の元を示します。
  • @@ -1,3 +1,3 @@: これは「ハンクヘッダー」です。ちょっと謎めいていますが、変更箇所のコンテキストを示します。-1,3は「このハンクは元のファイルの1行目から始まり、3行の長さである」ことを意味し、+1,3は「このハンクは新しいファイルの1行目から始まり、3行の長さである」ことを意味します。
  • スペース( )で始まる行はコンテキスト行です。両方のファイルで同一であり、変更がどこで発生したかを理解するのに役立ちます。
  • -で始まる行は削除された行です。「変更前」のテキストにのみ存在します。
  • +で始まる行は追加された行です。「変更後」のテキストにのみ存在します。

このツールは An apple a day. から An apple a day, への変更を、1行の修正としてではなく、古い行の削除と新しい行の追加として表示します。この行ベースのアプローチは、ほとんどの伝統的なdiffツールの核となる特徴です。

プレーンテキストを超えて:セマンティックdiff

標準的な行ベースのdiffは、散文やコードには最適ですが、JSON、XML、YAMLのような構造化データでは破綻してしまいます。

このJSONを考えてみましょう。

// Original
{
  "name": "Alex",
  "role": "Developer"
}

そして、こちら。

// New
{
  "role": "Developer",
  "name": "Alex"
}

テキストベースのdiffは、これを完全な削除と書き換えとして見てしまいます。

-  "name": "Alex",
-  "role": "Developer"
+  "role": "Developer",
+  "name": "Alex"

これは技術的には正しいですが、意味論的(セマンティック)には役に立ちません。JSONオブジェクトのキーの順序は、通常問題になりません。セマンティックdiffツールはもっと賢いです。まずテキストをデータ構造にパースし、次にその構造を比較します。これにより、これら2つのJSONオブジェクトが同一であることを正しく識別し、結果として差分はなしとなります。これは、テキストのフォーマットだけでなく意味を重視する設定ファイルやAPIレスポンスなどを比較する際に非常に重要です。

実録、現場のストーリー

たった1文字のバグがチェックアウトをクラッシュさせた話

あるジュニア開発者が、新しい決済プロバイダーを統合していました。彼はドキュメントからAPIリクエストの例をコピーし、自分のキーを差し込んで実行しました。失敗。もう一度試しました。また失敗。彼は何時間も自分のコードとドキュメントをにらめっこし、両者は同一だと確信していました。イライラした彼は、「動くはず」のドキュメントの例をdiffチェッカーの片側に、自分のコードをもう片側に貼り付けました。

最初は、まったく同じに見えました。しかし、彼は自分のAPIキーの行の末尾にある、かすかなハイライトに気づきました。たった一つの、目に見えない末尾のスペース。Webページからのコピー&ペーストがそれを含んでしまっており、彼のコードは忠実にそれを送信し、キーを無効にしていたのです。そのスペースを単なる一文字として認識したdiffツールだけが、それを見つけ出すことのできる唯一の「目」でした。

教訓: diffは究極の顕微鏡です。何の思い込みもなく、システムを停止させうる見えない文字を含め、そこにあるものを正確に示してくれます。

設定ドリフトの大失敗

ある高トラフィックなウェブサイトが、奇妙で断続的なエラーに見舞われ始めました。オンコール担当のエンジニア、マヤは途方に暮れていました。最後のデプロイは1週間前で、安定稼働していました。ログには明確な原因を示すものは何もありません。彼女のスパイダーセンスが、サーバー上で何かが手動で変更されたと告げていました。

彼女はGitリポジトリから公式のNginx設定ファイルを取得し、本番サーバーにSSHで接続して実際に動いている設定をコピーしました。両方をdiffツールに貼り付けました。ビンゴ。3行が異なっていました。誰かが先週、些細な問題を修正するためにサーバー上で直接「一時的な」リダイレクトルールを追加し、それをすっかり忘れていたのです。この「修正」が、新しいトラフィックパターンと衝突していました。マヤがその不正な行を削除すると、エラーは消えました。チームは即座に、サーバーの設定を毎日Gitに対して監査するポリシーを導入しました。

教訓: バージョン管理システムは信頼できる唯一の情報源(source of truth)です。現実をその情報源とdiffすることで、「設定のドリフト」を検出し、許可されていない、あるいは忘れ去られた変更を見つける最良の方法となります。

「LGTM(Looks Good To Me)」なコードレビュー

シニア開発者のベンは、新人からのプルリクエストを受け取りました。タイトルは「更新」。diffは20ファイル、3,000行以上にわたる赤と緑の海でした。そこには新機能、無関係なバグの修正、タブからスペースへの大規模なコード再フォーマット、そしてライブラリのアップグレードが含まれていました。レビューするのは不可能でした。バグ修正は正しいのか?新機能はセキュリティホールを生まないか?それは空白文字の変更という吹雪の中に隠されていました。

ベンは親切なメモを添えてPRを却下しました。「ようこそ!PRのdiffは物語を語るものです。このPRは一度に4つの異なる物語を語ろうとしています。これを4つの別々のPRに分割してもらえませんか?」新人はその通りにしました。再フォーマットのPRは即座に承認されました。バグ修正は簡単に検証できました。ライブラリのアップグレードも簡単でした。そして、新機能はついにそれ自体の価値でレビューされることができたのです。

教訓: diffの価値は、そのサイズと複雑さに反比例します。単一の論理的な変更を表す、小さく焦点の合ったdiffは、レビューしやすく、理解しやすく、後でデバッグするのも簡単です。

よくある間違いと落とし穴

  • 空白文字を無視すること。 タブからスペースへの変更や、末尾の改行の追加は、diffツールにとってはファイル全体にわたる大規模な変更に見えることがあります。これが意図的な場合もありますが、多くの場合、本当の意味のある変更を隠してしまうノイズを生み出すだけです。ツールを設定して、空白文字の変更を適切に無視またはハイライトするようにしましょう。
  • 「意味を解さない(セマンティック・ブラインドな)」diff。 前述の通り、JSONやXMLのような構造化データにプレーンテキストのdiffを使うと、非常に誤解を招く可能性があります。属性やキーの順序を変更すると、機能的には何も変わっていないのに、大きな変更に見えてしまいます。これらのフォーマットには、常にセマンティックを認識するdiffツールを使いましょう。
  • コンテキストを忘れること。 diffは何が変わったかを示しますが、なぜ変わったのかは決して教えてくれません。それはコミットメッセージやプルリクエストの説明の仕事です。コンテキストのないdiffは、質問のない答えのようなもので、それが正しいか間違っているかを判断するのは困難です。
  • 「フランケンシュタイン」のようなdiffを作ること。 関連のない変更(バグ修正、機能追加、タイポ修正)を1つのコミットにまとめてしまうと、diffは読むのが悪夢になります。後でそれらの変更の1つだけを元に戻すことが、他方に影響を与えずに不可能になります。各コミットは、単一の、論理的な、アトミックな変更であるべきです。

なぜ注目すべきか

diffを理解することは、現代の開発者にとって選択肢ではなく、キーボードの使い方を知っているのと同じくらい基本的なことです。あなたは毎日、一日に何度もdiffに遭遇します。

  • git statusやgit diffを実行して、自分自身の未コミットの作業を確認するとき。
  • 同僚にレビューしてもらうためにプルリクエストを作成するとき。
  • 他の誰かのプルリクエストをレビューするとき。
  • git blameを使って、特定のコード行を誰が、なぜ書いたのかを調べるとき。
  • 正常に動作する設定と壊れた設定を比較して問題をデバッグしているとき。

開発者でなくても、この概念は強力です。それはWord文書の「変更履歴の記録」機能です。Wikipediaの記事のバージョン履歴です。契約書がドラフト間でどのように進化したかを見る能力です。diffを理解することは、私たちがデジタル世界でどのように変化を管理し、伝達するかを理解することです。それは、監査可能で検証可能な進捗の記録なのです。

さらに深く

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: 差分チェッカー