はじめに
pull requestを開くとき、ドキュメントの改訂版をレビューするとき、merge conflictを解決するとき——これらすべての場面で、あなたはテキスト diff と向き合っています。diff(differenceの略)とは、2つのバージョンのテキスト間の変更を表現したもので、何が追加され、何が削除され、何が変わらなかったかを示します。
テキスト diff ツールは現代のソフトウェア開発の基盤です。バージョン管理システム、コードレビュープラットフォーム、コラボレーションツール、デプロイメントパイプラインを支えています。diff の仕組みを理解すること——単に読み方だけでなく、背後にあるアルゴリズムの動作原理まで——は、より効果的な開発者、より思慮深いコラボレーターになるための重要なスキルです。
本記事では、1974年のUnix diff の起源から、現在 git が使用する最新アルゴリズムまでを網羅し、diff フォーマット、三方向マージ、可視化戦略、そして実践的なベストプラクティスを詳しく解説します。
diff の歴史
1974年——diff の誕生
Doug McIlroyは1974年、ベル研究所でUnix用の diff ユーティリティを作成しました。これは革命的な発明でした:開発者は初めて、2つのテキストファイルを自動比較し、その差異を構造化された形式で出力できるようになりました。これはソフトウェアパッチの配布やソースコードの変更追跡に即座に役立ちました。
1984年——GNU diff
フリーソフトウェア財団が GNU diffutils の一部として GNU diff をリリースし、ポータブルで改善されたバージョンをすべての人が利用できるようにしました。GNU diff は context diff と unified diff という追加の出力フォーマットを導入し、これらは業界標準となりました。
1986年——Myers アルゴリズム
Eugene Myers が1986年に画期的な論文 「An O(ND) Difference Algorithm and Its Applications」 を発表しました。最短編集スクリプト(Shortest Edit Script、SES) を見つけるこのアルゴリズムは、git を含む最新のほとんどの diff 実装の理論的基盤となりました。
1990年代——diff/patch が標準パッチ形式に
diff と patch の組み合わせがソフトウェア更新配布の事実上の標準となりました。オープンソースプロジェクトは .patch ファイルを配布し、コントリビューターはメーリングリストに diff をメールし、Linuxカーネルはメールによる diff/patch ワークフローでほぼ完全に開発・保守されました。
2005年——Git と Myers Diff
Linus Torvaldsが2005年に git を作成し、Myers アルゴリズムをデフォルトの diff エンジンとして採用しました。Git の diff サブシステムは歴史上最も広く使用された diff 実装の1つとなり、GitHub や GitLab などのプラットフォームで毎日数十億回の比較を処理しています。
2010年代——Histogram Diff とウェブ可視化
Git は多くの操作で好ましいデフォルトとして histogram diff アルゴリズムを導入しました。同時に、ウェブベースの diff 可視化が隆盛し——GitHubのスプリットビューとインライン PR diff、GitLabのレビューツール、Gerritの変更追跡が、diff 出力を大勢の開発者の手に届けました。
Diff アルゴリズム
LCS——最長共通部分列
diff を計算する古典的なアプローチは、2つのシーケンスの 最長共通部分列(Longest Common Subsequence、LCS) に基づいています。LCS とは、両方の入力に同じ相対順序で現れる最長の要素列(必ずしも連続している必要はありません)です。
例:
- 文字列 A =
"ABCBDAB" - 文字列 B =
"BDCAB" - LCS =
"BCAB"(長さ 4)
diff は LCS に含まれない要素から導出されます:A にのみ存在する要素は削除、B にのみ存在する要素は挿入です。LCS の計算には O(M×N) の時間とスペースが必要で、小さなファイルには許容できますが、大きなファイルには低速です。
Myers アルゴリズム——最短編集スクリプト
Eugene Myers の1986年のアルゴリズムは、シーケンス A をシーケンス B に変換するために必要な最小の挿入・削除回数である 最短編集スクリプト(SES) を見つけます。LCS を求めることと等価ですが、Myers のアプローチは実践においてはるかに効率的です。
主な特性:
- 時間計算量: O(ND)(N = len(A) + len(B)、D = 編集距離)
- 空間計算量: O(N)(線形空間改良版を使用)
- 「スネーク」グラフ探索を使用——編集グラフ上の対角パスで、各「スネーク」は一致する文字のシーケンスを表す
- git、GNU diff、ほとんどの現代の diff ツールで使用
Myers アルゴリズムは、変更がファイルサイズに対して小さい場合(低い D 値)に優れており、これはバージョン管理での一般的なケースです:ほとんどのコミットはファイルのごく一部しか変更しません。
Patience Diff——コード構造に最適
Patience diff は異なるアプローチを取ります:まず両ファイルにそれぞれ1回だけ現れる ユニークな行 を見つけ、それをアンカーとして使い、その間のセクションを再帰的に diff します。
これは、}、{、return、空行など、ソースファイル全体に多く現れる同一の構造的な行を含むコードに対して劇的に優れた結果をもたらします。Myers は間違った閉じ括弧にマッチするかもしれませんが、patience diff はユニークで意味のある行にアンカーし、はるかに理解しやすい diff を生成します。
Patience diff は Bazaar と Mercurial で使用されており、git では git diff --diff-algorithm=patience で利用できます。
Histogram Diff——Git の現代的なデフォルト
Histogram diff は patience diff の進化版です。行の出現頻度のヒストグラムを構築し、この頻度情報を利用してよりスマートなマッチング決定を行います。何度も出現する行は意味のあるアンカーとなりにくく、稀な行がより良い候補です。
Git は histogram diff を導入し、2012年頃から多くのシナリオで 推奨のデフォルト となっています。グローバルに設定するには:
git config --global diff.algorithm histogram
Diff 出力フォーマット
通常の diff 形式(元の Unix フォーマット)
diff file1.txt file2.txt が生成する元の出力フォーマット:
2d1
< file1 にのみある行
5,7c4,6
< 古い行 A
---
> 新しい行 A
2d1(file1 の2行目を削除)や 5,7c4,6(file1 の5〜7行目を file2 の4〜6行目に変更)などのコマンドは、このフォーマットを機械可読にしましたが、人間には難解でした。
Context Diff(-c フラグ)
GNU diff で導入された context diff は、読みやすさのために前後の行を追加し、変更された行を ! でマークし、古いブロックと新しいブロックを *** / --- で分離します。
Unified Diff フォーマット(-u フラグ)——標準フォーマット
Unified diff フォーマット は現代の標準であり、git とすべての主要なパッチワークフローで使用されています。古いコンテンツと新しいコンテンツを単一のブロックに結合し、+ と - で変更をマークし、各変更の場所を識別する hunk ヘッダー を含みます。
git diff 出力
Git の出力は追加のメタデータ付きの unified diff です——ファイルモード、インデックスハッシュ、リポジトリパスを識別する diff --git ヘッダーが含まれます。
Unified Diff フォーマットを深く理解する
Unified diff フォーマットを詳しく解読しましょう:
--- a/config.py
+++ b/config.py
@@ -10,7 +10,8 @@
DATABASE_HOST = 'localhost'
DATABASE_PORT = 5432
-DATABASE_NAME = 'myapp_dev'
+DATABASE_NAME = 'myapp_production'
+DATABASE_SSL = True
# Cache settings
CACHE_TTL = 300
ファイルヘッダー
--- は元のファイル(バージョン A)を示し、+++ は新しいファイル(バージョン B)を示します。git では、a/ と b/ は慣例的なプレフィックスです。
Hunk ヘッダー
@@ -10,7 +10,8 @@
これは hunk ヘッダー であり、このチャンクの変更がファイルのどこにあるかを正確に教えてくれます:
-10,7→ 元のファイルでは、このhunkは10行目から始まり、7行にわたる+10,8→ 新しいファイルでは、このhunkは10行目から始まり、8行にわたる(1行が追加された)
フォーマットは常に @@ -開始行,行数 +開始行,行数 @@ です。
行マーカー
hunk ボディの各行は、以下の3つの文字のいずれかで始まります:
(スペース)——コンテキスト行:変更なし、読みやすさのために表示-(マイナス)——削除行:元のファイルに存在するが、新しいファイルには存在しない+(プラス)——追加行:元のファイルには存在しないが、新しいファイルに存在する
この例では、DATABASE_NAME = 'myapp_dev' が削除されて本番環境名に置き換えられ、DATABASE_SSL = True が新しく追加された行です。
行レベル vs 単語レベル vs 文字レベルの Diff
標準 diff は行レベルで動作します——各行は原子単位として扱われます。これはソースコードに理想的で、行が変更の自然な単位です。
単語レベルの Diff
散文、ドキュメント、設定ファイルには、単語レベルの diff がより有益です。次の変更を考えてみましょう:
変更前: The quick brown fox jumps over the lazy dog
変更後: The quick red fox leaps over the sleeping cat
行レベルの diff では行全体が変更されたと表示されます。単語レベルの diff は何が変わったかを正確にハイライトします:
The quick
brownred foxjumpsleaps over thelazy dogsleeping cat
Git は git diff --word-diff で単語レベルの diff をサポートしています。
文字レベルの Diff
文字レベルの diff(Levenshtein 距離などのアルゴリズムを使用)は個々の文字レベルで機能します。1文字の変化も重要なパスワード、識別子、設定値などの短い文字列に最適です。
比較表
| アプローチ | 粒度 | 最適用途 | ツール例 |
|---|---|---|---|
| Line diff | 行 | ソースコード | git diff |
| Word diff | 単語 | 散文/ドキュメント | git diff --word-diff |
| Char diff | 文字 | 短い文字列 | Levenshtein ベース |
| Semantic diff | AST ノード | コードリファクタリング | difftastic |
difftastic のような semantic diff ツールは、ソースコードを抽象構文木(AST)に解析し、生テキストではなくツリー構造を diff します。言語の構文を理解し、空白などの表面的な変更を無視した diff を生成します。
三方向マージとマージコンフリクト
三方向マージモデル
2人が同じファイルを独立して変更した場合、単純な双方向 diff ではどちらの変更を採用すべきかを決定できません。Git は 三方向マージ(three-way merge) を使用します:
- Base ——共通の祖先コミット
- Ours ——現在のブランチのバージョン
- Theirs ——受け取るブランチのバージョン
アルゴリズムは ours と theirs の両方を base と比較します:
- ours だけがある領域を変更した → ours を使用
- theirs だけがある領域を変更した → theirs を使用
- 両方が同じ領域を異なる方法で変更した → コンフリクト
マージコンフリクトマーカー
git が自動的にコンフリクトを解決できない場合、ファイルにマーカーを挿入します:
<<<<<<< HEAD
DATABASE_NAME = 'myapp_production'
=======
DATABASE_NAME = 'myapp_staging'
>>>>>>> feature/staging-config
<<<<<<<と=======の間はあなたのバージョン(HEAD)=======と>>>>>>>の間は受け取るバージョン- ファイルを手動で編集してコンフリクトを解決し、
git addを実行する必要があります
使用ケース
コードレビュー
Diff はコードレビューの言語です。GitHub、GitLab、Bitbucket の pull request はすべて、変更を diff として表示し、レビュアーが何が変わったかを行ごとに把握できるようにします。小さく集中した diff はレビューの品質とスピードを大幅に向上させます。
ドキュメント比較
法務チームは契約書の改訂を比較するために diff ツールを使用します。テクニカルライターはドキュメントの変更をレビューするために使用します。バージョン管理されたドキュメントが関わるワークフローはすべて、構造化された diff 出力から恩恵を受けます。
ログ分析
システム管理者はログファイルを比較して、実行間で何が変わったかを特定します——新しいエラー、欠落したエントリ、設定のドリフト。diff や colordiff などのツールはシステム管理者のツールキットの標準的な一部です。
法的・コンプライアンス要件
規制提出物、監査証跡、コンプライアンス文書は、バージョン間の変更の公式記録を必要とすることが多くあります。Diff ツールは何が、いつ、どのように変更されたかの客観的で再現可能な記録を提供します。
セキュリティ分析
セキュリティ研究者は設定スナップショットやシステム状態を比較して、不正な変更を検出します。ファイル整合性監視システムは diff の原理に基づいて構築されています。
可視化アプローチ
サイドバイサイド(スプリットビュー)
2つのパネルが旧バージョンと新バージョンを並べて表示し、対応する行に変更をハイライトします。大きな変更に最適で、両側のコンテキストが役立つ場合に特に有効です。
インライン(統合ビュー)
削除と追加がコンテキスト行と混在した単一のストリームで表示されます。ほとんどのコマンドラインツールと GitHub の PR ビューのデフォルトです。密集した小さな変更に最適です。
GitHub PR ビュー
GitHub は unified diff を、シンタックスハイライト、展開可能なコンテキスト、行インラインレビューコメント、スプリットビュー切り替え、ファイルごとの「確認済み」追跡で強化し、大きな pull request をレビュアーにとってナビゲートしやすくします。
単語レベル差分ハイライト
git diff --word-diff=color などのツールは、行内で変更された単語をハイライトし、行レベルの diff コンテキストで文字レベルの変更を可視化します。設定ファイルや散文ドキュメントに特に有用です。
ベストプラクティス
コミットは小さく集中させる。 1つの論理的なことだけを変更する diff は、数十のファイルを複数の理由で変更する diff よりもはるかにレビューしやすいです。
意味のあるコミットメッセージを書く。 Diff は何が変わったかを示し、コミットメッセージはなぜ変わったかを説明します。
適切な diff アルゴリズムを使用する。 コードには、histogram または patience diff が Myers より読みやすい出力を生成することが多いです。グローバル設定:
git config --global diff.algorithm histogram。コミット前に diff をレビューする。
git diff --stagedはコミットされる内容を正確に表示します。git commitを実行する前に必ず確認してください。散文には word-diff を使用する。 ドキュメントや README ファイルを書くとき、
git diff --word-diffは行 diff よりはるかに読みやすいです。hunk コンテキストを理解する。 各 hunk 周辺の3行のコンテキストには理由があります——コンテキストの中で変更を理解するのに役立ちます。
コンフリクトを慎重に解決する。 相手の変更を理解せずにコンフリクトの一方を受け入れないでください。両方の変更が重要である可能性があります。
バイナリファイルには
.gitattributesを使用する。 無意味な diff を避けるために、バイナリファイルや特殊ファイルの処理方法を git に伝えてください。
よくある質問
Q:diff と patch の違いは何ですか?
A:diff は2つのファイルを比較して diff 出力を生成します。patch はその diff 出力を受け取り、ファイルに適用して変更を再現します。それらは連携して動作するように設計された補完的なツールです。
Q:git はデフォルトでどの diff アルゴリズムを使用しますか?
A:Git はデフォルトで Myers アルゴリズムを使用しますが、histogram diff が推奨されています:git config --global diff.algorithm histogram。
Q:@@ -10,7 +10,8 @@ は何を意味しますか?
A:hunk が両ファイルの10行目から始まることを意味します。旧ファイルでは7行をカバーし、新ファイルでは8行(1行追加)をカバーします。
Q:バイナリファイルを diff できますか?
A:標準の diff ツールはテキストを対象とします。バイナリファイルには専用ツール(bsdiff など)があります。ほとんどの diff ツールは単に「Binary files differ」と報告します。
Q:"hunk" とは何ですか?
A:hunk は diff 内の連続した変更のブロックで、前後のコンテキスト行を含みます。変更がファイル全体に分散している場合、1つの diff に複数の hunk が含まれることがあります。
Q:なぜ git は移動されたコードに対して混乱した diff を生成することがあるのですか?
A:標準の行ベース diff には「移動」の概念がありません——追加と削除しか見えません。移動されたコードは、1つの場所で削除され、別の場所で追加されたように表示されます。AST 構造を理解する difftastic のようなツールは移動を検出できます。
Q:三方向マージとは何ですか?
A:共通の祖先(base)と2つの変更バージョンを使用して変更を智的にマージし、非競合編集を自動解決し、真のコンフリクトをフラグする統合戦略です。
テキスト diff の理解は単なる技術的な好奇心ではありません——テキスト、コード、ドキュメントを長期にわたって扱うすべての人にとって、基本的なスキルです。Unix diff コマンドのエレガントなシンプルさから、現代のコードレビュープラットフォームを動かす洗練されたアルゴリズムまで、この控えめな diff ツールは50年以上にわたってソフトウェアの構築、レビュー、保守の方法を形作ってきました。