2012年4月21日土曜日

[python] コミットメッセージのprefixを自動生成するbzr pluginを作った。

背景


コミットメッセージ入れるのめんどくさい。
そして、他のメンバーにコミットメッセージ規約を守ってもらうのは難しい。

で、過去のコミットメッセージを参考にして、
コミットメッセージをレコメンドしてくれるbzr pluginを作ってみた。


ソース


ソースは以下。
gist: ec92107adc5315a74aed

これを ~/.bazaar/plugin/suggest.pyに保存すればよい。

使い方


bzr suggestと入力すると、現在編集したファイルと、
過去にコミットされたファイルを比較して、
関係ありそうなコミットのメッセージを表示する。


[karino@localhost trunk]$ bzr suggest

changed files 
 + apps/frontend/modules/job/actions/actions.class.php

Loading commit logs...
 +  karino karino  :  1.0
 +  first commit  :  0.0157465671305
 +  karino  :  1.0
 +  test commit  :  1.0
 +  karino  :  0.707106781187
 +  commit  :  1.0

recommended prefix : karino

commit message detail > karino

commit message : karino karino

Select action
 + j: set prefix and commit
 + k: no prefix commit
 + l: no commit
............... j/k/l > j

exe commit  karino karino
Committing to: /home/karino/repo/bzr_test/trunk/                                                                                                                                                                                            
modified apps/frontend/modules/job/actions/actions.class.php
No syntax errors detected in /home/karino/repo/bzr_test/trunk/apps/frontend/modules/job/actions/actions.class.php                                                                                                                           

Congratulations! There are no errors.
No syntax errors detected in /home/karino/repo/bzr_test/trunk/apps/frontend/modules/job/actions/actions.class.php                                                                                                                           

Congratulations! There are no errors.
Committed revision 12.                                                                                                                                                                                                                      




技術的問題点の検討


1、コミットメッセージを指定してコミット
まず、「どうやってコミットメッセージを指定してコミットするか」

① hookスクリプトを作る。
一番スマートな方法だけど、
コミットメッセージを指定・変更する方法が、わからん。
ちょっと覚悟を決めてソース読みにかからないと難しい。。

1,start_commit
→ コミットメッセージとか変更できるのかわからんかった。

2, pre_commit
→ コミットメッセージとか変更できるのかわからんかった。

3,commit_message_template
→ 使い方わからん。

4,set_commit_message
→ まさにやりたいのこれだけど、2.4からしか使えないorz。
aptでinstallしても2.1しか入らない。
「このプラグインを使うためには、bazaarを2.4にあげてください」って言うのは、
ちょっときついな。

② commitするコマンドを自前で作る。
組む量も多くなるし、あまりスマートではない。
そして、しっかり理解したうえで作ってる感じじゃないから、
何かしらの不具合が出る可能性が残る。。

http://people.canonical.com/~mwh/bzrlibapi/bzrlib.commit.Commit.html#commit

def run(self, revision=None):   
        from urlparse import urlparse  
        from bzrlib import commit
        from bzrlib.workingtree import WorkingTree

        wt = WorkingTree.open_containing(os.getcwd())[0]
        cm = commit.Commit()   
        revno = cm.commit(message="test commit",working_tree=wt)
  
        print revno            
        return


③ そもそもbzr pluginじゃなくしちゃう。
bzr logたたいて過去のコミットメッセージ取得して、
bzr statusたたいて今の変更ファイル名取得して、
bzr commitでコミットする・・・。
まぁ、②よりも安全な方法ではあるが、
やりたくないな。。

【結論】
とりあえず、②で作って、そのうち①のどれかのやり方を調べる。


2、関連するコミットメッセージの抽出
次に問題となるのは「過去のコミットメッセージの抽出方法」

あんまり速度遅くなってもやなので、
過去100件に絞り、特異値分解はやらない。
ただ単に、cos計算するだけ。



