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では簡単に確認できません(パッチは当てられます)

まとめ

Pypiwhatthepatchパッケージを利用すると、プラットフォームに依存せずpythonの実行環境さえあれば、容易にパッチを当てることが出来ることが分かりました。

未分類python

Posted by first_user