世界を動かす技術を、日本語で。

DiffX – 次世代拡張可能な差分フォーマット

概要

  • Unified Diff 形式は一般的だが、現代の開発要件には不十分
  • メタデータやエンコーディングなど、標準化されていない部分が多い課題
  • DiffX は拡張性と後方互換性を両立した新しいフォーマット
  • 複数コミットやバイナリ対応、標準化されたメタデータ管理が特徴
  • 既存ツールとの互換性を維持しつつ、将来性も確保

ソフトウェア開発者とdiffファイル

  • diffファイル はテキストファイル間の差分を示すファイル
  • Git、Subversion、CVSなど多様なバージョン管理システムで利用
  • 一般的なdiffは、挿入(+)や削除(-)行、ファイル名、タイムスタンプなどの基本情報を含む
  • Unified Diff 形式が最も広く使われている標準形式
  • ツールや開発者はdiffをレビューやパッチ適用など多目的で利用

Unified Diff形式の課題

  • Unified Diff は差分部分の表現のみ標準化
  • ファイルパス、エンコーディング、リビジョン、メタデータなどは標準化されていない
  • 複数コミットやバイナリパッチの表現が非標準
  • テキストエンコーディング未対応による解析の困難
  • メタデータ形式が統一されておらず、ツールごとに独自実装
  • 複数のバージョン管理システム間での互換性確保が難題

現状の良い点と限界

  • Unified Diff は柔軟で多様なデータを格納可能
  • パッチツールは認識できない情報をスキップするため、後方互換性が高い
  • Gitのdiffはバイナリ対応やメタデータ管理が進んでいるが、標準定義は未完成
  • 構造化と標準化が不足しており、将来性に課題

DiffXフォーマットの提案

  • DiffX はExtensible Diffsの略で、拡張性と後方互換性を両立
  • 完全なUnified Diff互換でありつつ、追加のメタデータや構造情報を格納可能
  • 1つのdiffファイルで 複数コミット やバイナリ差分を表現
  • テキストエンコーディングやファイルごとのメタデータも標準化
  • 既存ツールで読み書き可能、さらに新機能には追加対応も容易
  • 例:
    #diffx: encoding=utf-8, version=1.0
    #.change:
    #..preamble: indent=4, length=319, mimetype=text/markdown
    ...
    #..meta: format=json, length=270
    {
      "author": "Christian Hammond <christian@example.com>",
      ...
    }
    #..file:
    #...meta: format=json, length=176
    ...
    #...diff: length=629
    --- /src/message.py
    +++ /src/message.py
    @@ -164,10 +164,10 @@
    ...
    

DiffXの主な特徴

  • パースルールの標準化 による解析・編集の容易化
  • diff全体・コミット・ファイル単位での 正式なメタデータ管理
  • 拡張性 :新しい情報追加が既存ツールを壊さず可能
  • 1ファイルで複数コミットやバイナリ内容の表現
  • エンコーディング情報 の明示
  • 既存のパッチツールやパーサーとの 互換性維持
  • ツールによる 差分ファイルの編集・再利用 が容易

DiffXが目指さないもの

  • すべてのツールに新フォーマット対応を強制しない方針
  • 既存diff形式やツールを破壊・再実装させるものではない
  • ベンダーロックインや独自仕様の押し付けは意図しない

DiffXの実装状況と利用例

  • Python実装 :pydiffx
  • 導入事例 :Review Board(Beanbag社製品)
    • 長年のdiff運用課題を解決するために開発・実装
    • 今後全製品でサポート予定

さらに知りたい場合

  • diff形式の課題や違いについては「The Problems with Diffs」参照
  • DiffXファイル仕様やサンプルは「DiffX File Format Specification」「example DiffX files」で公開
  • FAQも用意

まとめ

  • DiffX は既存のUnified Diffの後方互換性を維持しつつ、構造化・拡張性・標準化を実現
  • 現代的な開発・解析・レビュー要件に対応する新しいdiffファイル標準の提案

Hackerたちの意見

この文書全体が読みづらいと思う。カジュアルに言うと「diff」は二つのものの違いを指すよね、ファイルとかディレクトリツリーとか。TFAが言ってるdiffは、少なくとも私にとっては常にパッチとして知られてきたものだ。これはdiffについてじゃなくて、パッチメタデータ管理の話だよ。確かに、立派な目標だけど、ただビットを移動させてるだけじゃん。もしメタデータがJSONであるべきだって提案してたら話は別だけど、代わりに変な自己記述型の長さ区切りのナンセンスで、今日ある問題を隠してるだけだよ。もう拡張性はあるんだから!ただ言葉を入力すればいいだけ!私はgitのコミットやパッチファイルから情報を解析するのにかなりの時間を費やしてきたけど、ある程度の標準化は面白いけど、これは違うね。とはいえ、git diffスタイルがほぼ標準的だっていう主張には、以前よりも納得できる部分がある。そういうことだ。