3、コミットメッセージの作成
「コミットメッセージの作成」
ファイルの修正内容とかからコミットメッセ^時を作成しても面白いと思ったけど、
チョット大変そうだったから、とりあえず、プレフィックスとして使えそうな文字列を抽出する事にした。

① prefixの作成
修正するファイルから抽出した関連するコミットメッセージ上位10件に、
一番共通して存在する単語をprefixとする。

語句の分割には、mecab使いたかったけど、そこまでする必要ないかなって思って、
ただ単に、スペースで分割した。

重要な語句の定義を、「一番出現回数が多い」単語を重要な語句としているけど、
tf-idfとかで重み付けした方が精度上がるかと思ったが、まだやってない。


② 詳細の作成(めも)
編集されているファイルから修正内容を予想する。
*.yml → テーブル追加 or 変更
PHPファイル2,3個+数行修正 → バグfix
PHPファイル2,3個+数十行 → 機能追加
とか。


To Do

① いかんせん遅い。
リビジョン番号毎に編集されたファイルを抽出しているところ、
branch.repository.get_revision_delta(target_id)がめちゃくちゃ遅い。
もっと速くファイル抽出する方法を探すか、
この関数を呼び出す回数を少なくする方法を考える。

② レコメンドの精度を上げたい。

tf-idfで重み付け。


③ hookにしたい。
commit_message_template、set_commit_messageなりを使って、
通常のcommitにこの機能をつけるようにしたい。

2012年4月8日日曜日

[python][php] tagファイルを読んで、依存関係のあるファイルを抽出するbzrプラグイン。

前から作ろうと思ってけど、いまいちのらなくて作ってなったやつ。

背景


今の開発環境では、プラットフォームが3つあり、
それぞれに本番環境、stg環境がある。
bazzarのレポジトリとはプラットフォームで共通のものを使っている。
stg環境では、レポジトリに含まれるプログラムが動作しており、
本番環境では、stg環境の中から展開されたものだけが動作している。
※ 展開は、ファイル・リビジョン番号を指定して、stg環境から本番環境にプログラムを移すことをさす。

そのため、本番環境のプログラムはプラットフォームごとに異なるが、
stg環境のプログラムはプラットフォームで共有となっており、
実質、3つの本番環境と1つのstg環境で運用していることになる。

ある機能について「プラットフォームAでは今日出すが、プラットフォームBでは来月出す」ってときは、
Aプラでは本番環境に展開されているが、
Bプラでは本番環境にはプログラムが展開されていないという状態になる。。
そして、Bプラでその機能を公開するときは、stgのプログラムを本番環境に展開する。

このとき、展開しておかなきゃいけなかったプログラムを展開し忘れる、展開もれが発生することがある。
もれるともちろん、参照する先のファイル、クラス、ファンクションがないので、
大量のエラーログがはかれることになる。
とくに、依存関係のあるプログラムで起こりやすい。
「え、これも展開しなきゃいけないの!?」って声はちょくちょく聞く。

「完全にレポジトリ分ける」か、
「Aプラが出すときにプログラムは全プラ一緒に出して、機能は公開しない」ってこと
ができればいいんだけど、どちらも現状難しそう。
直近対応として、「あるプログラムに対して依存関係のあるプログラムをリストにする」bzr pluginを書いた。
これを使って、依存関係のある全ファイルの、本番との差分を確認すれば展開漏れはすくなるなる気がする。


流れ

① あるリビジョン番号で追加・修正されたファイルを抽出する。
② それらのファイルから、参照しているクラスを抽出する。
③ クラスファイルのパスをtagファイルから特定する。
④ 本番との差分を確認するコマンドを表示する。


ソース


ソースは以下。
gist: 2335403

これを、~/.bazzar/plugin/rel.pyとして保存すれば、使えるようになる。


以下、中身を備忘録として残しておくか。

① リビジョン番号からファイルを抽出
bzr deployの流用。
[python] 修正内容をscpで転送するbzrコマンド。

