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

光栄にも[ラクス Advent Calendar 2016](http://qiita.com/advent-calendar/2016/rakus)のクリスマス・イブを担当させていただいております。責任持ってイブに向けて（Mステ見ながら）投稿しております。

----

# はじめに

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

## Pythonを使った集計

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

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


# GitPython

PythonでGitを扱うには[GitPython](https://github.com/gitpython-developers/GitPython)という便利なライブラリがあります。 インストール手順などは[公式ドキュメント](http://gitpython.readthedocs.io/en/stable/index.html)を参照してください。

## コミット情報の取得

`Repo('/path')`でローカルのリポジトリを指定して取得します。そして [Repo.iter_commits](http://gitpython.readthedocs.io/en/stable/reference.html?highlight=iter_commits#git.repo.base.Repo.iter_commits) で特定のブランチのコミット情報を取得できます。

```py
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リファレンス](http://gitpython.readthedocs.io/en/stable/reference.html#module-git.objects.commit)を参照してください。

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

```text:出力例
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`を指定するとシンタックスエラーになりました。[ここ](https://git-scm.com/docs/git-rev-list#git-rev-list---no-merges)に書いてあるように`git log`のオプションの仕様としては`max-parents=1`と同じなので`max_parents=1`でいけました。

## revision rangeの指定

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


# ログ集計・分析への応用

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

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

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

```py
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))
```

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

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

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

```py
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](http://orange.biolab.si/)というライブラリでアソシエーション分析ができます。

### Orangeの使い方

公式ドキュメントの[Association rules and frequent itemsets](http://docs.orange.biolab.si/2/reference/rst/Orange.associate.html)によると、分析対象となるリストをCSV形式で`.basket`という拡張子を付けて保存すればOrangeがアソシエーション分析してくれるようです。そのため１回のコミットで一緒にコミットされたファイルをCSV形式で出力させることにします。

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

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

```py: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()))
```

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

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

```py
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)
```

```text:出力例
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へのマージ対象なのでダブルドット構文を使ってコミットを取得しそれに紐づくファイルを出力します。

```py: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()))
```

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

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

```py
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()))
```

```text:出力例
['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を０．５にすると思った以上に候補が出力されました。このあたりはプロジェクトの状況や特性に合わせてチューニングするのがいいと思います。


# おわりに

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

# 参考
[OrangeでPythonからAprioriを動かす](http://qiita.com/gingi99/items/8c97f9c304b654b7d3d4)
[Gitのログをワンライナーで整形する](http://qiita.com/radiocat/items/3abba165d259f8db5642)


---

楽しかった [ラクス Advent Calendar 2016](http://qiita.com/advent-calendar/2016/rakus) も明日でいよいよ最終日です。 @kawanamiyuu さんが最後を締めくくってくれますのでお楽しみに。
それではみなさん良いクリスマスを。