壊れたパッチと、その対処方法

はじめに

本記事は、何らかの理由によってパッチの内容が壊れてしまう理由と、その対処方法について述べます。IT系に限っても「パッチ」という言葉には様々な意味がありますが、本記事で言うパッチとは、diffコマンドやgit format-patchなどによって作成した、2つのテキストファイルの差分を表現するテキストファイルのことを指します。「パッチの内容が壊れる」とは、後述する様々な理由によって、パッチが、作成時とは異なる正しく適用できない状態になってしまっていることを言います。具体的にはタブが1つまたは複数のスペースに誤変換されることを指します。これ以降、単にこのような誤変換のことを「壊れる」と記載します。

本記事は、どちらかというとメーリングリスト(以下ML)ベースで開発をしている(せざるを得ない)旧来型の開発者向けです。「開発は全部githubとgitを使ってやるからパッチを直接扱うことなんて無いよ」とか「patchコマンドとかgit {apply,am}ってそもそも何?」という次世代開発者は本記事を見なくても問題無いです。

パッチが壊れる理由

「パッチが壊れる理由なんて知っとるわ、どう対処すればいいかだけ教えろ」というかたは、この節を飛ばして下さい。

ある人がパッチを作ったとします。それを誰か別の人と共有するには

  • パッチファイルを直接コピーして渡す
  • パッチの内容をwebページ(ブログなど)に貼り付ける
  • パッチを貼り付けたメールを個人ないしMLに送信する

などの方法があります。直接コピーなら大丈夫なのですが、それ以外の場合は、例えば次のような時にパッチが壊れます。

  • Webページの表示時: htmlソースには正しいパッチが挿入されていても、webブラウザ上では任意長のタブないしスペースから成る列(POSIX正規表現で言うと/:space:+/)は全て1つのスペースに変換されてしまう
  • メールの送信時: 送信時にタブをスペースに自動変換する(パッチという観点から見ると)邪悪なメールクライアント(参考)がある
  • 端末からwebページへのコピペ時: タブが、その文字幅に応じたスペースの列に変換されることがある

こうなると、誰かがせっかく書いて公開してくれた便利なパッチをみなさんが使いたくても使えないという問題が発生します(逆のパターンもあり)。パッチを書いた人に連絡して正しいパッチを貰えればいいのですが、連絡するのに気が引けたり、連絡が取れなかったり、はたまた投稿者本人もそのパッチをもう持っていないなどということもありえます。

壊れたパッチの具体例

linuxの開発MLに投稿されたカーネルパッチを題材として壊れたパッチの具体例を示しましょう。これは、ML投稿時には正しかったものの、MLアーカイブサイトの誤整形によって壊れてしまったパッチです。

linuxカーネルソースのコーディングスタイルでは、インデントは8文字幅のタブです。しかし、この壊れたパッチ内のインデントはすべてタブではなくスペースになってしまっています。これではpatchコマンドやgit {apply,am}コマンドなどにとっては、パッチ内の各断片をソース中のどこに適用してよいのかわからないため、適用に失敗します。

対処方法

本節では壊れたパッチに遭遇した場合の様々な対処方法について述べます。

別サイトを探す

あるMLアーカイブサイト上にあるパッチが壊れているからといって、別のサイトでもそうであるとは限りません。問題が発生していない別のサイトがあれば、そちらを見ればいいのです。例えばこれはさきほどのパッチ(が挿入されたメール)を別のサイト上で見たものです。こちらは、タブはタブとして正しく整形されています。

linux開発について言えば、主な開発MLに投稿されたパッチがpatchwork.kernel.orgに正しく適用可能な形式でまとめられています。例に挙げた述べたパッチについてはここにあります。

直接 html をダウンロードする

パッチの著者が何も考えずにパッチをそのままhtmlにベタコピーしているような場合は、curlなどを使ってhtmlファイルを直接ダウンロードして、そこからパッチ部分を抽出することによって正しく適用可能なパッチが得られます。

パッチ適用時にマッチングのルールを緩くする

patchコマンドやgit {am,apply-patch}コマンドの--ignore-whitespaceオプションを使えば(patchコマンドの場合は-lでも可)、パッチ適用時にタブとスペースに関するマッチングのルールを緩めてくれるため、壊れたパッチも適用可能になります。

ただしこれには注意が必要です。パッチ適用後には"+"で始まる行のコンテンツがそのまま元のテキストに追加されます。このため、タブやスペースに特別な意味を持たせている場合に問題が発生します。例えばタブがスペースに変換されているパッチを、タブにが特別な意味を持つMakefileに対して適用すると、その後正しく動作しないことがあります。また、このような問題が無いケースでも、パッチ適用後のコードがプロジェクトのコーディングスタイルに違反してしまうこともあるので、パッチ適用後にフォーマッティングツールを適用するとよいかもしれません。

壊れたパッチを手元で修復する

次のようなスクリプトを適用すると、ある程度修復作業が自動化できます。

#!/bin/bash                                                                                                                                                                                                                                

if [ $# -ne 1 ] ; then
    echo "usage: $0 <broken-patch>" >&2
    exit 1
fi

PATCH=$1

unexpand -a $PATCH | sed -e 's/^\t/ \t/' -e 's/^$/ /'

単にunexpandコマンドを使うだけだと、patchファイルにおいて特別な意味を持つ行頭の1文字目をうまく扱えないため、sedで行頭を追加整形しています。このスクリプトは次のように壊れたパッチを引数として受け取り、修正後のそれを標準出力に出力します。

$ ./patch-unexpand bad.patch >good.patch

タブ幅が8ではない場合はman 1 unexpandを参照の上、スクリプトのソースを改変してください(-tオプションを使うことになると思います)。

タブからスペースへの誤変換については、これでだいたいのものは修正できます。このスクリプによって作成した新たなパッチが依然適用できない場合は、元から意図的に連続スペースだったものをスクリプトがタブに誤変換してしまっていることが考えられます。しかし、それを考慮しても手作業で全て修正するよりは、事前にこのスクリプトを適用しておくほうが遥かに作業効率が高いです。

さらに、ここまでは触れませんでしたが、1行に所定の文字数(例えば80文字)以上書くと自動的に改行する(パッチという観点から見ると)極悪非道なサイト、メールクライアントがあります。たとえば前記の例に挙げたパッチの中の"buf_start,"で始まる行が該当します。この文字列は、本来この直前の行の末尾に配置されているべきものなのですが、サイトの設定によって、このように表示されてしまっています。このような整形をされた場合は、上記スクリプトでは全く救えません。

後は(残念ながら)修正不完全なパッチを、適用前のソースと見比べながら、手動でなんとか適用可能な状態にします。辛く苦しい作業ですが、しょうがないです。

おわりに

上記のスクリプトに加えて、パッチ内の修正箇所の前後数行と対応する元ソースとを比較して、より高精度な復元をするスクリプトの実装も可能かと思いますが、まずはここまで。

なお、「何でこんなダサいことしてるの?このコマンド使えば簡単じゃん」というような便利コマンドをご存知のかたがいれば教えてください。なお、{patch,{git {apply,am}}の--ignore-whitespaceオプションについては優しい人に教えていただいたので、後から追記しました。