一つのdiffはコミットのリストを表せない パッチセットはできる!なんで一つのdiffでそれを表現したいのか全く理解できない。

パッチフォーマットはこれらの問題をすべて解決してるんじゃないの? https://git-scm.com/docs/git-format-patch

これがあるって今日知った。ありがとう!(ただの一般人、著者じゃないよ)

これはgitには解決策になるかもしれないけど、Review Boardチームが考えたものに見えるし、SVNやCVS、Perforceなどの他の多くのバージョン管理システムと統合しなきゃいけないんだよね。これは多くの異なるバージョン管理システムを一つのフォーマットでサポートするためのものみたいだ。私はReview Boardを使っていたところで働いていたけど、SVNが主なVCSで、多くの開発者がローカルのgit-svnミラーを使っていた。時々、SVNとgit-svnが一つのレビューで混ざると、diffのアップロードに問題が起きた。Review BoardのCLIが両方のために共通のdiffフォーマットを生成してくれたら助かっただろうな。

diffツールで解決できていない問題の一つは、改行属性に依存していることだ。長い行(圧縮されたJSONや長い配列みたいな)での変更をレビューするのは難しすぎる。

gitには行単位のdiffよりも細かい単語単位のdiffもあるよ、デフォルトの区切りは空白。

問題の一部は、一般的なフォーマットが人間に読みやすくて、ツールでも解析できる妥協点になってることだと思う。著者がメタデータでその辺を解決しようとしてるのは分かるけど、プレーンテキストに依存しないフォーマットを考える方がいいんじゃないかな。あなたが言ってるようなファイルの合理的な差分を計算する方法を考えるのは難しそうだけど、解決できないわけじゃないと思う。

完全に同意する!構造化データのためのより良い差分表現を探る道はたくさんあると思う(ASTにもいいし、これについても考えてる)。このフォーマットはUnified Diffsの拡張を目指してる(ほとんどのSCMの差分フォーマットみたいに)で、全く新しいものではないんだ。でも、もっと具体的な差分フォーマットが広まれば、DiffX内でそれを直接サポートできるようになるかもね、バイナリ差分フォーマットのように。

あの階層的なフォーマットがすごく嫌いなんだ。「..meta」とか「…meta」がどこかにあるのがね。全体のdiffを注釈したいっていうのはわかるけど、各ファイルや各チャンクに対してそれをやるのは、深さが3レベルになるよ。もっと明確な名前を付けて、フルYAMLにはしないでほしい。これで読みやすくなるし(例えば「meta」ブロックの一つが欠けてても、点を数えなくても何を指してるか一目でわかる)、エラーも少なくなると思う(全体のdiffに関連するメタデータがファイルのメタデータと同じフィールドを持つのはおかしい)。それに、なんで二つのフォーマットがあるの?JSONとkey=valueペア?注釈を付けたいものの数はかなり少ないと思うから、一つのフォーマットだけ使えばいいのに。単一の構造にすることで、パーサーを書くのも既存のツール(grep、sed、jq - でも同時には使えない)との統合もずっと楽になるよ。他のメモ:

  • リストにトレーリングカンマを許可してほしい
  • diffは本質的に分割可能だ。diffの半分を取って適用することもできる。あなたのフォーマットはそれにどう影響するの?多分、前文をコピーして、20行スキップして、必要なブロックをコピーしなきゃいけないから壊れるんだろうね?
  • リビジョンはファイルのプロパティ?コミットのチェックサムじゃないの?(私がバカなだけかも)

初期のドラフトでは、構造に関していくつかのアプローチを試したんだ。「commit-meta」とかね。最終的には、パースの要件を簡素化するために#に分解した。メタブロックはすべてメタブロックで、どのセクションレベルにいるべきかを知って、実際に得られるセクションレベルと比較するのは「点を数える」ことになる。ヘッダーフォーマットは、パーサーが知っている非常にシンプルなキー/バリューのペアを意図していて、自由形式のメタデータではない。それが「メタ」ブロックの役割。ヘッダーのパースルールは意図的にとてもシンプルだ。JSONは、私たちと外部の関係者との間でたくさん議論した結果、他の文法を試した後に選ばれた。メタブロックのヘッダーは、将来的にJSONに代わる何かが意味のある形で登場した場合にデータをシリアライズするためのフォーマットを指定できる。自分たちを縛りたくはなかったけど、どんなフォーマットでも入れられるわけにはいかない(そうすると、今直面しているフォーマットの互換性の頭痛に戻るから)。他のメモについては、1. 互換性はここでの重要な要素だから、基本的なJSONを選びたい。リストの末尾にカンマをつけられるといいけど、JSON5パーサーにアクセスできない人が実装するのを難しくするほどではない。2. もしGNU patch(またはそれに類似したもの)にフィードするのが目的なら、まだ分けられる。この追加データはUnified Diffの「ゴミ」エリアにあるから、どうせ無視される(衝突しない限り、そしてその点についてはエンコーディングの推奨で注意を払ってる)。もしDiffXファイルを2つに分けるのが目的なら、先頭のヘッダーを再追加する必要があるから、もっと複雑になる。ただし、実際に使われているすべての差分フォーマットが分けられて、すべてのメタデータを保持できるわけではない。例えば、Mercurialの差分には、親コミット情報を示すために必ずトップに存在しなければならないヘッダーがある。それを削除してGNU patchにフィードすることはできるけど、Mercurial(またはそのフォーマットをサポートするツール)は親コミットの情報を持たなくなる。3. リビジョンはSCMに大きく依存する。一部のSCMはコミット識別子を使うし、他はファイルごとの識別子を使う。中にはその二つの組み合わせを使うものもあるし、さらに差分に注入されるか、境界外で知られる必要がある追加情報を使うものもある。SCMの世界にはさまざまな要件がある。

