2008-09-24

Emacs で Rej ファイルを処理する人への 10 ステップ

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 ファイル対処の流れ

  1. Reject ファイルのリストを作る
  2. Rej ファイルを開く
  3. Rej ファイル専用のモード
  4. Rej ファイルとローカル・ファイルのカーソル位置を同期させる
  5. 行頭の + 記号を削る
  6. Rej ファイルとローカル・ファイルの比較
  7. この変数は使われているのか? と調べてみる
  8. タグ・ジャンプ
  9. 変更の巻き戻し
  10. 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 カラム目)。

Emacs - rectangle region

そして、C-x r k を実行。すると、行頭の + 記号が削除される。

C-x r k
kill-rectangle (短形削除コマンド) を実行する

Rej ファイルとローカル・ファイルの比較

Rej ファイルが出来るシチュエーションの一つに、experimental での変更分を original でも既に行なっていた、というものがある。この場合、既に変更してある所に同じ変更を加えやうとして、patch コマンドは混乱し Rej ファイルを作り出す。

プログラマーは、Rej ファイルの中身と original が同じことを確認したら、何も変更を加えず Rej ファイルを閉じればいい。

問題は確認する方法。もしかしたら、1 行だけ違うかもしれない。もしかしたら、マスク一箇所分だけ違うかもしれない。もしかしたら演算子 1 個分だけ... 目で確認するのは、愚直に過ぎる。

ediff-windows-linewise を使う

こんな感じに色付してくれると、変更場所が (あるなら) 分かって嬉しい。

Emacs - ediff-windows-linewise

スクリーン・ショットでは、rsvg の行が足りないことが見て取れる。

ediff-windows-linewise の使い方
  1. ウィンドウを分割して、片方に rej ファイル、もう片方にローカル・ファイルを表示させる。
  2. お互いのウィンドウが同じ部分を表示するようにする (ex. next-error-follow-minor-mode を使って同じ部分を表示し、更に比較したい行で C-u 0 C-l, C-x o, C-u 0 C-l を実行する)
  3. 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 マージは次のコマンドで行なう。

  1. M-x ediff-merge-with-ancestor
  2. 「File A to merge」の入力を促されるので「original/hello.c」と入力
  3. 「File B to merge」の入力を促されるので「experimental/hello.c」と入力
  4. 「Ancestor file」の入力を促されるので「rev1/hello.c」と入力

すると、スクリーン・ショットのやうな画面が現れる。

Emacs - ediff-merge-with-ancestor

画面は三分割されている。上段左側が A ウィンドウ。original な hello.c が表示されている。上段右側が B ウィンドウ。experimental な hello.c が表示されている。下段は、マージ用のウィンドウ。diff3 の出力結果が出力されている。

「a」キーで A ウィンドウの内容が、「b」キーで B ウィンドウの内容がマージ用ウィンドウにコピペされる。(今回の例のやうに) 両方の変更を取り込みたい場合は、直接マージ用ウィンドウの概当部分を編集する。

あとがき

理想を言えば、ちゃんとしたバージョン管理を使って、3 way merge を実行して conflict を修正する。よく分からない点は、experimental なり original のリビジョン・ログを読んで、どういう趣旨の変更かを理解してマージする。ということが出来るといい (darcs などは、かなり進んだマージが出来ると聞く)。

残念なことに、パッチだけしか存在しないとか、パッチだけしか提供されないとか、パッチだけが送り付けられた、という不幸もある。そんな場合に限って、Rej ファイルの数が 100 を越えたり、越えなかったり。

この記事が、そんな不幸と出会った人達の一助になれば嬉しい。

No comments:

Post a Comment