pythonでpatch
linuxだとdiff -uprN
でパッチを作成して、patch -p1 <パッチファイル
みたいなことをしてパッチを当てますが、この方法linuxでしか使えません。これをパッチ当てる部分だけpythonで使えるようにしたい、というのがこの記事の主旨です。
pypi(pip)でパッケージ探せばいいのかなぁと思ってたのですが、古かったり機能が足りなかったり、テスト用のmockのためのコードだったり、欲しいものとやや違ってて、当初は自作するしかという状況でした。しかし、最近 whatthepatch
というパッケージを見つけ、自作をやめて使用例を記事にすることにしました。
前提
- 差分はgit diffの出力を想定する
- encodingはUTF-8固定
- 行単位の差分のみを考慮する
- 単一のファイルだけではなくディレクトリ構造を含む差分を扱う
- 改行はプラットフォームの改行に統一されている
- 最終行は必ず改行を伴って出力される
上記前提のもと、patch -p1をpythonで実現することが目的です。
改行の扱いなどが緩いのであまり厳密なデータには向かず、patch本来のソースコード用になります。
whatthepatchを使ってパッチを当てる
パッチ当てのためのpypi
パッケージであるwhatthepatch
を使ってパッチを当てる手順を紹介します。
準備
まずはpypi
パッケージをインストールします。venv
などの環境でも構いません。
$ pip install whatthepatch
参考: https://pypi.org/project/whatthepatch/
whatthepatchを使ったコード
import whatthepatch, os, sys
with open(sys.argv[1], 'rt', encoding='utf8') as fp:
for diff in whatthepatch.parse_patch(fp):
header = diff.header
if header.new_path == '/dev/null': # 削除の場合
os.remove(header.old_path)
else:
if header.old_path == '/dev/null': # 新規の場合
old = []
else: # 更新の場合
with open(header.old_path, 'rt', encoding='utf8') as fo:
old = [line.rstrip(os.linesep) for line in fo]
if diff.changes is not None: # 変更がある場合
new = whatthepatch.apply_diff(diff, old)
else: # 変更がない=新規で空の場合
new = old
try: # 必要ならディレクトリを作成
dirpath = os.path.dirname(header.new_path)
if dirpath != "":
os.makedirs(dirpath)
except FileExistsError:
pass
with open(header.new_path, 'wt', encoding='utf8') as fn:
for line in new:
print(line, file=fn)
引数で指定されたdiff
ファイルを元にディレクトリ構造を持つ差分をparse
して、新規/更新/削除に分けて処理しています。ライブラリはparseまではしてくれますが、種類の判別などはしてくれず、単体ファイルの新規/更新処理に特化しています(新規空ファイルは不可)。
diff
ファイルを用意する
他人様のリポジトリを使うのも気が引けるので、自分のリポジトリを使ってdiff
ファイルを用意します。
$ git clone https://git.elephantcat.work/first_user/django_apps.git
$ cd django_apps
$ git diff `git rev-list --max-parents=0 HEAD | tail -n 1` >../patch.diff
clone
しているリポジトリの初期コミットからHEADまでの差分をpatch.diff
として出力しています。
パッチ適用前のコードを用意する
diff
ファイルを作成したリポジトリでそのまま作業します。
$ mkdir ../initial_commit
$ git archive --format tar `git rev-list --max-parents=0 HEAD | tail -n 1` | tar xvf - --directory=../initial_commit
django_apps
の隣に、initial_commit
という名前のフォルダが出来ており、そこに初期コミットのファイル群がtar
アーカイブリダイレクト経由で展開されています。サブシェル呼出部分は単に初期コミットのリビジョンを取得しているだけです。Windows
の場合は手動で実施し、tar
ではなくzip
を使ってください。
パッチを適用する
先のソースコードが./../patch.py
とし、上で用意したinitial_commit
ディレクトリがカレントディレクトリだとして、以下でパッチを適用します。
$ python ../patch.py ../patch.diff
正しく実行されるとHEADの内容とほぼ同じ内容になっています。
結果を確認する
リポジトリが丁度HEAD
になっているので、このディレクトリの.git
フォルダを削除して比較対象とします。initial_commit
ディレクトリがカレントだとして、以下のようにします。
$ cd ../django_apps
$ rm -rf .git
$ cd ..
$ diff -uprN django_apps initial_commit
少し差分が表示されますが、ファイルの末尾の改行の違いだけなので、問題ありません。空白を無視して差分を取ると(--ignore-space-change
)差分がなくなります。
※Windowsでは簡単に確認できません(パッチは当てられます)
まとめ
Pypi
のwhatthepatch
パッケージを利用すると、プラットフォームに依存せずpythonの実行環境さえあれば、容易にパッチを当てることが出来ることが分かりました。
ディスカッション
コメント一覧
まだ、コメントがありません