Edited at
ラクスDay 24

GitのログをGitPythonを使って集計しOrangeを使ってアソシエーション分析する

More than 1 year has passed since last update.

光栄にもラクス Advent Calendar 2016のクリスマス・イブを担当させていただいております。責任持ってイブに向けて(Mステ見ながら)投稿しております。



はじめに

git logコマンドはいつ誰が何をどのようにコミットしたかを知ることができる、バージョン管理に欠かせない便利なツールです。しかしこれを品質の管理や分析に活かそうとしたら少し工夫が必要です。序章ではgit logをワンライナーで整形する方法をまとめてみましたが、1行でできることには限界があります。実業務では毎朝ログを集計して開発の状況を確認したり、プロジェクトの終盤でテストの計画や品質の評価をするなど、ログの整形したうえでほかにもやることがあるので今回はそのような実践的なログの活用法を考えてみました。


Pythonを使った集計

というわけで今回はGitPythonを使ってGitのログを集計・分析する方法をまとめました。他の言語でもGitを扱う方法はあると思いますが、プロジェクトの状況に応じて品質をチェックしやすいように手軽にコードを書き換えて集計や分析ができる言語ということで、集計や分析が得意なスクリプト言語としてPythonが扱いやすいと考えました。ちなみに、Pythonは今回紹介するようなちょっとした集計などの使い捨てレベルのコードしか書いたことがないのでコードの拙い部分があるかもしれませんがご容赦ください。何かありましたらコメントお願いします。

なお、ここにまとめたサンプルコードはPython 2.7で確認しています。


GitPython

PythonでGitを扱うにはGitPythonという便利なライブラリがあります。 インストール手順などは公式ドキュメントを参照してください。


コミット情報の取得

Repo('/path')でローカルのリポジトリを指定して取得します。そして Repo.iter_commits で特定のブランチのコミット情報を取得できます。

from git import *

import datetime, time

repo = Repo('./')
for item in repo.iter_commits('master', max_count=10):
dt = datetime.datetime.fromtimestamp(item.authored_date).strftime("%Y-%m-%d %H:%M:%S")
print("%s %s %s " % (item.hexsha, item.author, dt))

取得できるコミット情報はObjects.CommitのAPIリファレンスを参照してください。

上の例はカレントディレクトリにあるリポジトリ上のmasterブランチから直近10件のGitログのハッシュ値、コミットしたユーザー、コミット日時を出力します。


出力例

ddffe26850e8175eb605f975be597afc3fca8a03 Sebastian Thiel 2016-12-22 20:51:02 

3d6e1731b6324eba5abc029b26586f966db9fa4f Sebastian Thiel 2016-12-22 20:48:59
82ae723c8c283970f75c0f4ce097ad4c9734b233 Sebastian Thiel 2016-12-22 20:44:14
15b6bbac7bce15f6f7d72618f51877455f3e0ee5 Sebastian Thiel 2016-12-22 20:35:30
c823d482d03caa8238b48714af4dec6d9e476520 Sebastian Thiel 2016-12-09 00:34:04
b0c187229cea1eb3f395e7e71f636b97982205ed Sebastian Thiel 2016-12-09 00:07:11
f21630bcf83c363916d858dd7b6cb1edc75e2d3b Sebastian Thiel 2016-12-09 00:01:35
06914415434cf002f712a81712024fd90cea2862 Sebastian Thiel 2016-12-08 22:32:58
2f207e0e15ad243dd24eafce8b60ed2c77d6e725 Sebastian Thiel 2016-12-08 21:20:52
a8437c014b0a9872168b01790f5423e8e9255840 Vincent Driessen 2016-12-08 21:14:27

ちなみに上記の出力例は2016/12/23時点のGitPythonのコミットログです。


Commit Limitingの指定

序章で紹介したgit logのCommit Limitingはiter_commitsの引数で指定できます(上記の例では max_count=10を指定)。,で複数指定もできます。また、-の部分は_に置き換えて指定します。