② クラスの抽出。
クラスメソッドとインスタンスをnewしているとこから、
正規表現でクラス名を抽出した。

    def get_class(self, file_obj):
        func_list = []
        for line in file_obj:
            
            # get by ::
            func = self.re_search(line, r'([A-Z]\w+)::')
            if func != None and not(func in func_list):
                func_list.append(func)
            
            # get by new
            func = self.re_search(line, r'new ([A-Z]\w+)')
            if func != None and not(func in func_list):
                func_list.append(func)
            
        return func_list

ここでは、クラス名の先頭が大文字である事を前提としている。
「クラスメソッドの参照」と「メンバ定数の参照」をどうやって区別すればよいかわからなかったため。。
つまり、TEST::test1()と、$test::TEST_NAMEの違い。
たいてい、クラスは大文字から始まって、変数は小文字にするでしょ?っていう逃げ。
そのうち何とかしよう。


② tagファイルの読み込み
全部読むとかなり長くなってしまいそうだったので、
上から順番に読んでいって、あったらそこで読むのやめている。

また、どうやらでふぉのctagsで作られるタグファイルは、
[A-Za-z]見たいな並び方で作っているようなので、
対象になるクラス名の先頭と1文字、タグファイル読んだクラスの中で一番新しいクラス先頭1文字を比較し、
タグのほうが進んでいたら、それ以降に対象になるクラス名は出てこないはずだから、
そこで読むのをやめている。

つまり、zzとかで始まる奴が遅くなる。
これもあって、クラス名の先頭は大文字で始まるやつのみに絞るってことをしてしまった。

    def get_ctags_file_path(self, func_name):
        # chk target file
        for row in self.CTREADER:
            
            # read one
            tmp = []
            if row[0] in self.CTDICT.keys():
                tmp = self.CTDICT[row[0]]
            tmp.append(row[1])
            self.CTDICT[row[0]] = tmp
            self.LATEST_ASCII_HEAD = int(hex(ord(row[0][0])),16)
            
            # search
            if func_name in self.CTDICT.keys():
                return self.CTDICT[func_name]
            
            # check ascii head
            func_name_ascii_head = int(hex(ord(func_name[0])),16)
            if self.LATEST_ASCII_HEAD > func_name_ascii_head:
                return
        return
self.CTREADERは、タグファイルをcsv.readerで読んだrederオブジェクト。
self.CTDICTは、今までよんだ内容を保存しとくやつ。


③ 本番との差分を確認
社内ではmakuoっていうデプロイコマンド?を使っている。
このコマンドをdryrunすると、本番のファイルと差分を確認してくれるので(タイムスタンプのみだっけ?)、
dryrunするためのコマンドを表示する。


使い方

① tagファイルの作成
    ctags "-f" "./tags" "--langmap=PHP:.php.inc" "--php-types=c+f+d+v+i" "-R" "./lib/"
    ctags "-a" "-f" "./tags" "--langmap=PHP:.php.inc" "--php-types=c+f+d+v+i" "-R" "./plugins/"
    ctags "-a" "-f" "./tags" "--langmap=PHP:.php.inc" "--php-types=c+f+d+v+i" "-R" "./apps/"

② コマンド実行
[karino@115x125x111x181 trunk]$ bzr rel 5

dependence_file

apps/frontend/modules/job/actions/actions.class.php
|
+-- Doctrine_Core
|   ['./lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/vendor/doctrine/Doctrine/Core.php']
|
+-- Request
|   None


config/ProjectConfiguration.class.php
|
+-- CoreAutoload
|   None

-------------------------------

makuo -n \
game12/./lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/vendor/doctrine/Doctrine/Core.php \
game12/apps/frontend/modules/job/actions/actions.class.php \
game12/config/ProjectConfiguration.class.php

-------------------------------


こんな感じ。


ToDo

① クラス抽出をもうチョット何とかする。
・一行に複数の候補があったとき、最初のクラスしか抽出できない。
・先頭が大文字のクラスしか対象にしていない。

