diff を使った作業

私たちは diff を表示するためにさまざまなソースに依存しています。以下のようなものがあります:

  • Gitalyサービス
  • データベース (merge_request_diff_files)
  • Redis (ハイライトされた差分のキャッシュ)

ディープダイブ

2019年1月、Oswaldo FerreiraはGitLab DiffsとCommenting on Diffs機能に関するDeep Dive(GitLabチームメンバー限定:https://gitlab.com/gitlab-org/create-stage/-/issues/1 )を開催し、将来コードベースのこの部分で働く可能性のある人とドメイン固有の知識を共有しました:

このディープダイブで取り上げた内容はすべて GitLab 11.7 時点でのものであり、それ以降に具体的な内容が変更されている可能性がありますが、それでも良い入門書として役立つはずです。

アーキテクチャ概要

マージリクエストの差分

マージリクエストを更新する場合(ソースブランチにプッシュする場合、ターゲットブランチに強制プッシュする場合、ターゲットブランチに MR からのコミットが含まれる場合)、Gitlab::Git::Compare を使用して比較情報を取得します。 は Gitaly を使用してbasehead のデータを取得し、Gitlab::Git::Diff.betweenを使用してそれらの間の diff を取得します。diff の取得プロセスでは、一連の定数値によって単一ファイルの diff サイズと diff 全体のサイズを_制限して_います。生の diff ファイルはmerge_request_diff_files テーブルに永続化されます。

ApplicationSettings#diff_max_patch_bytes の値の10%より大きな差分は折りたたまれても、PostgreSQL上に保持されます。しかし、定義された_安全限界_(「差分の限界」セクションを参照)を超えるサイズの差分ファイルはデータベースに保存さ_れません_。

マージリクエストの差分ページに差分情報を表示するために、私たちは次のようにしています:

  1. データベースからすべての diff ファイルを取得merge_request_diff_files
  2. _新旧の_ファイル blob をバッチで取得します:
    • 新旧ファイルの内容をハイライト
    • 各ファイル(テキスト、画像、削除済みなど)にどのビューアを使用すべきかを把握
    • ファイルの内容が変更されたかどうか
    • 外部に保存されているかどうか
    • 保管エラーの有無
  3. diffファイルがキャッシュ可能(テキストベース)であれば、Redisにキャッシュされます。Gitlab::Diff::FileCollection::MergeRequestDiff

を使ってRedis上にキャッシュされます。

diff (あらゆる比較) にコメントする場合、NoteDiffFile (これは実際のDiffNote に関連付けられます) に切り詰めた diff バージョンを永続化します。そのため、ファイルの diff が必要になるたびにリポジトリにアクセスする必要がなくなります:

  1. NoteDiffFile#diff が永続化されているかどうかを確認し、それを使用します。
  2. そうでなければ、現在のMRリビジョンであれば、永続化されたMergeRequestDiffFile#diff
  3. 最後のシナリオでは、リポジトリに移動して diff を取得します。

差分の制限

上記で説明したように、単一の diff ファイルと diff 全体のサイズを制限しています。diff ファイルを折りたたむシナリオや、diff ファイルが全く表示されず、ユーザーが Blob ビューに誘導されるケースもあります。

diffコレクションの制限

すべての diff ファイルコレクションに適用される制限です。ファイル数、行数、ファイルサイズが考慮されます。

Gitlab::Git::DiffCollection.collection_limits[:safe_max_files] = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_files] = 100

100 個のファイルがすでにレンダリングされている場合、差分ファイルは折りたたまれます (ただし、拡張は可能です)。

Gitlab::Git::DiffCollection.collection_limits[:safe_max_lines] = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] = 5000

5000行がすでにレンダリングされている場合、ファイル差分は折りたたまれます(ただし展開可能です)。

Gitlab::Git::DiffCollection.collection_limits[:safe_max_bytes] = Gitlab::Git::DiffCollection.collection_limits[:safe_max_files] * 5.kilobytes = 500.kilobytes

500キロバイトがすでにレンダリングされている場合、ファイルの差分は折りたたまれます(ただし、拡張は可能です)。

Gitlab::Git::DiffCollection.collection_limits[:max_files] = Commit::DIFF_HARD_LIMIT_FILES = 1000

すでに 1000 ファイルがレンダリングされている場合は、それ以上のファイルはレンダリングされません。

Gitlab::Git::DiffCollection.collection_limits[:max_lines] = Commit::DIFF_HARD_LIMIT_LINES = 50000