ちなみにno_mergeを指定するとシンタックスエラーになりました。ここに書いてあるようにgit logのオプションの仕様としてはmax-parents=1と同じなのでmax_parents=1でいけました。


revision rangeの指定

同じく序章で紹介したダブルドット構文も指定できます。これは単純に上記のmasterの部分をmaster..experimentのようにするだけです。


ログ集計・分析への応用

それではログの集計・分析への応用を考えてみます。前述のコードで取得したコミット情報にはコミットしたファイルの情報が全てがぶらさがっています。stats.filesでコミットされたファイル情報のリストが取得できます。


コミットファイルごとの情報詳細

コミットされたファイルごとの追加行、削除行をコミットした日時などの情報と合わせてCSV形式で標準出力する例です。

from git import *

import datetime, time

repo = Repo('./')
print('hexsha,author,authored_date,file_name,deletions,lines,insertions')
for item in repo.iter_commits('master', max_count=10):
file_list = item.stats.files
for file_name in file_list:
dt = datetime.datetime.fromtimestamp(item.authored_date).strftime("%Y-%m-%d %H:%M:%S")
insertions = file_list.get(file_name).get('insertions')
deletions = file_list.get(file_name).get('deletions')
lines = file_list.get(file_name).get('lines')
print("%s,%s,%s,%s,%s,%s,%s" % (item.hexsha, item.author, dt, file_name, insertions, deletions, lines))

コマンドだけでこれらの情報を取得して1行にまとめるのはかなり大変ですがGitPythonを使えばループをまわして取得するだけです。


ファイルごとのコミット回数

今度はコミット単位ではなくファイル単位で期間内に変更した回数を集計してみます。以下は6カ月以内にコミットされた回数をファイルごとに出力します。

from git import *

import datetime, time

repo = Repo('./')
print('file_name,commit_count')
file_list = {}
for item in repo.iter_commits('master', since='6 months ago'):
for fileName in item.stats.files:
if file_name not in file_list:
fileList[fileName] = []
author = {}
author[item.author] = datetime.datetime.fromtimestamp(item.authored_date).strftime("%Y-%m-%d %H:%M:%S")
file_list[file_name].append(author)

for file_name in file_list:
print("%s,%d" % (file_name, len(fileList[file_name])))

さらにこれを応用して、プロジェクトの期間内に絞ってファイル単位の変更行数を集計したり、コミットの間隔(前回変更されてからの日数)などを計算することもできそうです。ファイル単位だけでなく日単位や月単位、コミットした人単位などで集計することもできますね。


Gitログの活用

Pythonで自由に集計ができるようになれば、他のPythonライブラリに取り込んで分析したり、外部のツールやサービスと連携させるなど、ログを新たな手段に活用することが可能です。


アソシエーション分析

というわけで、いよいよコミット情報をアソシエーション分析することを考えみます。やりたいことは「このファイルを変更した人はこれも変更してますよ」という情報を取得する方法です。

Pythonでは Orangeというライブラリでアソシエーション分析ができます。


Orangeの使い方

公式ドキュメントのAssociation rules and frequent itemsetsによると、分析対象となるリストをCSV形式で.basketという拡張子を付けて保存すればOrangeがアソシエーション分析してくれるようです。そのため1回のコミットで一緒にコミットされたファイルをCSV形式で出力させることにします。


過去1年にコミットされたファイルをアソシエーション分析する

まずは前述の手法の応用でGitPythonを使って同時にコミットしたファイルをカンマ区切りで出力します。


commit-file-list.py

from git import *

repo = Repo('./')
for item in repo.iter_commits('master', since='1 years ago'):
print(",".join(item.stats.files.keys()))


$ python commit-file-list.py > commit-file-list.basket

Orangeのサンプルコードを参考に上記で作成したcommit-file-list.basketをアソシエーション分析してみます。

import Orange

