本記事の対象
日本語原稿を git で管理し、各コミットの文字数で進捗を評価しているが、推敲した回は字数が増えないので、サボったみたいで気に入らない人。
やること
git のコミット間の編集距離(レーベンシュタイン距離)を比較する Python3 スクリプトを書く。
git コマンドでの比較
まず以下のような原稿がある。
むかしむかし、あるところにおじいさんとおばあさんが住んでいました。
おじいさんは山へ芝刈りに、おばあさんは川へ選択に行きました。
2行目に誤字があるのでこれを修正(芝→柴、選択→洗濯)し、再度コミットする。
むかしむかし、あるところにおじいさんとおばあさんが住んでいました。
おじいさんは山へ柴刈りに、おばあさんは川へ洗濯に行きました。
変更箇所を知りたい場合は git diff
を使う。
$ git diff HEAD^ HEAD momotaro.txt
...
むかしむかし、あるところにおじいさんとおばあさんが住んでいました。
- おじいさんは山へ芝刈りに、おばあさんは川へ選択に行きました。
+ おじいさんは山へ柴刈りに、おばあさんは川へ洗濯に行きました。
編集した行数を知りたいときは git diff
に --numstat
オプションをつければわかる。1行追加し1行削減している。
$ git diff --numstat HEAD^ HEAD
1 1 momotaro.txt
だが現実的に言って、日本語原稿で1行単位は大雑把すぎる。文字単位での diff をとりたい場合は --word-diff-regex
オプションをつける。
$ git diff --word-diff-regex='.' HEAD^ HEAD momotaro.txt
...
むかしむかし、あるところにおじいさんとおばあさんが住んでいました。
おじいさんは山へ[-芝-]{+柴+}刈りに、おばあさんは川へ[-選択-]{+洗濯+}に行きました。
なお一部環境ではこの結果が文字化けするので、その場合はオプションを以下のように修正する。(手元の Debian 環境ではうまくいくが macOS では修正が必要)
$ git diff --word-diff-regex=$'[^\x80-\xbf][\x80-\xbf]*' HEAD^ HEAD momotaro.txt
ただこの --word-diff-regex
と --numstat
を併用する方法が見当たらなかった(行単位の変化が出てしまう)ので、仕方なく Python で編集距離を測るスクリプトを作ることにした。
Python で編集距離を比較する
使うのは Levenshtein というモジュール。まずこれを入れる。
$ pip install python-Levenshtein
これ自体の使用方法はきわめて簡単。
#!/usr/bin/env python3
import sys
import Levenshtein
print(Levenshtein.distance(sys.argv[1], sys.argv[2]))
$ ./levenshtein.py 三島由紀夫 三波春夫
3
次に Python スクリプト内で git コマンドを実行する。GitPython というライブラリがあるらしいけど、内部で subprocess を走らせてるだけらしいので、直接 subprocess でいいや。
#!/usr/bin/env python3
import sys
import subprocess
from subprocess import PIPE
import Levenshtein
commit1 = sys.argv[1]
commit2 = sys.argv[2]
# 3個目の引数があればそのファイルを、なければ編集した全ファイルを比較
try:
filenames = [ sys.argv[3] ]
except IndexError:
proc0 = subprocess.run(["git", "diff", "--name-only", commit1, commit2], stdout=PIPE, stderr=PIPE, text=True)
filenames = proc0.stdout.split()
print("{}\t{}\tdiff\tLeven".format(commit1, commit2))
total1 = 0
total2 = 0
total_leven = 0
for filename in filenames:
proc1 = subprocess.run(["git", "show", "{}:{}".format(commit1, filename)], stdout=PIPE, stderr=PIPE, text=True)
text1 = proc1.stdout
proc2 = subprocess.run(["git", "show", "{}:{}".format(commit2, filename)], stdout=PIPE, stderr=PIPE, text=True)
text2 = proc2.stdout
leven = Levenshtein.distance(text1, text2)
l_text1 = len(text1)
l_text2 = len(text2)
total1 += l_text1
total2 += l_text2
total_leven += leven
print("{}\t{}\t{}\t{}\t{}".format(l_text1, l_text2, l_text2-l_text1, leven, filename))
print("{}\t{}\t{}\t{}\t{}".format(total1, total2, total2-total1, total_leven, "TOTAL"))
$ ./gitleven.py HEAD^ HEAD
HEAD^ HEAD diff Leven
66 66 0 3 momotaro.txt
66 66 0 3 TOTAL
これで総字数の変化がなくても3文字の進捗があったことがわかり、桃太郎も後顧の憂いなく鬼退治に集中できる。
参考資料
こちらの記事を参考に、Perl がわからんので Python で書いた。
https://stackoverflow.com/questions/23974443/how-to-get-an-edit-distance-between-two-commits