② tagの読み方
もっと速くなる読み方検討。

③ 再帰的に調べる
そこまでやる必要ないかな。。

2012年4月7日土曜日

[vim][python] 社内で「vim」って発言したら、僕まで伝わっちゃうけどあしからず。

社内で交わされているvimネタはすべて押えておきたい。

そこで、いろんなチャンネルで「vim」を含む発言があったら、
「#vim」チャンネルにその発言を流すIRCボットを作った。

結構簡単に書けたけど、pingがうまく返っていなかったことで引っかかった。
try and errorで書いたから、無駄なところ結構ある。
もうチョットちゃんとpythonかけるようになろう。
っていっつも思って何もしてないorz。


この辺とか参考にした。
Python で IRC クライアント的な


ソース


#!/usr/bin/python
# coding=utf-8
 
import socket
import string
import time
import re
import sys
 
SERVER = 'irc.karino.org'
PORT = 6667
NICKNAME = 'vimbot'
CHANNEL = '#vim'
 
class IRC(object):
 
    """ init """
    def __init__(self):
        """__init__ documentation"""
        self.SERVER = SERVER
        self.PORT = PORT
 
    def connect(self):
        """__init__ documentation"""
        self.__con = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.__con.connect( (self.SERVER, self.PORT) )
 
    def __send_data(self, command):
        res = self.__con.send(command + '\n')
 
    def __recv_data(self):
        res = self.__con.recv(1024)
        return res
 
    def login(self, nickname, username='hogename', password = None, realname='Hoge Hoge', hostname='host', servername='server'):
        self.__send_data("USER %s %s %s %s" % (username, hostname, servername, realname))
        self.__send_data("NICK " + nickname)
 
    def join(self, channel):
        #self.__send_data("JOIN %s" % channel)
        self.__send_data(u"JOIN " + channel)
 
    def post(self, text):
        self.__send_data((u"PRIVMSG " + CHANNEL + u" :" + text).encode('iso-2022-jp', 'ignore'))
 
    def pong(self, text):
        self.__send_data(u"PONG " + text)
 
    def part(self, channel):
        text = 'Leaving...'
        self.__send_data(u"PART " + channel + u" :" + text)
 
    def quit(self):
        text = 'Leaving...'
        self.__send_data(u"QUIT " + u" :" + text)
 
    def channel_list(self):
        _re_channel_search = re.compile(r'(#.+) \d+ :').search
        def get_channel(line):
            try:
                mo = _re_channel_search(line)
            except IndexError:
                return None
            return mo.group(1) if mo else None
 
        # send
        self.__send_data("LIST")
        buf = ""
        result = []
        flg = 0
 
        # recv
        while 1:
            buf = buf + self.__recv_data()
            tmp = string.split(buf, "\n")
            buf = tmp.pop()
 
            for line in tmp:
                #print line
                line=string.rstrip(line)
 
                if(":End of /LIST" in line):
                    flg = 1
                    break
 
                else:
                    #print line
                    if get_channel(line) != None:
                        result.append( get_channel(line) )
 
            if flg==1:
                break
 
        return result
 
    def recv(self):
 
        _re_get_message_search = re.compile(r'PRIVMSG (#.+ :.+)').search
        def get_message(line):
            try:
                mo = _re_get_message_search(line)
            except IndexError:
                return None
            return mo.group(1) if mo else None
 
        buf = ''
        buf = buf + self.__recv_data()
        tmp = string.split(buf, "\n")
        buf = tmp.pop()
 
        for line in tmp:
            print line
            line=string.rstrip(line)
            return line
 