data = Orange.data.Table('commit-file-list.basket')
rules = Orange.associate.AssociationRulesSparseInducer(data, support=0.02, confidence=0.5)
print "%4s %4s %4s" % ("Supp", "Conf", "Rule")
for r in rules:
if 'git/config.py' in r.name:
print "%4.1f %4.1f %s" % (r.support, r.confidence, r)


出力例

Supp Conf Rule

0.0 0.6 git/test/test_git.py -> git/cmd.py
0.0 0.2 git/cmd.py -> git/test/test_git.py
0.1 0.7 git/test/test_diff.py -> git/diff.py
0.1 0.7 git/diff.py -> git/test/test_diff.py
0.0 0.4 git/test/test_diff.py -> git/diff.py doc/source/changes.rst

supportは全体の中でルールが出現する割合なので今回のケースではあまり重要ではありません。そのためしきい値はなるべく低い値に設定しておきます。confidenceはルールの一部を前提条件としたうえでのルール全体が出現する割合(Aが含まれる全パターンを踏まえてAのときA,Bとなる割合)なのでまさに今回のケースではこの値が高いルールを重要視したいところです。


masterにマージする前に「このファイルも変更してますよ」をアソシエーション分析のルールから探す

まずはマージ対象となるファイルをコミット単位で1行のCSV形式で出力し、.basketファイルを作成します。masterへのマージ対象なのでダブルドット構文を使ってコミットを取得しそれに紐づくファイルを出力します。


merge-target-list.py

from git import *

repo = Repo('./')
for item in repo.iter_commits('master..experiment', max_parents=1):
print(",".join(item.stats.files.keys()))


$ python merge-target-list.py > merge-target-list.basket

前述の1年分のGitログを元にルールを抽出する手法を応用し、マージ対象のmerge-target-list.basketがルールのleftに一致するものを探し、rightもコミットしたほうがいいかもしれないという結果を出力します(例えば1年分のログからA,B,Cがルールとして抽出されて、マージ対象にA,Bが存在する場合Cが候補として出力される想定です)。

import Orange

data = Orange.data.Table('commit-file-list.basket')
rules = Orange.associate.AssociationRulesSparseInducer(data, support=0.02, confidence=0.5)
for r in rules:
if 'git/config.py' in r.name:
print "%4.1f %4.1f %s" % (r.support, r.confidence, r)

merge_data = Orange.data.Table('merge-target-list.basket')
for d in merge_data:
for rule in rules:
#print rule
if rule.applies_left(d):
print (u"%s をコミットした人は %3.1f %%の割合で %s もコミットしています" %(rule.left.get_metas(str).keys(), (rule.confidence*100), rule.right.get_metas(str).keys()))


出力例

['git/test/test_remote.py'] をコミットした人は 55.0 %の割合で ['git/test/lib/helper.py'] もコミットしています

['git/test/test_remote.py'] をコミットした人は 55.0 %の割合で ['git/test/test_base.py'] もコミットしています
['git/test/test_remote.py'] をコミットした人は 50.0 %の割合で ['git/util.py'] もコミットしています
['git/test/test_base.py'] をコミットした人は 55.0 %の割合で ['git/test/test_git.py'] もコミットしています
...(省略)

confidenceを0.5にすると思った以上に候補が出力されました。このあたりはプロジェクトの状況や特性に合わせてチューニングするのがいいと思います。


おわりに

Pythonには便利なライブラリがたくさんあるのでアソシエーション分析に限らず色々な応用ができそうです。本当はグラフを扱えるmatplotlibを使ったりChatBotを作る活用例も考えていましたが、アソシエーション分析が思いのほか長くなったので今回はここまで。続きは冬休みの宿題にしたいと思います。


参考

OrangeでPythonからAprioriを動かす

Gitのログをワンライナーで整形する


楽しかった ラクス Advent Calendar 2016 も明日でいよいよ最終日です。 @kawanamiyuu さんが最後を締めくくってくれますのでお楽しみに。

それではみなさん良いクリスマスを。