Karl Fogel <kfogel@collab.net>
$LastChangedDate: 2003-01-14 07:43:13 +0900 (Tue, 14 Jan 2003) $
Original Date: 22 May, 2001
Canonical URL: http://svn.collab.net/repos/svn/trunk/www/variance-adjusted-patching.html
ブランチのマージに伝統的なコンテキスト diff / unidiff 形式を使うバージョン 管理システムは「高変動」状況では高い確率で失敗する傾向にあります。 ここで「高変動」状況というのは、ブランチのテキストが、ソースのテキストに 比べて、変更された行、削除、追加などの合計が、大雑把に言って、5-10%以上 異なっているような状況を言います。
普通のパッチアプリケーションの問題は、関連するテキストハンクや、その ハンクのコンテキストが、ソーステキストからそれほど大きく変更されて いない、ということに依存していることです。そのようなハンクの位置は 変動します。つまり、そのハンクのそばにテキストが挿入されたり削除され たりするので、ターゲットファイル中での位置がそれによって影響を受けます。 しかし、ハンクそれ自身は、空白や、他のささいな(つまり、自動的に無視 できるような)変更を除いては、変更されていないかもしれません。もしそのような 変更があれば、パッチアプリケーションは失敗するでしょう。
変動調整法は、パッチが作成されてからターゲットテキストにおきた差分 を補償するためにパッチを変形する手続きです。これによって、新しいパッチ は新しいターゲットテキストに適用可能となり、依然として古いパッチと同 じ効果をもつようになります。これは本質的には、パッチに対するパッチ であると言えます。たとえば、ブランチ B が、トランク T から大きく離れて しまっている状態で、リビジョン T:18 から T:19 への修正内容を、B:10 に マージして B:11 を作りたいとしましょう。もし B:10 が T:18 と非常に 異なっていると、マージは、論理的な衝突がないにもかかわらず、テキスト 上の衝突によって失敗するかも知れません。変動調整法は T:18-T:19間の 変更を修正することで、それをB:10に対してきれいに適用できるようにするもの です。
これは可能です。なぜなら、パッチのハンクが、ターゲットテキスト中に現れる ことを期待している任意の与えられた行について、(たとえば、コンテキスト diff では、それはコンテキスト行と、影響を受けた前のバージョンのこと になりますXXX)、バージョン管理システムは、もしそれらに共通の祖先が あるなら、ソースとターゲットテキストの両方に存在する行に関する履歴 を知ることができるからです。この手続きは "cvs annotate" で実行される計算と非常によく似ています。大雑把に言うと、境界外の挿入 や削除による変動を無視する限りにおいて、それぞれの行について以下のどれか が成立します。
わたしたちが、その行が"変更された"というとき、それは単にこの行が 現れた場所では異なる行が現れる、ということを意味します。(XXX) これは、その行が編集されたのか、あるいはターゲットに新しい行が挿入 されたからであるのかは、関係ありません。(べつの言い方をすると、 古いバージョンの行がターゲット中のどこか近くに現れるかどうかには 関係がない、ということです)XXX 重要なのは新しいテキストはハンクが 古いテキストを期待する場所に現れるため、われわれはハンクが新しい行 を期待するように変更する方法をもっているということです。
ソースに存在しない行のケースは起こりえません: もし現在のソースに存在しないのなら、それは diff の期待されるテキスト 部分の中に現れることはできないからです。
パッチプログラムは適用される時点での変動を修正することができますが、 バージョン管理システムも、ハンク中の行番号を調整することができますが それは、ソースまたはターゲット中の、ハンクによって覆われた範囲の 外側でおきた挿入や削除を補償することによっておこないます。もちろんターゲット に対する、まだコミットされていないローカル編集は diff 中で補償される ことはできませんが -- その場合でも、私たちはパッチプログラム自身の オフセット調整機能と、それを扱う場合のあいまいな要因を信頼することが できるはずです。(XXX)
上の決まりはどのようにして変動調整法が動くかのあまり形式的ではない 説明です。以下では、アルゴリズムをもう少し形式的に記述し、バージョン 管理システムが任意の状況下で、どのようにして変動調整を実行するかを 見ていきます。このアルゴリズムは実際には強力すぎるものです -- 論理的な最大限度まで許容すれば、それは、実際にはユーザがほとんど 衝突と解釈して欲しいような状況にまで、きれいに適用することができる パッチを生成してしまいます。それで、調整レベルを選択させることが 必要になります。デフォルトの選択レベルとして良いものは、多分 コンテキストの変動を補償することを許しはするが、期待されるターゲット の行にある変動については、補償しない、というものでしょう。 いずれにせよ、完全な、非選択的なアルゴリズムは以下ですが、必要に 応じて選択レベルのつまみをつける場所は明らかでしょう。
変動調整法のアルゴリズム
いくつかの定義:
シナリオの説明:
二つの作業ラインが "trunk"/"branch"である必要はありませんが、理解を 容易にするため、以下ではそのような言葉を使います。現実的な観点から、 その二つは、共通の祖先を持つ二つの開発ラインであるとします。
ブランチ B は トランク T の T:8 で分岐したものだとしましょう。 つまり、T:8 == B:1 です。最新のトランクのリビジョンは T:20 で、最新のブランチのリビジョンは B:15 です。ブランチ B 上の 作業コピーを持ったユーザが T:19-T:20 間の変更をマージしたいとします。 簡単のためにこの変更を"patch"の頭文字をとって P とします。(一般性を 失うことなく、一つのファイルだけがパッチされると仮定することが できます。マージ中に複数のファイルがある場合は、すべてのファイルに 対して同じアルゴリズムを適用することになります。)
調整されていない P を B:15 に直接パッチすることには二つの問題 があります。T:19 のソーステキストを元にした P は、
P を調整するためには、まず diff T:8-T:19 の間を一通り見ていき、適切な形 でハンクを調整します。T:8-T:19 間のそれぞれの行は以下のどれかのタイプに 帰着します:
つぎに、diff B:1-B:15 間を一通りたどって、 P をさらに調整します。
これで P は B:15 に対して適用する準備は整っていて、ローカル編集に 対する適用に対しても十分な文脈を持っています。
両方のループ中のステップは圧縮することができます。たとえば、ハンク中の 追加行と削除行の数がバランスしている場合、後のハンク中での行番号 調整は不要となります。バランスしているかどうかは、問題のハンク中の 行の数を調べるだけで簡単に知ることができます。
アルゴリズムは、情報を失うことなしに、"本来の"衝突を柔軟に 扱います。"本来の"衝突というのは、T:8-T:19 の変更のうち、B:1-B:15 の変更と重なっているもののことです。それは、文脈と、"前の"行の状態 によって、できるだけ目立つ形で取り扱われます。依存情報を含む形 でエラーを報告するか、両者の重なった部分を、CVSスタイルの衝突 マーカでくくった形で、ユーザに処理させることもできます。あるいは もし衝突マーカが望むようなインターフェースではない場合、パッチは衝突 を何か別のメタデータ形式(パッチ形式のコメントの部分を使って)で保存する こともでき、他のツールはそれを報告し、解消するのを助けることが できます。
いくつかのパッチに対して、調整法がどのように働くかを簡単に見て見ましょう。 ここでは簡単な例を用意しました。トランクからブランチへ、個別の変更を 移そうという場合です。トランク T 上のリビジョン 1 は以下のようになって います:
int main (int argc, char **argv) { /* line minus-five of context */ /* line minus-four of context */ /* line minus-three of context */ /* line minus-two of context */ /* line minus-one of context */ printf ("Hello, world!\n"); /* line plus-one of context */ /* line plus-two of context */ /* line plus-three of context */ /* line plus-four of context */ /* line plus-five of context */ }
ブランチ B も存在し、トランクのリビジョン T:1 から分岐したものだと します。ブランチ B 上で、文脈に対する変更をコミットします(単語で表した 数値を数字に置き換えます)。するとB:2 は以下のような感じになります:
int main (int argc, char **argv) { /* line -5 of context */ /* line -4 of context */ /* line -3 of context */ /* line -2 of context */ /* line -1 of context */ printf ("Hello, world!\n"); /* line +1 of context */ /* line +2 of context */ /* line +3 of context */ /* line +4 of context */ /* line +5 of context */ }
一方トランクでは、中間の行だけを変更します。それで T:2 は以下のように コミットされました。
int main (int argc, char **argv) { /* line minus-five of context */ /* line minus-four of context */ /* line minus-three of context */ /* line minus-two of context */ /* line minus-one of context */ printf ("Good-bye, cruel world!\n"); /* line plus-one of context */ /* line plus-two of context */ /* line plus-three of context */ /* line plus-four of context */ /* line plus-five of context */ }
通常の文脈ベースの対処法を使うと、T:1->T:2 の変更に対する B:2 の パッチの適用は失敗します。純正の`patch'を使った場合には リジェクトファイルができますし、`rcsmerge' を使った場合は 衝突マーカーが書き込まれます:
$ cvs up -j 1.1 -j 1.2 foo.c ### Purists will note that -j HEAD would have been sufficient; however, ### ### two -j's more clearly reflects the intent of the example. ### RCS file: /usr/local/cvs/vap0/foo.c,v retrieving revision 1.1 retrieving revision 1.2 Merging differences between 1.1 and 1.2 into foo.c rcsmerge: warning: conflicts during merge $ cat foo.c int main (int argc, char **argv) { <<<<<<< foo.c /* line -5 of context */ /* line -4 of context */ /* line -3 of context */ /* line -2 of context */ /* line -1 of context */ printf ("Hello, world!\n"); /* line +1 of context */ /* line +2 of context */ /* line +3 of context */ /* line +4 of context */ /* line +5 of context */ ======= /* line minus-five of context */ /* line minus-four of context */ /* line minus-three of context */ /* line minus-two of context */ /* line minus-one of context */ printf ("Good-bye, cruel world!\n"); /* line plus-one of context */ /* line plus-two of context */ /* line plus-three of context */ /* line plus-four of context */ /* line plus-five of context */ >>>>>>> 1.2 }
文脈は変更されたので、中間の行に対する変更は見つけることができません。 もしパッチをT:1->T:2で調整すれば、このようになります:
*** foo.c 2001/05/18 08:15:28 1.1 --- foo.c 2001/05/18 08:20:59 1.2 *************** *** 5,11 **** /* line -3 of context */ /* line -2 of context */ /* line -1 of context */ ! printf ("Hello, world!\n"); /* line +1 of context */ /* line +2 of context */ /* line +3 of context */ --- 5,11 ---- /* line -3 of context */ /* line -2 of context */ /* line -1 of context */ ! printf ("Goodbye, cruel world!\n"); /* line +1 of context */ /* line +2 of context */ /* line +3 of context */
これだと、B:2 に対して完璧に適用することができます。この diff は どの二つのコミットバージョン間でも存在しないにもかかわらず、です。
もう少しだけ複雑な場合を考えてみます。T:1 と B:1 は同じもので始めます:
int main (int argc, char **argv) { /* line minus-five of context */ /* line minus-four of context */ /* line minus-three of context */ /* line minus-two of context */ /* line minus-one of context */ printf ("Hello, world!\n"); /* line plus-one of context */ /* line plus-two of context */ /* line plus-three of context */ /* line plus-four of context */ /* line plus-five of context */ }
T:2 はファイルの先頭の近くで何行か挿入されます:
#include <stdio.h> int main (int argc, char **argv) { /* line minus-five of context */ /* line minus-four of context */ /* line minus-three of context */ /* line minus-two of context */ /* line minus-one of context */ printf ("Hello, world!\n"); /* line plus-one of context */ /* line plus-two of context */ /* line plus-three of context */ /* line plus-four of context */ /* line plus-five of context */ }
それから別の変更がトランクでコミットされ、中間の行が編集されました。 T:3 はこんな感じになります:
#include <stdio.h> int main (int argc, char **argv) { /* line minus-five of context */ /* line minus-four of context */ /* line minus-three of context */ /* line minus-two of context */ /* line minus-one of context */ printf ("Good-bye, cruel world!\n"); /* line plus-one of context */ /* line plus-two of context */ /* line plus-three of context */ /* line plus-four of context */ /* line plus-five of context */ }
一方、一つの複雑な変更がブランチ上でコミットされ、そのため B:2 は こんな感じになります:
int main (int argc, char **argv) { /* line minus-five of context */ /* line minus-four of context */ /* line minus-three of context */ /* line -1 of context */ printf ("Hello, world!\n"); /* newly inserted line of context */ /* line plus-one of context */ /* line plus-two of context */ /* line plus-three of context */ /* line plus-four of context */ /* line plus-five of context */ }
文脈の minus-two の行は削除され、minus-one の行は "-1" に変更 され、文脈の新しい行がずっと下のほうで挿入されました。この状態で ブランチ上の開発者が T:2-T:3 間の変更をマージしたいとします。 調整されていないパッチは以下のような感じになります:
Index: foo.c =================================================================== RCS file: /usr/local/cvs/vap1/foo.c,v retrieving revision 1.2 retrieving revision 1.3 diff -c -r1.2 -r1.3 *** foo.c 2001/05/22 21:17:14 1.2 --- foo.c 2001/05/22 21:19:42 1.3 *************** *** 7,13 **** /* line minus-three of context */ /* line minus-two of context */ /* line minus-one of context */ ! printf ("Hello, world!\n"); /* line plus-one of context */ /* line plus-two of context */ /* line plus-three of context */ --- 7,13 ---- /* line minus-three of context */ /* line minus-two of context */ /* line minus-one of context */ ! printf ("Good-bye, cruel world!\n"); /* line plus-one of context */ /* line plus-two of context */ /* line plus-three of context */
調整アルゴリズムの後では、以下のようになります:
Index: foo.c =================================================================== RCS file: /usr/local/cvs/vap1/foo.c,v retrieving revision 1.2 retrieving revision 1.3 diff -c -r1.2 -r1.3 *** foo.c 2001/05/22 21:17:14 1.2 --- foo.c 2001/05/22 21:19:42 1.3 *************** *** 5,11 **** /* line minus-four of context */ /* line minus-three of context */ /* line -1 of context */ ! printf ("Hello, world!\n"); /* newly inserted line of context */ /* line plus-one of context */ /* line plus-two of context */ --- 5,11 ---- /* line minus-four of context */ /* line minus-three of context */ /* line -1 of context */ ! printf ("Good-bye, cruel world!\n"); /* newly inserted line of context */ /* line plus-one of context */ /* line plus-two of context */
T:1-T:2 間の変更の不在(なぜなら、適用しているパッチは T:2-T:3 間の ことしか考慮していないからです)を補償するためにどのようにして行番号が調整 されたか、またその文脈が B:1-B:2 間の差分を取り込むためにどのように 変更されたか、に注意してください。
ここでも、このパッチはどの二つのリビジョン間でも存在していないにも かかわらず、B:2 にたいしてきれいに適用することができます。
調整アルゴリズムのコストは、分岐地点からトランクのヘッドまでの diff の 大きさと、分岐地点からブランチのヘッドまでの diff、そして、適用される diff にあるサイズの合計に比例します。これは、最初から逆-差分の保存を 使う場合のコストと比較することができます -- 必要なものは、それぞれの 終端に対する diff 、二つのラインの共通の祖先、パッチが適用される リビジョン、これらに対するアクセス許可のみです。中間のリビジョンにユーザ がアクセスする必要はありません。
変動調整法にるパッチを使えば、引き続くリビジョンを保存している リポジトリはどれも、大きくかけ離れてしまったブランチ間であっても、 それらが共通の先祖を持つ限り、パッチを適用することができます。