def main():
 
    _re_get_ping_search = re.compile(r'PING :(.+)').search
    def get_ping(line):
        try:
            mo = _re_get_ping_search(line)
        except IndexError:
            return None
        return mo.group(1) if mo else None
 
    _re_get_message_search = re.compile(r'PRIVMSG (#.+ :.+)').search
    def get_message(line):
        try:
            mo = _re_get_message_search(line)
        except IndexError:
            return None
        return mo.group(1) if mo else None

    # connect irc server / irc channel
    irc = IRC()
 
    #irc.connect(SERVER, PORT)
    irc.connect()
    irc.login(NICKNAME)
 
    # getchannel
    clist = irc.channel_list() 
    print clist
 
    # join all channel
    for l in clist:
        irc.join(l)
 
    # join all channel
    while 1:
        try:
            # recv
            line = irc.recv()
 
            if ("PING" in line):
                line=get_ping(line)
                irc.pong(line)
                continue
 
            elif ("ERROR" in line):
                print "error :" + line
                irc.connect()
                irc.login(NICKNAME)
                for l in clist:
                    irc.join(l)
                continue
 
            elif (CHANNEL in line):
                continue
 
            elif ("PRIVMSG" in line):
                buf = get_message(line)
                if buf.find('vim') >= 0:
                    irc.post(buf)
                continue
 
        except:
            continue
 
if __name__ == '__main__':
    main()

2012年4月3日火曜日

[vim] 今日は、vimからIRCにポストできるように、codepaste.vimを修正した。

この前作ったcodepaste.vimに以下3点の修正をした。

1, ircにポストできるようにした。
2, オプションを補完できるようにした。
3, getではなくpostで投げるようにした。

※ Codepasteは、社内で使っているコード共有サービス
Codepaste.vimは、vimからそのサービスを利用できるようにしたもの。

インストールは以下より。
Codepaste.vim


1, ircにポストできるようにした。

これだけはやろうと思っていた修正。
codepasteに--ircオプションをつけて実行すると、
開いているファイル or 選択している範囲をコードペーストに送ってURLを取得した後、
そのURLをvimrcに設定しているIRCにポストする機能。
socket通信にvimprocを使っているので、それをインストールする必要がある。

Shougo / vimproc

① まず、vimrcに以下の用にIRCサーバとかの情報を設定する。
let g:codepaste_put_url_to_irc_channel_after_post = 1
let g:codepaste_irc_server   = 'irc.klab.org'
let g:codepaste_irc_port = 6667
let g:codepaste_irc_channel  = '#karino'
let g:codepaste_irc_nickname = 'karino-vim'

② 後は以下のように、codepasteを実行するとき、--ircオプションをつける。
:Codepaste --irc
let g:codepaste_put_url_to_irc_channel_after_post = 1を設定していれば、
--ircオプションを付けなくても、ircに投げるようになる。


2, オプションを補完できるようにした。

いちいち、--ircとうつのめんどくさいので、
Tabで補完できるようにしておいた。

やり方は意外と簡単だった。
コマンドを定義するときに-complete=customlistってのを指定して、
オプションのリストを返す関数を指定してやればできた。
command! -nargs=? -complete=customlist,codepaste#complete_source -range=% Codepaste :call codepaste#Codepaste(,,,)

function! codepaste#complete_source(arglead, cmdline, cursorpos)
   return ['--irc','--debug']
endfunction


3, getではなくpostで投げるようにした。

前回まで、codepasteにコードをポストするとき、getのパラメータとして渡していたので、
あまりに長くなると送信できなくなっていた。
ので、postに変えた。

TODO

① socketのthrow
サーバとのコネクションが上手く貼れなかった時の処理が適当なので、
もうちょっと治す。

② IRCのthrow
IRCにポスト出来なかった時の処理が適当なので、
もうちょっと治す。

③ ヘルプを書く
今回はちゃんとへるぷを書きたい。

④ vimprocをもっとつかう。
vimproc使ったらもっといろいろできそうな気がする。

⑤ vimprocを使わない。
すごく便利だし、自分で使う分には全然いいんだけど、
他の人が使うとき、インストールしなきゃいけないものが増えるのは
ちょっとめんどいと感じるやも。
vimproc使わなくても、socket通信できるやり方探す。