50,000 行がすでにレンダリングされている場合、それ以上のファイルはまったくレンダリングされません。

Gitlab::Git::DiffCollection.collection_limits[:max_bytes] = Gitlab::Git::DiffCollection.collection_limits[:max_files] * 5.kilobytes = 5000.kilobytes

5メガバイトがすでにレンダリングされている場合、それ以上のファイルはまったくレンダリングされません。

すべてのコレクション制限パラメータは、Gitaly上で送信され、適用されます。つまり、制限を超えた後、Gitalyはmerge_request_diff_files に永続化される安全なデータ量のみを返します。

個々のdiffファイルの制限

コレクションの各diffファイルに作用する制限です。ファイル数、行数、ファイルサイズが考慮されます。

展開可能なパッチ (折りたたみ)

ApplicationSettings#diff_max_patch_bytes. ApplicationSettings#diff_max_patch_bytesDiffで設定した値の10%を超えると、Diffパッチは折りたたまれます。ApplicationSettings#diff_max_patch_bytesつまり、最大許容値が 100kb の場合、10kb に相当します。パッチサイズが.NET Frameworkで設定された値の10%を超えない場合、diffは永続化され、拡張 ApplicationSettings#diff_max_patch_bytes可能になります。

この命名法(Collapsing)はGitalyでも使われていますが、この制限はGitLabでのみ使われています(ハードコードされており、Gitalyには送信されません)。Gitalyはコレクションリミットを超えた場合のみDiff.Collapsed (RPC) 。

拡張できないパッチ (大きすぎる)

パッチがApplicationSettings#diff_max_patch_bytes より大きい場合はレンダリングされません。ユーザーにはChanges are too large to be shown. メッセージと、そのコミット内のファイルのみを表示するボタンが表示されます。

Commit::DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] = 5000

ファイルの diff は 5000 行を超えると抑制されます (技術的には折りたたみとは異なりますが、動作は同じで、展開可能です)。

この制限はハードコードされており、GitLabでのみ適用されます。

閲覧者

models/diff_viewer/* にある Diff ビューアは、Diff ファイルの各タイプに関するメタデータをマップするためのクラスです。バイナリであるかどうか、どのパーシャルをレンダリングに使うべきか、どの拡張子に対応しているかといった情報があります。

DiffViewer::Base blob(新旧バージョン) の内容、拡張子、ファイルタイプを検証し、レンダリング可能かどうかをチェックします。

マージリクエストの差分をターゲットブランチのHEAD にマージします。

歴史的に、マージリクエストの差分はgit diff target...source によって計算されてきました。これは、ソースブランチのHEAD と、ターゲットブランチとソースブランチのマージベース (または共通の祖先) を比較するものです。この解決策は、ターゲットブランチがソースブランチによって導入された変更の一部を含むようになるまではうまく機能します:ソースブランチがfeature_a で、ターゲットブランチがmain の場合を考えてみましょう:

  1. main から新しいブランチfeature_a をチェックアウトし、その中のfile_afile_b を削除します。
  2. mainfile_a を削除するコミットを追加します。

マージリクエストの diff にはfile_a の削除がコンテナとして残っていますが、mainHEAD と比較した実際の diff にはfile_b の削除のみが残っています。このような冗長な変更を含む diff はレビューしにくくなります。

最新の diff を表示するために、GitLab 12.9 ではターゲットブランチのHEAD と比較したマージリクエスト diff を導入しました。ターゲットブランチをソースブランチに人為的にマージし、その結果のマージ ref をソースブランチと比較して正確な diff を計算します。

diff にマージ参照を使用する」と「diff で競合をマージする」というエピックが完成するまでは、main (base)main (HEAD) の両方のオプションがマージリクエストで表示可能です:

Merge ref head options

main (HEAD) オプションは将来的にmain (base) を置き換えるためのものです。

両方のオプションのコメントをサポートするために、差分ノートの位置はmain (base)main (HEAD) の両方のバージョンに保存されます(12.10 で導入)。main (base) バージョンの位置はNote#positionNote#original_position カラムに格納され、main (HEAD) バージョンについてはDiffNotePosition が導入されました。

マージリファレンス差分を扱う際の重要な課題のひとつに、マージ競合があります。ターゲットブランチとソースブランチにマージ競合がある場合、ブランチを自動的にマージすることはできません。そのため YouTube の録画は、この問題とエピックが生まれたきっかけを簡単に紹介しています。

13.5では、両変更マージ衝突の解決策が導入されました。しかし、今後アドレスされるマージ競合のクラスはもっとあります。