これが解決しようとしている実際の問題は何なの?パッチ/diffフォーマットが十分じゃないって言ってるけど、誰のためにそうなのか説明してない。GNU Patchの人たちが文句言ってるの?どんな人たちがより良いパッチフォーマットを必要としてるの?

これ、Review Boardで使われてるみたいだね。ソースコードレビューに差分をめっちゃ活用してるし、いろんなバージョン管理システムに対応してるんだ。

これについては、コメント一つじゃ収まらないくらい長い話があるんだけど、要はSCMを作る人やSCMと連携するツールを作る人向けだね。エンドユーザーはそんなこと気にしなくていいはず。パッチや差分のフォーマットは一つじゃないし、SCMごとに少なくとも一つはある。いくつかはかなり良い(Gitのやつ)、多くはまあまあ(Subversionのやつ)、そして本当にひどいか存在しないものもある。俺は古いコードレビュー製品の一つ、Review Boardを立ち上げたんだけど(来年で20周年)、この問題に常に取り組んでる。だから、俺たちが文句言ってる側なんだ :) で、これについては、長い間話してきたSCMベンダーからのフィードバックに基づいてる。ほとんどの人は気にしなくていいけど、差分フォーマットの地獄に対処しなきゃいけないツールには役立つ。

difftastic: https://difftastic.wilfred.me.uk/ は、より良い差分情報のためにtree-sitterを使っていて、個人的にはこれより優れてると思う。

difftasticは最高だね!これはファイルやASTの変更を表示するためのツールじゃないんだ。これは、20年以上にわたって差分パースツールを作り、さまざまなSCMと向き合ってきた中で遭遇した問題に対処するための、処理やパッチ作成用の単一の差分ファイルを生成する方法なんだ。エンドユーザー向けのツールではなく、コードレビュー製品のようなツールが使うのに便利なフォーマットだよ。

これ、すごくいいね。diffはCプリプロセッサのブランチでパッチを当てるにはかなり非効率的だよ。コードをパッチするから、ツリー構造を見て、diffは人間が読めるの?直接編集できるの?これが、俺がパッチにはsedを選ぶ大きな理由なんだ。

たった4つの項目で、驚くほど無駄で逆効果なスコープクリープがあるね:1つの差分ではコミットのリストを表現できない。2. バイナリパッチを表現する標準的な方法がない。3. 差分はテキストエンコーディングを知らない(思ってる以上に問題だよ)。4. 差分には任意のメタデータのための標準フォーマットがないから、みんなそれぞれのやり方で実装してる。この中で、バイナリパッチの表記だけが差分ファイルの合理的な一般化になると思う。他はすべて特定のリビジョン管理システムの内部データ構造やプロトコルで、クライアントとサーバー、バックアップの間でのみ交換されるものだ。

そうだね、最近はあんまり見かけないけど、俺はまだpatch(1)とかを使うことがあって、時々問題にぶつかることもあるよ。特に、異なるプラットフォームの開発者がいるときはね(ファイル名のマングリングやケースフォールディングの問題については、もう話したくもないよ)。

うちは、十数種類のSCMと連携するコードレビュー製品を作ってるんだ。約20年にわたってdiffパーサーを書いてきたけど、SCMが生成するdiffファイルには、予想もしなかったような問題や制限がいろいろあったよ(それを処理しなきゃいけないからね)。これらは、そこでの痛みや学びから来ていて、解決するのに大きな助けになってるんだ。エンドユーザーが心配する必要はないはずだけど、ツールはこれらの問題を考慮して対処しなきゃいけない。特に、自分のdiffフォーマットを持っていないSCMや、データが欠けているもの(例えば、削除されたファイルはすべての変更に表現できない場合がある)や、リポジトリ内のファイルを特定するのに十分な情報が含まれていないものにとってはね。

