Patch を当てたら、Reject ファイルが出来てしまった。プログラミングではよくある光景。プログラマーは、人力で Reject (拒絶された) 部分を修正する必要がある。この時の作業を楽にする Tips を、Emacs 使い向けにまとめてみた。
とりあえず、次のやうなシチュエーションを想定している。
Situation
ある日、実験用のコードを書きたくなった A 氏は、rev1 の時点のソース・コードをコピーして開発を行なった。実験コードは完成したけれど、その間にもオリジナルの開発は進んでいた。そこで A 氏は experimental コードの最新版と rev1 のコードの差分を取って開発チームに送り付けた。開発チームは送られたパッチを適用して、ほとんどの変更分は反映されたものの、いくつか競合 (Conflict) が発生した。開発チームは、Rej ファイルをもとに競合を解消する必要がある。
絵にすると、こんな感じ:
(experimental) o--o--o
/ \
(original) -o--o--O--o--o---X
(rev1) (Conflict!)
A 氏は patch を取る際、次のコマンドを使ったとする:
$ diff -uNr rev1 experimental > a.patch
開発チームは、次のコマンドでパッチを適用した:
$ cd original
$ patch -p1 < ../a.patch
diff
Rej ファイルのサンプル
例えば下記の hello.c ファイルに
#include <stdio.h>
int main(void)
{
printf("Hello, world\n");
return 0;
}
次の patch をあてると...
diff -uNr rev1/hello.c experimental/hello.c
--- rev1/hello.c 2008-09-21 17:56:48.000000000 +0900
+++ experimental/hello.c 2008-09-21 17:57:20.000000000 +0900
@@ -2,6 +2,6 @@
int main(void)
{
- printf("Hello, world");
+ printf("Hello, world and you.");
return 0;
}
次の Rej ファイルが出来上がる。
***************
*** 2,7 ****
int main(void)
{
- printf("Hello, world");
return 0;
}
--- 2,7 ----
int main(void)
{
+ printf("Hello, world and you.");
return 0;
}
目次 -- Rej ファイル対処の流れ
- Reject ファイルのリストを作る
- Rej ファイルを開く
- Rej ファイル専用のモード
- Rej ファイルとローカル・ファイルのカーソル位置を同期させる
- 行頭の + 記号を削る
- Rej ファイルとローカル・ファイルの比較
- この変数は使われているのか? と調べてみる
- タグ・ジャンプ
- 変更の巻き戻し
- 3 way Merge
Reject ファイルのリストを作る
まず、Conflict (競合) が起きたファイルのリストを作る。言い換えると、Rejct ファイルのリストを作る。
ここで、find コマンドを活用する。リストの一覧は rej-list.txt といふ名前で保存しませう。
$ cd original
$ find . -name "*.rej" -print > rej-list.txt
*.rej はダブル・クォーテーションで囲むこと。
Rej ファイルを開く
rej-list.txt を開くと、次のやうに Rej ファイルの一覧が出来ている。
./doc/html/index.html.rej
./src/ui/foo.c.rej
...
開きたいファイルの上にカーソルを持っていって、次のコマンドを実行する。
M-x ffap
すると、そのファイルを開くかどうか聞いてくる。RET キーを押すと Rej ファイルが開かれる。ちなみに ffap は、find-file-at-point の略。
長ったらしいパスを入力する必要がないので、手間も省けるし間違いも減る。
rej-list.txt Tips
ぼくが、rej-list.txt を使う時の Tips を紹介しませう。
- ファイルの先頭から、一つずつ ffap で Rej ファイルを開く。
- 修正を後回しにするファイルは、rej-list.txt の末尾に移動させる。
- 修正を終えたファイルは、行頭にスペースを一つ加える。
- 全く修正を加えなかった場合は、行頭に ! を付ける。
- 修正分が大きくて、自分の修正に自信がない場合は、行頭に @ を付けるdu
Tips 適用の結果は次のやうになる。
./src/foo/bar/fixed.c.rej
./src/foo/bar/finished.c.rej
!./src/foo/hoge/no-change.c.rej
@./src/foo/huga/big-change.c.rej
./src/foo/huga/done.c.rej < ここまで修正した
./src/foo/huga/undone.c.rej
./src/unfinished/foo.c.rej
...
./src/foo/bar/later.c.rej
Rej ファイルの数が 100 を越えたりしてくると、この Tips は便利。どの Rej ファイル分まで対処したかが一目で分かる。
Rej ファイル専用のモード
Rej ファイルを開くと、Emacs は Diff-mode というモードに入る。これは、Diff ファイル (パッチ/差分とも呼ぶ) 専用のモード。Rej ファイルの実体は、差分ファイルなので、Diff-mode は Rej ファイル用のモードとも言える。
Diff モードのコマンド
Diff モードで覚えておきたいコマンドを紹介しませう。
- C-c C-c
- ローカル・ファイルの対応部分を開く
- C-c C-s
- パッチを分割 (split) する
- C-c C-a
- パッチを適用する
- M-n
- 次の Reject 部分へ
- M-p
- 前の Reject 部分へ
- C-c C-u
- Reject ファイルを Unified diff 形式に変換する
- C-c C-d
- Reject ファイルを Context diff 形式 (デフォールト) に変換する
ffap で Rej ファイルを開いたら、C-c C-c でローカル・ファイルをオープン & 概当部分へジャンプするとスムーズに作業が進められる。
C-c C-s で conflict している部分とさうでない部分を分割し、conflict していない部分に対して C-c C-a すると、作業効率が上がる。
Context と Unified
Rej ファイルは Context diff 形式で出力される。C-c C-u で Unified diff 形式にすると幸せになる。
サンプルは以下の通り。まずは Context diff の出力。
***************
*** 2,7 ****
int main(void)
{
- printf("Hello, world");
return 0;
}
--- 2,7 ----
int main(void)
{
+ printf("Hello, world and you.");
return 0;
}
そして、こちらが Unified diff 形式。
@@ -2,6 +2,6 @@
int main(void)
{
- printf("Hello, world");
+ printf("Hello, world and you.");
return 0;
}
Rej ファイルとローカル・ファイルのカーソル位置を同期させる
更に Rej ファイル内 C-c C-f とする。すると、Rej ファイルのカーソル位置とローカル・ファイルのカーソル位置が同期するやうになる。C-c C-c で概当場所にジャンプするより、こちらの方が断然楽!
- C-c C-f
- next-error-follow-minor-mode をトグルする
ただし、Rej ファイルの行番号があさっての方向を指していると (変更点が多すぎると、時々さういふことがある)、ちゃんとカーソル位置が同期しない。この場合は、C-c C-f で next-error-follow-minor-mode をオフにして、C-c C-c と Emacs の Search を使う方がいい。
ケース・バイ・ケースで使い分けられたし。
行頭の + 記号を削る
Rej ファイルから、コードをコピーしたとする。すると、行頭の + 記号を削らないといけない。この作業には、短形削除コマンドが有効。
短形削除コマンドとは、範囲指定の始まりと終わりを「四角形の対角線」に見たてて、その四角形部分だけ削除するコマンド。
例えば、次のやうなコードがあった場合を考えてみやう。
hogehoge
+ foo
+ bar
+ null
+ ...
hugahuga
下のスクリーン・ショットのやうに範囲を指定する (範囲指定の始まりは「foo の 0 カラム目」で、終わりは「...」の 1 カラム目)。
そして、C-x r k を実行。すると、行頭の + 記号が削除される。
- C-x r k
- kill-rectangle (短形削除コマンド) を実行する
Rej ファイルとローカル・ファイルの比較
Rej ファイルが出来るシチュエーションの一つに、experimental での変更分を original でも既に行なっていた、というものがある。この場合、既に変更してある所に同じ変更を加えやうとして、patch コマンドは混乱し Rej ファイルを作り出す。
プログラマーは、Rej ファイルの中身と original が同じことを確認したら、何も変更を加えず Rej ファイルを閉じればいい。
問題は確認する方法。もしかしたら、1 行だけ違うかもしれない。もしかしたら、マスク一箇所分だけ違うかもしれない。もしかしたら演算子 1 個分だけ... 目で確認するのは、愚直に過ぎる。
ediff-windows-linewise を使う
こんな感じに色付してくれると、変更場所が (あるなら) 分かって嬉しい。
スクリーン・ショットでは、rsvg の行が足りないことが見て取れる。
ediff-windows-linewise の使い方
- ウィンドウを分割して、片方に rej ファイル、もう片方にローカル・ファイルを表示させる。
- お互いのウィンドウが同じ部分を表示するようにする (ex. next-error-follow-minor-mode を使って同じ部分を表示し、更に比較したい行で C-u 0 C-l, C-x o, C-u 0 C-l を実行する)
- C-u M-x ediff-windows-linewise を実行する
この変数は使われているのか? と調べてみる
カレント・ディレクトリー以下のファイルに対して、grep をかけたい場合、次のコマンドが便利。
- M-x grep-find
- カレント・ディレクトリー以下のコマンドに grep をかける
このコマンドを実行すると、ミニバッファーが次のやうになる。
Run find (like this): find . -type f -print0 | xargs -0 -e grep -nH -e
この行末に grep にかけたいキーワードを入力して RET を押すと、検索が実行される。
やっていることは、find + xargs + grep のワンライナーを呼び出しているだけ。ユーザーは、この長くて複雑なワンライナーを覚える (打ち込む) 必要がないのがメリット。
カレント・ディレクトリーじゃなくて、一つ上のディレクトリー以下の全てのファイルを検索したい場合は、find . の部分を find ../ に変えればいい。
パッチを当ててそっくりな名前の変数や関数が現れたら、変数名や関数名が変更されたのかもと疑ってみるといい。で、本当に変更されたかどうかを調べるのに、同じ変数名・関数名を使ってるソース部分を見る手がある。grep-find は、こんな時に役に立つ。
上と同じことを、タグ・ジャンプを使って検索することもできる。ここでいうタグは、検索用のインデックスのこと。デメリットはタグ作成に時間がかかる点。メリットは検索時間が早い点。
タグ・ジャンプの詳細については、過去記事をどうぞ。
変更の巻き戻し
Rej ファイルの対処をしていたら、わけが分からなくなることがある。そんな時に備えて、対処を始める前のファイルのバックアップを取っておきませう。
もしも、バージョン管理システムを使っているなら、パッチを当てた時点で一度 checkin することをオススメする。さうしておけば、次のコマンド一つで Rej ファイル対処分をキャンセルすることができる。
- C-x v u
- Revert (変更の巻き戻し) を行なう。RCS, CVS, Subversion といったバージョン管理システムで使える
3 way Merge
Rej ファイルも複雑になると、どこをどう直せばよいか分からなくなる。もしも (好運なことに)、オリジナルのファイル (rev1) と実験用のファイル (experimental) が手元にあるなら、3 way merge を試してみるとよいかもしれない。
おさらい。今、rev1 から派生した experimental なコードをローカルの original にマージしやうとしている。
(experimental) o--o--o
/ \
(original) -o--o--O--o--o---X
(rev1) (Conflict!)
この場合、3 way マージは次のコマンドで行なう。
- M-x ediff-merge-with-ancestor
- 「File A to merge」の入力を促されるので「original/hello.c」と入力
- 「File B to merge」の入力を促されるので「experimental/hello.c」と入力
- 「Ancestor file」の入力を促されるので「rev1/hello.c」と入力
すると、スクリーン・ショットのやうな画面が現れる。
画面は三分割されている。上段左側が A ウィンドウ。original な hello.c が表示されている。上段右側が B ウィンドウ。experimental な hello.c が表示されている。下段は、マージ用のウィンドウ。diff3 の出力結果が出力されている。
「a」キーで A ウィンドウの内容が、「b」キーで B ウィンドウの内容がマージ用ウィンドウにコピペされる。(今回の例のやうに) 両方の変更を取り込みたい場合は、直接マージ用ウィンドウの概当部分を編集する。
あとがき
理想を言えば、ちゃんとしたバージョン管理を使って、3 way merge を実行して conflict を修正する。よく分からない点は、experimental なり original のリビジョン・ログを読んで、どういう趣旨の変更かを理解してマージする。ということが出来るといい (darcs などは、かなり進んだマージが出来ると聞く)。
残念なことに、パッチだけしか存在しないとか、パッチだけしか提供されないとか、パッチだけが送り付けられた、という不幸もある。そんな場合に限って、Rej ファイルの数が 100 を越えたり、越えなかったり。
この記事が、そんな不幸と出会った人達の一助になれば嬉しい。