差分 (diff)

2 つのテキスト間の違いを検出・表示する処理。バージョン管理やコードレビューの基盤技術。

差分 (diff) とは、2 つのテキストファイルやデータを比較し、追加・削除・変更された部分を検出・表示する処理です。1974 年の第 5 版 Unix に収録された diff コマンドがその出発点です。行単位で最長共通部分列 (両方のファイルに同じ順序で現れる行の並び) を求め、そこから外れた行を削除・追加として示すという発想は、現在の実装にも受け継がれています。現在では Git、SVN、Mercurial などのバージョン管理システムの中核を担い、コードレビューやデプロイの自動化にも広く活用されています。

diff アルゴリズムにはいくつかの種類があります。Git が既定で使うのは Myers 系の貪欲法で、公式ドキュメントでも「基本的な貪欲法」と説明されています。ここで誤解が多いのは、既定のアルゴリズムが最小の差分を保証するわけではないという点です。Git には最小化に時間をかける minimal が別に用意されており、最小の差分が必要なら --diff-algorithm=minimal を明示します。patience diff は、片方に 1 回しか出てこない行を手がかりにマッチングすることで、コードの構造的な変更を読みやすく表示します。histogram diff は patience を拡張して出現回数の少ない共通要素にも対応させたものです。実務上の判断基準は「同じ変更でも差分の見せ方が変わるだけで、変更後のファイルの中身は当然どのアルゴリズムでも同一」という点で、読みにくい差分に当たったときにアルゴリズムを替えて見比べる、という使い方になります。恒久的に替えるなら diff.algorithm の設定です。

diff の出力形式にも複数の種類があります。unified diff 形式は追加行を +、削除行を - で表示し、変更箇所の前後にコンテキスト行を含めます。コンテキストの既定は前後 3 行で、-U<n> で増減できます。ハンクの先頭に付く @@ -2,7 +2,7 @@ という行は「変更前は 2 行目から 7 行分、変更後は 2 行目から 7 行分」を意味し、行番号を読み違えて別の箇所を直す事故を防ぐ手がかりになります。GitHub や GitLab のプルリクエスト画面で見る差分表示はこの形式がベースです。side-by-side diff は変更前後を左右に並べて表示する形式で、視覚的に変更を把握しやすい利点があります。

実務では、diff はコードレビューだけでなく多様な場面で活用されます。設定ファイルの変更追跡、ドキュメントの版管理、データベースのスキーマ変更の検出などが典型です。CI/CD パイプラインでは、diff の結果をもとにテスト対象やデプロイ対象を絞り込む使い方もあります。

行単位で比較するという性質から、いくつか定番の落とし穴があります。第一に、改行コードを LF から CRLF に変換しただけで全行が差分になります。3 行のファイルなら 3 行削除・3 行追加という出力で、中身は 1 文字も変わっていないのに全面変更に見えます。第二に、インデントを付け替えただけの変更も同じく行の差分として出ます。これは -w (空白の違いを無視) を付けると消えるため、意味のある変更が空白の変更に埋もれているときの切り分けに使えます。第三に、ファイル末尾の改行が失われると \ No newline at end of file という注記付きで最終行が差分になります。第四に、コードを別の場所へ移動しただけの変更も、削除と追加の 2 か所として表示されます。Git には移動を色分けする --color-moved がありますが、既定では無効です。いずれも「差分の行数」が変更の大きさを表さない例で、レビューで行数だけを見て判断しない理由がここにあります。

diff と混同されやすい概念にパッチ (patch) があります。diff が「違いの検出結果」であるのに対し、パッチは「その差分を適用するための指示書」です。diff コマンドで差分を生成し、patch コマンドで適用するという Unix の伝統的なワークフローは、現在でもオープンソースプロジェクトのメーリングリストなどで使われています。

文字数カウントの観点では、diff の出力には元のテキストに加えてメタ情報 (行番号、変更マーカー、ファイルパス、ハンクヘッダーなど) が含まれるため、diff 出力の文字数は元のテキストより多くなります。また、行単位ではなく語や文字のレベルで変更を見たい場合は word diff を使います。ただし word diff は名前のとおり単語単位で、単語の区切りは空白に依存します。英語では the quick brown foxthe quick brawn fox の変更が brownbrawn の置き換えとして出ますが、日本語は空白で区切られないため、既定のままでは一文がまるごと 1 語として扱われ、行単位の差分と大差ない出力になります。日本語の推敲差分では、区切りの正規表現を文字単位に指定する (Git なら --word-diff-regex=.) と、実際に直した数文字だけが浮き上がります。同じ一文を比べても、既定では文全体の置き換え、文字単位の指定では挿入された 3 文字だけが差分として示される、という違いが出ます。

この記事を共有