これって本当に問題なの?俺はこういう問題に遭遇したことがないから、いつ起こるのか想像できないんだ(バイナリファイル以外は)。- エンコーディング - ファイルがutf-8じゃなくても、なんでそれが問題になるの?patchアルゴリズムは同じように動くし、文字が有効なutf-8かどうかなんて関係ないよ。なんで一つのdiffが複数のコミットを表す必要があるの?複数のdiffがある方が自然だと思う。- メタデータ…まあ、確かにそうだけど、メタデータは主に一つのシステム内でしか役に立たない気がする。

一般的に言うと、特定のSCMや特定の環境で使われるSCMと連携するツールを書いていない限り、こういう問題に対処する必要はないと思うよ。各ポイントについていくつか例を挙げるね:1. エンコーディングが重要になるのは、ファイル名とdiffの内容の2つの重要な領域がある。Gitはファイル名のエンコーディングに気を使うけど、ほとんどのSCMはそうじゃないから、diffが生成されるときはローカルエンコーディングに基づいているんだ。ファイル名に非ASCII文字が含まれていると、ある環境で特定のエンコーディングで生成されたdiffが別の環境では適用できないことがある(あるいは、うちの場合のようにリポジトリから見つけられないことがある)。これは一般的ではないけど、起こりうるんだ(PerforceやSubversionで見たことがある)。それから内容の話。多くのSCMは実際にはテキストファイルの表現を提供するだけで、生の内容そのものを提供しないことがある。そのテキストファイルはローカルまたは好みのエンコーディングに再エンコードされ、新しい行も調整されることがある(\r\n\n)。変更をプッシュするときに、そのテキストファイルは再エンコードされる。これにより、異なる環境で同じファイルを操作できるようになるんだ。ただ、これがdiffに必ずしも反映されるわけではない。だから、あまり一般的でないエンコーディングのdiffをツールに送って処理させて、それをそのエンコーディングでチェックアウトしたファイルに適用しようとすると、パッチが失敗することがある。解決策は、処理しているファイルのエンコーディングを知るか、推測することだよ(うちのようなツールは、試すための好みのエンコーディングのリストを指定できるようになってる)。できれば事前に知っておくのがベストだね。ボーナスの面白い事実:いくつかのSCM(Perforceが思い浮かぶ)では、WindowsでファイルをチェックアウトしてからLinuxで共有マウント経由でdiffを取ると、 \r\r\nの改行を持つdiffが得られることがある。これは厄介で、パッチが壊れる。これがよく起こっていたけど、なんとか対処したよ。また、Perforceはしばらくの間、エンコーディングを誤って正規化することがあって、diffにBOMが入ってしまい、GNU patchが壊れることもあった。2. 直接適用やパッチを行う場合には重要だよ。処理のためにツールに渡すときに、シーケンスの中で一つのファイルが含まれないリスクがあると、後で処理するまで見えないような壊れた状態になることがある。すべての状態やメタデータが事前に揃っていると、一貫した方法で一度に処理できるから、すごく便利なんだ。ローカルで作業する場合も、ツールによるね。git format-patchgit amは素晴らしいけど、Git専用なんだ。もし(例えば)Subversionで作業しているなら、自分で何かをするか、別のツールを見つける必要がある。3. リポジトリ内のファイルを特定するために必要な情報の種類によっては重要だよ。一部のシステムでは、コミット全体の識別子が必要だし、ファイルごとの識別子が必要なものもある。両方の組み合わせが必要な場合もあるし、パスやリビジョンに表現されない追加データが必要な場合もある(一般的には特定のユースケースをターゲットにしたエンタープライズSCM)。Unified Diffフォーマットに含まれていない情報を表現するためにも重要だよ(具体的にはファイル名以外のもの)。だから、シンボリックリンク情報、ファイルモード、ファイルやディレクトリに関するSCM特有のプロパティなどがある。SCMが提供する情報はどこかに存在する必要があって、どのようにそのデータを保存するかは各SCMの選択に委ねられている(そして、どのようにエンコードするかなどもね)。

このフォーマットは後方互換性はあるかもしれないけど、前方互換性はないね。JSONは、ほとんどの実装がすでにやっていたことを標準化することで、これをほぼ解決したから、そうするのがいいと思う。もしgit diffがドキュメント化されていないなら、新しいフォーマットを作るのではなく、ソースコードを見てドキュメント化するのが解決策だよ。

フォーマットを拡張したり再構築するのはいいと思うけど、マルチライン(インデント依存)のJSONやYAMLを使うのは良くないと思う。diffファイルの面白い点の一つは、すべてのコマンドが単一行にあることなんだ。シンプルなシェルツールで行を削除するだけで簡単にパースしたり操作できるからね。