Pythonでgitのコミットログをパースしてみよう！

#目次

1. はじめに
2. subprocessモジュール
3. Commitオブジェクトの作成
4. コミットログをパースする

## はじめに

みなさん、Pythonで `git` を使ったモジュールを作ったりしたいと思ったときはありませんか？
僕はあります。現在進行中のプロジェクトです。

というわけで、今回は **PythonでGitのコミットログをパースする** というテーマで書きます。

## subprocessモジュール

gitを使ったモジュールを使うためには、まず **gitコマンドの出力結果を取得する** 必要があります。
そこで、 `subprocess` モジュールを使ってgitコマンドの出力結果を取得する ``git関数`` を作りましょう。

```py:git.py
from subprocess import Popen, PIPE

class GitError(BaseException):
    """git関数の取得結果でエラーがでた場合に発生する"""
    pass

def git(cmd, *args):
    """gitの実行結果を取得して返す"""
    proc = Popen(
        ("git", cmd) + args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
    out, err = proc.communicate()
    if len(err) == 0:
        return out[:-1]
    else:
        raise GitError(err)

```

簡単に関数の中身を説明すると（括弧内は行番号)、subprocessモジュールから `Popen` クラスと `PIPE` 変数をImportし(1)、Popen関数で `git` のプロセスを定義します。(8~9)
Popenクラスの **communicate** メソッドで定義したプロセスを実行させます。実行させると、 **標準出力の出力結果と、標準エラーの出力結果がタプルで返されます** ので、 out変数とerr変数で標準出力の出力結果と、標準エラーの出力結果を取得します。(10)その後、標準エラーがあるかどうかを確認し、あれば `GitError` 
クラスをraiseします。なければ、そのまま標準出力の結果を返します。

ざっとこんなかんじです。次は、このgit関数を使ってコミットログを取得しましょう。

## Commitオブジェクトの作成

その前に、やることがあります。パースした **コミットログを保管するCommitクラス** を作ります。

```py:commit.py
class Commit(object):
    """コミットのハッシュ値、製作者、日付時間、コミットコメント、マージしたのであればマージしたデータを保持するクラス"""
    def __init__(self, commithash, author, date, comment, merge_data=None):
        self.commithash = commithash
        self.author = self._get_user_obj(author)
        self.date = date
        self.comment = comment
        self.merge_data = merge_data

    def _get_user_obj(self, author_string):
        if " " in author_string:
            lines = author_string.split(" ")
            name = lines[0]
            email = lines[1][1:-1]
            return User(name, email)
        else:
            return User(author_string)


    def __repr__(self):
        return "CommitObject(%s)" % self.commithash
```

ビルトインの `Object` クラスを継承してクラスを作ります。

Commitオブジェクトが保持したいデータは、

1. コミットのハッシュ値
2. コミッター
3. コミットした日付・時間
4. コミットコメント
5. マージデータ(なければNone)

これらです。なので、 `__init__` メソッドではそれぞれを引数にとり、Selfクラスに値をつけているだけです。

`_get_user_obj` メソッドは、もともとCommitオブジェクトに挿入されるコミッター情報は、 ``Author <email@sample.com>`` というデータです。なので、この文字列を `Author` と `Email` という情報に分けるのが役割です。この2つの情報を保持させるクラスが、 **User** クラスです。

```py:user.py

class User(object):
    """コミットした者の名前とメールアドレスを保持"""
    def __init__(self, name, email=None):
        self.name = name
        self.email = email

    def __repr__(self):
        return "UserObject(name=&#39;%s&#39;, email=&#39;%s&#39;)" % (
                                    self.name, self.email)
    def __str__(self):
        return self.__repr__()
```

このクラスは簡単です。ただ単にAuthorをEmailを保持させるためだけのクラスです。

## コミットログをパースする

先にコードからいきましょう。

```py:parser.py

from dateutil import parser as dateparser 

def get_commits():
    """シェルからコミットログを取得してCommitオブジェクトにしてリストで返す"""
    commits = []
    commits_string = git("log")

    # analyzing
    commitlines = commits_string.split("\n\n")
    commits_string_list = []
    while 0 < len(commitlines):
        commits_string_list.append(
                    "\n".join(commitlines[0:2]))
        commitlines = commitlines[2:]

    # parse
    for commitline in commits_string_list:
        commitdata = parse_commit(commitline)
        commits.append(
            Commit(**commitdata))

    return commits


def parse_commit():
    """シェルから取得したコミットログをパースして、コミットのハッシュ値、コミットした者、日付・時間、コミットコメント及びマージした場合はsourceとdestinationを返す"""

    commitlines = commitstring.split("\n")

    # 1 row
    commithash = commitlines[0].replace("commit ","")

    # 2 row
    merge_source = merge_dest = None
    author = None
    if "Merge" in commitlines[1]:
        # parse merge data
        merge_source = commitlines[1].split(" ")[1]
        merge_dest = commitlines[1].split(" ")[2]
    else:
        # parse author
        author = commitlines[1].replace("Author: ","")

    # 3 row
    if "Author" in commitlines[2]:
        # parse author
        author = commitlines[2].replace("Author: ","")
    else:
        # parse date
        date = dateparser.parse(commitlines[2].replace("Date:   ",""))

    # 4 row
    if "Date" in commitlines[3]:
        # parse date
        date = dateparser.parse(commitlines[3].replace("Date:   ",""))
    else:
        # parse comment
        comment = " ".join([i.strip() for i in commitlines[3:]])

    # 5 row
    if "Merge" in commitlines[1]:
        # comment -> under 4 row
        comment = " ".join([i.strip() for i in commitlines[4:]])

    return {"commithash": commithash,
            "merge_data": (merge_source, merge_dest) if merge_source != None else None,
            "author"    : author,
            "date"      : date,
            "comment"   : comment}

```

```
commit c92b57f9418eddf75cd96e268bac616aaecd95c4
Merge: 46a71a7 137a6ca
Author: alice <takemehighermore@gmail.com>
Date:   Sat Nov 17 01:00:14 2012 +0900

    Merge branch &#39;rewrite&#39;

    Conflicts:
        commithash.py
```

普通のコミットログは上記のような形になっています。(Mergeがあるバージョン）
上記のパーサーはこのコミットログを一行ずつ評価していき、コミットのハッシュ値とマージデータ、コミッターに日付・時間、コミットコメントを取得するスクリプトです。1行ずつ説明するのはすごく面倒なので省略させてください…
まぁでもコード読めばわかると思います。そんなに高度なことはしてませんので。

このコミットデータをどう扱うかは君次第！！