2012年11月24日土曜日

[vim] 別のファイル開いた時、Ctrl+tで元のファイルに戻れたら便利だと思う

背景


ソース読むときなり、プログラム書くときなり、タグジャンプはよく使ってるな。というか使わんとやってられん。今のこの環境、結構満足できてるんだけど、ひとつ不満なことがある。それは、別のファイル開いたら、さっきまで開いてたファイルがどれだったか、わかんなくなってしまうこと。そこまで深くないソースなら、それほど開くバッファ多くないから不便は無いが、深いソースになってくるとbuffer内に10個くらい溜まってきて、さっき見てた奴どれかわからなくなる。

うまく定着するかはわからんが、対策を一つ思いついた。今開いてるwindowからバッファが離れるタイミングで、タグスタックにそのバッファを登録しておけば、別のファイル開いてもタグでジャンプしたみたいな感覚で、Ctrl+t押せば元のファイルに戻れるのではないか?。これやってみるか。

しかし、タグスタックに対象を追加する方法が見つからない。そんなコマンドヘルプにも無いし、ぐぐってもいいの出てこない。いやでも絶対やり方あるでしょ、と思って調べ続けるけどない。。仕方ない、力技でいく。

方法は、「BufWinLeaveするときに、どっか適当なファイルにジャンプして、そのあと別のバッファを開く」というやりかた。ジャンプすれば、タグスタックに追加されるってなら、どこでもいいからジャンプしちゃえばいいじゃんってやりかた。・・・絶対、もっと良いやり方あるよな。。でも、できないよりはマシか。。

やってみた感



vimrcには以下の感じで設定する。
"----------------------------------------------------------+
" add currnet file to tagstack
"----------------------------------------------------------+

autocmd BufWinLeave .vimrc call AddTagstack()
autocmd BufWinLeave *.vim call AddTagstack()
autocmd BufWinLeave *.py call AddTagstack()
autocmd BufWinLeave *.php call AddTagstack()
autocmd BufWinLeave *.markdown call AddTagstack()

set tags+=/home/karino/.foraddtagstack
function! AddTagstack()
    execute "tag tag_for_add_tagstack"
endfunction
そして、/home/karino/.foraddtagstackは以下の感じ。
tag_for_add_tagstack ~/.foraddtagstack /^tag_for_add_tagstack/;" f
とりあえず、できるようにはなった。。
なかなか便利だ。
でも、なんていうか。絶対もっといい方法あるはず。。

2012年11月12日月曜日

[git] コンフリクトしたところだけ、vimdiffで分割して表示してほしい

背景


よくコンフリクトする気がする。

そんなとき普通、ファイル開いて、コンフリクトしてるところ検索して、解消する。でも結構めんどくさい。コンフリクトしてる箇所が1行とか2行程度ならいいけど、数十行にわたってコンフリクトしてるのを解消するのは結構しんどい。そんなときはやっぱりvimdiffとかで開いて、コンフリクトを解消したいと思うところ。

調べてみると,こんなやり方が見つかる。
Three-way merging for git using vim
なんかそれっぽいけど、このやり方だと「変更のもとになるファイル」(BASE)と、「LOCALの変更ファイル」(LOCAL)、「REMOTEの変更ファイル」(REMOTE)を一緒に表示しているだけ。つまり、「コンフリクトが発生している状態のファイル」(MERGED)で、コンフリクト箇所調べて、解消するのは変わりない。

やりたいのは、自動でマージできたところはそのままで、コンフリクトしたところだけ分割して、vimdiffで表示すること。ということで書いてみた。


やりかた


基本的なやり方は参考リンクと同じ。mergetool設定して、mergeする時それを呼び出す。違うところは、単純にvimdiffで開くのではなく、MERGEDファイルのコンフリクトしてる箇所を分割したファイルを作り、そのファイルとdiffをとっているところ。

mergetoolはこんな感じのを準備する。
#!/bin/bash

function fixpatch(){
    basefile=${1}
    tmp="tmp.php"
    tmp2="tmp2.php"

    cp $basefile $tmp
    if [ "${2}" = "local" ]; then
        sed "/>>>>>>>/d" $basefile > $tmp
        start_array=($(cat $tmp  | grep -n "<<<<<<<" | cut -d ':' -f 1))
        end_array=($(cat $tmp | grep -n "=======" | cut -d ':' -f 1))
    elif [ "${2}" = "remote"  ]; then
        sed "/<<<<<<</d" $basefile > $tmp
        start_array=($(cat $tmp  | grep -n "=======" | cut -d ':' -f 1))
        end_array=($(cat $tmp | grep -n ">>>>>>>" | cut -d ':' -f 1))
    fi

    cat $tmp > $tmp2

    # コンフリクト箇所を省く.
    for (( i=0; i<${#start_array[@]}; i++ ))
    do
        cp $tmp2 $tmp
        num=$((${#start_array[@]} - ${i} - 1))
        sed -e "${start_array[$num]},${end_array[$num]}d" $tmp > $tmp2
    done

    cat $tmp2
    rm $tmp
    rm $tmp2
    return 0
}

# ブランチ名を取得
BRANCH=$(git branch --no-color | sed -n -e 's/^\* //p')
gitroot="$(git rev-parse --show-toplevel)/"

# mergeに関するファイルを取得
MERGED=${1}
LOCAL=${2}
BASE=${3}
REMOTE=${4}

# MERGEDファイルのコンフリクト箇所を除外
fixedpatch_local="fixedpatch_local.php"
fixedpatch_remote="fixedpatch_remote.php"
fixpatch ${MERGED} "local" > $fixedpatch_local
fixpatch ${MERGED} "remote" > $fixedpatch_remote

echo ${fixedpatch_local}
echo ${fixedpatch_remote}

cp ${fixedpatch_local} ${MERGED}


# vimdiffで開く
vim -f \
    -c 'set nofoldenable' \
    -c 'wincmd p'         \
    -c 'set nofoldenable' \
    -c 'wincmd p'         \
    -c 'noremap J ]c'     \
    -c 'noremap K [c'     \
    -c "set tags+=.tags;${gitroot}"  \
    -d $MERGED $fixedpatch_remote

# ゴミファイルを削除
rm $fixedpatch_local
rm $fixedpatch_remote


そして、mergetoolに設定する。これは、参考リンクとほぼ同じ。
git config --global mergetool.vimdiff3.cmd 'git_merge_wrapper3 "$MERGED" "$LOCAL" "$BASE" "$REMOTE"'
git config --global mergetool.keepBackup false
git config --global merge.tool vimdiff3

使い方


コンフリクトしてるときに、git mergetoolを叩くと、コンフリクトしているファイルを開き、コンフリクトしている箇所を分割して、vimdiffで表示する。


karino@goldfish ~/src/trunk $ git st
# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       modified:   lib/model/PersonGuildMember.php
#
# Unmerged paths:
#   (use "git reset HEAD <file>..." to unstage)
#   (use "git add/rm <file>..." as appropriate to mark resolution)
#
#       both modified:      apps/main/config/template.yml
#       both modified:      apps/main/modules/guild/actions/actions.class.php
#       both modified:      config/schema.yml
#       both modified:      lib/util/GuildUtil.php
#
karino@goldfish ~/src/trunk $ git mergetool 
Merging the files: apps/main/config/template.yml
apps/main/modules/guild/actions/actions.class.php
config/schema.yml
lib/util/GuildUtil.php

Normal merge conflict for 'apps/main/config/template.yml':
  {local}: modified
  {remote}: modified
Hit return to start merge resolution tool (vimdiff3): 

ここでenter押せばvimdiffが表示される。左側がworkingtreeのファイルで、右側がREMOTE側のファイル。diffの時と同じ用に、Shift-j、Shift-kで差分にジャンプできるようにしている。僕の好み。これで、コンフリクトなんか怖くない。


2012年11月10日土曜日

[git] diffはvimdiffで開こうよ

背景


diffはvimdiffで開きたい。


やり方


やりかたは、ほとんど以下のまんま。
vimdiffの開き方だけちょっと変えた。
Git Diff with Vimdiff


① .gitconfigに設定

以下を.gitconfigに書いておけば、
git diffした時、git_diff_wrapperが呼ばれることになる。

[diff]
    external = git_diff_wrapper
[pager]
    diff =

もしくは、以下をコマンドラインで叩いても可。
git config --global diff.external git_diff_wrapper
git config --global pager.diff ""


② git_diff_wrapperを準備


以下を、git_diff_wrapperという名前で、PATH通っているところに置いておく。


変更点1:折りたたまれないようにする。

ただ、普通にvimdiff開くと、どうしても折りたたまれてしまう。
.vimrcにnofoldable書いても効果なし。
無理やり開くとき設定するようにした。


変更点2:タグファイルを読み込む

プロジェクト毎の読み込むtagファイル変更しているが、
その際、開くファイルのパスを見て、どのtagファイルを読むか切り替えている。
でも、vimdiffでtmpファイル開くとtagが読み込まれないため、
tmp側ではジャンプできなくなってしまう。

コレまた無理やりだけど、gitrootの取得して、その下にあるtagファイルを読み込む用にした。
これでtmpで開いてる側でもジャンプできるようになった。


#!/bin/sh

BRANCH=$(git branch --no-color | sed -n -e 's/^\* //p')
gitroot="$(git rev-parse --show-toplevel)/"

vim -c 'set nofoldenable' \
    -c 'wincmd p'         \
    -c 'set nofoldenable' \
    -c 'wincmd p'         \
    -c 'noremap J ]c'     \
    -c 'noremap K [c'     \
    -c "set tags+=.tags;${gitroot}"  \
    -d "${2}" "${5}"

2012年11月3日土曜日

[git] masterブランチにpushできる人を制限したい

背景



今の開発環境を軽く説明。
テストサーバに共有レポジトリがおかれていて、開発メンバはそこから、cloneしてきたローカルレポジトリで開発をおこなう。ブランチの構成は、masterブランチ1本と、機能ブランチが複数本。

開発を始める際、機能ブランチを切ってもらい、そこにガンガンコミットしていく。テスト環境で動作を確認する場合は、その機能ブランチを共有レポジトリにpushすれば、自動でdeployされて、確認できるようになる。

また、進捗状況の確認とかにも使えるかなとも思うので、メンバには定期的に共有レポジトリにpushしてもらっている。もし、masterに何かしらの更新がかかった場合、機能ブランチをrebaseしてもらっている。そして、開発が完了し、動作確認できたら、masterにmergeしていく。


このように、開発メンバはちょくちょく共有レポジトリにpushする。
しかし、gitに慣れていないメンバもいるので、間違って、ぶっ壊れた履歴を、masterにpushしちゃうこともある。

まぁ機能ブランチであれば、もし間違ったコミット達をpushしたとしても、gitなら割と簡単に履歴修正できるし、取り返しのつかない履歴上げられたとしても、最悪、masterからブランチ切り直して、必要なコミットだけ、cherry-pickで持っていく事もできる.

しかし、masterを壊されると結構厄介な事になりそう。
機能ブランチは、適度なタイミング(メンバが気づいた時)、masterの変更を取り込む作業をしてもらっているので、もし壊れたmasterを共有された場合、その影響が各機能ブランチまで波及してしまう可能性がある。

そのため、機能ブランチはいいけど、masterはpushされたくない。ということで、masterへのpushができるユーザを制限したかった。


方法


似たようなことやってるところ。
[Git] Git でサーバに認証したユーザと Author が同じであることを強制するフックスクリプト
やり方としては、pushされた時に走るhookを仕込んで置いて、
masterブランチへのpushの時だけ、
接続してきたユーザが許可されたユーザであればpushを実行、それ以外なら失敗させる。


pushを止めるhookとしては、「pre-receive」と「update」がある模様。
「pre-receive」は、push毎に実行される。
「update」は、pushが更新するリファレンス毎に実行される。
今回やりたいのは、masterブランチへのpushだけ止めて、
その他のブランチへのpushは許可したいので「update」を使ってみる。


ソース


① ユーザの取得

参考にしたところでは、environmentを使って環境変数を設定していたが、
今の僕の仕事上rootを持っていないので、この方法は使えない。
が、他の案件の人が、command使ってやっているのをみて、丸パクりした。

.ssh/authorized_keysには以下。
command="if test -n \"$SSH_ORIGINAL_COMMAND\"; then SSH_CLIENT_USER=karino-t eval $SSH_ORIGINAL_COMMAND; else SSH_CLIENT_USER=karino-t  exec /bin/bash -l ; fi" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCMi+sgejbfTsc8FR2fL+yIP2vbVCcwE5th0cI8HB9NG5NEe6VwH8VfbxVq7I/VOST09Ww+sWm6BgZCewNyJfV9GNsfEOVOC93VEeTDLudKzB2I+21ybvVa1A+/PtFdMY4trOeRMc6HwTbt0uwpVhZLJjGJxISoAoH9clF8I4x85qlXZeOsePoyW55uwHpyoojB+UhIxuzG60+mQutmMwgAQ5OF1tSBqAjB7fKVOievjKq/AZ8hGW3HuR5Yj6XE8RaUPWzu91wEqF/FSXrS0i8nUp2+ZZ2Loapu9xVhFxRASacVBLrwV+BnNhcC5lIV0/lClttVxY2eLCXxeVr4EFx karino@goldfish


② update hook

これで、masterにpushする時のみ止められることになる。
多分問題無いはず。


#!/bin/sh

BRANCH=${0}

echo
echo "SSH_CLIENT_USER : ${SSH_CLIENT_USER}"
echo "target branch   : ${1}"

                                           
PERMIT_USER=("karino-k" "karino-n" "karino-k")
STATUS=1                                   
                                           
# master以外はpush OK
if [ ! `echo ${1} | grep 'master'` ]; then
    STATUS=0
fi

# 特定ユーザはpush OK
for USER in ${PERMIT_USER[@]}
do
    if [ ${SSH_CLIENT_USER} = ${USER} ]; then
        STATUS=0
    fi
done

# エラーメッセージ
if [ $STATUS = 1 ]; then
    echo
    echo "You don't allow to push master"
    echo "Please contact to karino-t"
    echo
fi

exit ${STATUS}

2012年11月2日金曜日

[git] bashでプロンプトにブランチ名表示したら、git branch叩く回数がめっちゃ減った

背景


gitでブランチをいっぱい切って運用してる場合、ブランチの移動を頻繁に行う。
そして、ちゃんと自分が今何処のブランチで作業しているかを把握していないと、
機能ブランチだと思って、コミットしたらmasterブランチだったとかになってしまう。
結果、git branchを叩きまくることになる

と言っても、やっぱりめんどくさい。
そこで、よくやられていることかもだけど、
プロンプトにブランチ名表示した。
めっちゃ便利。これ使う用になってから、git branch叩く回数が極端に減った。
もう、これないと死ぬ。

参考URL
zsh で Git の作業コピーに変更があるかどうかをプロンプトに表示する方法
Gitとプロンプト変数PS1とbash_completionと

ソース


2つ目の参考URLのやり方だと上手く行かなくて、
いろいろ試行錯誤した結果、下の形に落ち着いた.
やらなきゃいけないことは、.bashrcに以下を書き込めば使えるはず。
ちなみに、indexに変更がある場合は赤、
working treeに変更がある場合は黄色、
なんの変更もない場合は青いろになるようになっている。

#!/bin/bash
# プロンプトにブランチ名表示
function parse_git_branch {
  RED="\e[0;31m"
  YELLOW="\e[33m"
  GREEN="\e[0;32m"
  BLUE="\e[0;34m"
  NONE="\e[0m"

  NOW_BRANCH=$(git symbolic-ref HEAD 2> /dev/null | cut -d/ -f3)
  IND_STAT=$(git status --porcelain 2> /dev/null | grep -v "?" | cut -d" " -f1 )
  WTR_STAT=$(git status --porcelain 2> /dev/null | grep -v "?" | cut -d" " -f2 )

  if [ "${IND_STAT}" != "" ]; then
    [[ ${NOW_BRANCH} ]] && echo -e "${RED}[${NOW_BRANCH}]${NONE}"

  elif [ "${WTR_STAT}" != "" ]; then
    [[ ${NOW_BRANCH} ]] && echo -e "${YELLOW}[${NOW_BRANCH}]${NONE}"

  else
    [[ ${NOW_BRANCH} ]] && echo -e "${BLUE}[${NOW_BRANCH}]${NONE}"

  fi

}
export PS1='\u@\h \w $(parse_git_branch)$ '

2012年11月1日木曜日

[git] pushしたらdeployしてほしいよね

背景

pushしたとき、自動でdeployしてくれるhookスクリプトが欲しかった。


post-update hook


git push した際、push先のレポジトリで実行される。
設定してあるhookスクリプトには、リファレンス名が引数として渡ってくる。
同じようなものでpost-receiveフックがあるが、
post-receiveは、更新されるリファレンスの数にかかわらず一回実行。
post-updateは、対象になるリファレンス毎に実行される模様。



ソース


すごく単純。いま使っているデプロイコマンドは引数にブランチ名を取るので、
pushされた時渡ってくる引数から、ブランチ名を取得して、引き渡している。
これをサーバ側レポジトリに、.git/hooks/post-updateという名前で保存して、
実行権限持たせておけば、OK
pushしたら自動でdeployしてくれる。

BRANCH=$(git rev-parse --symbolic --abbrev-ref $1)
echo ${BRANCH}
deploy ${BRANCH}

2012年10月31日水曜日

[git] コミットした時、コミットメッセージにブランチ名つけた方がいいんじゃない?

背景


コミットメッセージにブランチ名をつけてくれるといいかなって思った。

prepare-commit-msg


コミットを実行するとき、呼ばれるhook スクリプト。
引数として、「コミットログメッセージを保存したファイル名」、「ログメッセージの出処」、
「コミットログメッセージを再利用している元のコミットオブジェクト名」の3つが渡される。


ログメッセージの出処には、5つの種類がある。

1,message
git commit コマンドに -m や -Fオプションでメッセージを与えた場合

2,template
git commit コマンドに -t でメッセージを与えた場合

3,merge
git merge コマンドでメッセージを与えた場合

4,squash
git merge --squash コマンドでメッセージを与えた場合

5,commit
git commit コマンドに -c や -Cオプションでメッセージを与えた場合

また、このフックがゼロ以外で終了すると、コミットを作らず終了してしまう。

なんかようわからんから、とりあえず、messageの時だけ、頭にブランチ名付けることにした



ソース


以下を、.git/hooks/したに、prepare-commit-msgという名前で保存しておくと、
コミットメッセージの頭に現在のブランチ名をつけてくれる。

#!/usr/bin/python
# coding: utf-8

import sys
import subprocess

def get_now_branch():

    # コマンドを実行
    proc = subprocess.Popen(['git', 'symbolic-ref', 'HEAD'],
                            #shell=True,
                            #cwd='/',
                            stdin=subprocess.PIPE,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT,
                            close_fds=True)

    # 結果を読む
    line = proc.stdout.readline()
    gitlist = line.rstrip("\n").split('/')
    branch  = gitlist[len(gitlist)-1]

    return branch


def get_commit_msg(file):
    for line in open(file, 'r'):
        return line.rstrip('\n')

def set_commit_msg(file, msg):
    f = open(file, 'w')
    f.write(msg)
    f.close()
    return

def main():


    # 引数取得
    argvs = sys.argv  # コマンドライン引数を格納したリストの取得

    # branch取得
    branch = get_now_branch()
    if branch.find('fatal')>=0:
        return

    # edit fileのパスを取得
    git_commit_editmsg=argvs[1]

    # commit msgにブランチを設定
    if len(argvs) >=3:
        if argvs[2] == 'message':

            # commit msgにブランチを設定
            cm = get_commit_msg(git_commit_editmsg)
            commit_msg =  "["+branch+"] " + cm

            # commmit messageを保存
            set_commit_msg(git_commit_editmsg, commit_msg)

if __name__ == '__main__':
    main()

2012年10月29日月曜日

[git] とりあえず、pre-commit hookは必要でしょ

背景


コミットする時、PHP errorチェックをするいつものやつ。

pre-commit フック


git commitでコミットする時に実行サれるフック。
このスクリプトが0以外で終了すると、コミットが止められる。
引数は特にない・・・?

ソース


こんな感じ。
gitの場合、パスが通っているところにgit-*って名前で保存されて、
実行権限があればgitのhookスクリプトとして認識してくれるみたいなので言語はなんでもいい。

これは、pythonで書いてみた。

地味に長くなったな・・・。


これを、.git/hooks/下に、pre-commitという名前で保存しておけば、
コミットする時エラーチェックをやってくれる。

#!/usr/bin/python
# coding: utf-8

import sys
import os
import subprocess

def get_index_files():

    # コマンドを実行
    proc = subprocess.Popen(['git', 'status', '--porcelain'],
                            #shell=True,
                            #cwd='/',
                            stdin=subprocess.PIPE,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT,
                            close_fds=True)

    # 結果を読む
    files = []
    while True:
        line = proc.stdout.readline()
        if not line:
            break

        # status, fileを分割
        status, file_path =line[:2], line[3:].rstrip('\n')
        if status[:1]=='A' or status[:1]=='M':
            root, ext = os.path.splitext(file_path)

            # not php file
            if ext == ".php":
                files.append( file_path )
                continue

    return files

def chk_php_error(file):

    # php -l
    proc = subprocess.Popen(['php', '-l', file],
                            #shell=True,
                            #cwd='/',
                            stdin=subprocess.PIPE,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT,
                            close_fds=True)

    # 結果を読む
    line = proc.stdout.readline()
    if line.find('No syntax errors') >= 0:
        return ""

    return  line.rstrip('\n')


def main():

    result=0

    # ファイルを取得
    files = get_index_files()

    # php -l 実行
    for f in files:
        error_str = chk_php_error(f)
        if error_str != "":
            print error_str
            result = 1

    # exit
    if result == 1:
        print '!!! git commit Failed !!!'

    sys.exit(result)


if __name__ == '__main__':
    main()





2012年10月28日日曜日

[vim] 末尾の空白を除去する

背景


virtualeditを設定しておくと、文字がないところにもカーソルが移動できるようになる。
好みの問題だけど、僕は移動できたほうが好き。
でもこれをやると、行末に空白がついたコードが増えちゃうとの指摘を受けたので、
保存する時、保存するとき空白を除去する設定をした。
ついでにtabも変えるようにした。


ソース


こんな感じ
最初は、 '%s/ \+$//e' だけだったけど、
それだと、保存するたびにカーソル移動してうざかったので、
カーソル保存して、最後に元の位置に移動してる
なんかあれな感じだけど
autocmd BufWritePost *.vim call DeleteLastSpace()                                                                     
autocmd BufWritePost *.php call DeleteLastSpace()                                                                     
autocmd BufWritePost *.py call DeleteLastSpace()                                                                                                                                   
                                                                                                                      
function! DeleteLastSpace()
    let save_cursor = getpos('.')                                                                                     
    silent exec 'retab'
    silent exec '%s/ \+$//e'                                                                                          
    call setpos(".", save_cursor)                                                                                     
endfunction


書いた後調べてみると、全く同じコトしてる人いた。
ちゃんと調べてから書こう。。
vimを使って保存時に楽をする

[git] gitの便利な設定をチームで共有するためには。

背景


最近、会社で使うバージョン管理ツールをbzrからgitに変えてみた。

慣れてしまうとすごくいろんな事できて楽しい。
ただ、慣れるまではたいへんだから、新しいメンバが入ってきたときは、すごくたいへんそう。
少なくとも、便利なコマンドとかは、チーム内で共有できるようにしておきたい。

 「gitコマンド」、「alias」、「hook script」の3点は必須。
かつ、後々変更があった時も、簡単に取り込めるようにしたい。

git コマンド


git コマンドは簡単に共有できるようにしておきたい。

経験的に、何か作っても、「これインストールすれば使えます」じゃ、誰も使わない。
「このコマンド叩いてください」ってレベルにしておかないと。

git は、パスの通ったところにgit-*って名前で保存しておけば、
勝手にgitのコマンドとして認識してくれるようなので、
作成されたコマンドをプロジェクトのレポジトリに混ぜることにした。

 新しいプロジェクトメンバーが入ってきたら、
.bashrcに、そのディレクトリへのパスを通してもらえれば、
後々、あたらしくコマンドが追加されてもpullすればOK。

共有用ディレクトリには、作ったgitコマンドを入れておく。
$ ls -l (共有用ディレクトリへのパス)
git-scp 
git-test
git-getbranch

.bashrcに以下を書く。
export PATH=$PATH:(共有用ディレクトリへのパス)

alias


aliasも共有にしたい。

 logの表示形式とか、diffをvimdiffで開くとか。

でも、なかなかうまい方法がなかった。

① .git/configを共有
cloneではここ共有されないらしい。
そして、pullしても共有されない。

② ~/.gitconfigを共有にする。
全部共有にすると、こみったーが全員一緒に・・・。

③ [include]を使う
1.7.10から、別のファイルを読み込めるようになったらしい。
これが使えれば、gitコマンドのときみたいに、
共有にしたい設定だけレポジトリの中入れといて、 新しいプロジェクトメンバーが入ってきたら、 .gitconfigに、名前の設定とincludeを書いてもらえればOK。 

やっぱりこの方法が一番スマートだと思う。
.gitconfigを別ファイルで読み込む gitインストール

が、みんなが入れてるgitのversion古くて使えない。。


④ setupコマンドを準備

結局、setupというコマンドを準備して、やってもらいたいalias全部書き込んでおくことにした。
何か変更があったら、pullして、setupを実行してもらう。
まぁまぁこれくらいならやってくれるだろう。 
以下を、(共有用ディレクトリへのパス)に、git-setupというん名前で、
実行権限つけて保存しておいた。


#!/bin/sh

# git rootの取得
gitroot=$(git rev-parse --show-toplevel)

# alias の設定
echo "+ setting alias"
git config --global diff.external git_diff_wrapper
git config --global pager.diff ""
git config --global diff.tool vimdiff
git config --global difftool.prompt false

git config --global alias.files "diff --name-only"
git config --global alias.hist "log --pretty=format:\"%h %C(yellow)%ad%C(reset) | %s%C(red)%d%C(reset) %C(blue)[%an]%C(reset)\" --graph --date=iso"

git config --global alias.ch checkout
git config --global alias.co commit
git config --global alias.st status
git config --global alias.br branch
git config --global alias.pull "pull --rebase"
git config --global alias.sb "show-branch --color"
git config --global core.pager less


# hookの設定
echo "+ setting hooks"
ln -sf ${gitroot}/data/gittool/pre-commit ${gitroot}/.git/hooks/pre-commit
ln -sf ${gitroot}/data/gittool/prepare-commit-msg ${gitroot}/.git/hooks/prepare-commit-msg

hook script


hookスクリプトも共有にしたい。

commitのときエラーチェック入れるとか、
commitメッセージを決めるとか、
master push するときIRCに投げるとか。 

1,2と同じディレクトリにスクリプト置いといて、 2のsetupコマンドに書いておいた。

2012年10月27日土曜日

[vim] PHPのエラー箇所をハイライトする

背景


前に、PHPのエラーをステータスラインに表示する設定をした。
[vim] phpファイルを保存するとき、エラーチェックして、エラーがあったらステータスラインを赤にする設定

 この状態で使ってて思った。
 ステータスラインに表示されても気づかない。。 

修正して、動かして見たらPHP Fatal Errorはかれてて、
ソースに戻って見たら、ステータスライン赤くなってる・・・。

無意味。

やっぱり、画面の下じゃなくて、画面の真ん中あたらを赤くしてくれないと気が付かない。
なんとかしなきゃ。


対処方法


① エラー時にsyntaxをoffにする。
やってみたけど、そんなにインパクトなかった。

② エラー箇所をハイライトする。
まぁ、こっちでしょう。 
errormarker.vimってのでもできるらしいが、 そんなに長く無いし、自分で書く。

phpでもerrormarker.vimをつかってflymakeしたい!

ソース


機能としては以下2点。
① エラー箇所をハイライト

ハイライトする。

② エラー箇所にジャンプ
spaceキーおしたら、そのエラー箇所にジャンプするようにしみてた。

またこれに伴って、エラー時のステータスラインに
ファイル名とか行番号とか表示指定してたのを外して、
エラーの内容のみを表示させるようにした。
縦分割するとき長すぎて全部入っていなかったので。




"----------------------------------------------------------+
" エラー行の背景色変更
"----------------------------------------------------------+

" キーバインドとautocmd設定
nnoremap  :call JumpToPHPError()
autocmd BufEnter,BufWritePost *.php call CheckErrorLine()

" エラー箇所にジャンプ
function! JumpToPHPError()
  silent! exe 'sign jump 99999 buffer='.winbufnr(0)
endfunction

" エラー箇所をハイライト
function! CheckErrorLine()

  " とりあえず消す.
  silent! exe 'sign unplace 99999 buffer=' . winbufnr(0)

  " チェックを実行(PHP)
  let l:tmp = system("php -l ".bufname(""))

  " エラーがあった場合
  if ! (l:tmp =~ "No syntax errors" )
      let s:line = matchstr(l:tmp,'on line \d\+')
      let l:line = split(s:line, 'line ')

     silent exe 'sign define PHPERROR text=>> linehl=ECOLOR texthl=ECOLOR'
     silent exe 'sign place 99999 line=' . l:line[1] . ' name=PHPERROR buffer=' . winbufnr(0)
     silent exe 'hi ECOLOR ctermfg=Yellow ctermbg=Red cterm=none'

     return
  endif

  return

2012年7月29日日曜日

[vim] monday.vimを修正して、datetime形式のインクリメントができるようになった

久々にブログかく。

背景


monday.vimというものがあることを知った。
monday.vim : Ctrl-a、Ctrl-xで曜日、月をループ (+他の用途への応用) 

ctrl+a / ctrl+x で数字だけでなく、曜日や日付をインクリメントできるようにするプラグイン。
これを使えば、ctrl+aで、monday → tuesday → wednesdayと変更していくことができる。
さらにちょっと修正すれば、true → false → true みたいなこともできる。
よくよく見てみると、Vim Advent Calenderでも紹介されているではないか。読んでたけど、スルーしてたっぽい・・・。
monday.vimカスタマイズしてみた


で、ちょっと思った。

 「2012/07/12 23:12:00」 みたいな形式で書いてあるものを、数字としてでなく、
日付や時間としてインクリメントできたら面白いかもと。

やったこと


① インクリメント要素を配列で受け渡す。
Advent Calenderでもやってたけど、すっきりするなと思いパくる。 

② datetimeのインクリメント
カーソル下に「2012/07/12」とか「23:12:00」とかの形があった場合、
それをdatetime型に置き換え、日付 or 秒数を+1/-1して、置き換える。
datetimeを使うために、初めてvital.vimを使った。
Vital.DateTime 書いた

 できたものはここ。 kokukuma/monday

使い方


① 「2012/07/12」とか「23:12:00」とかの上にカーソルを持っていく。
② ctrl+a を押すと「2012/07/13」とか「23:12:01」になる
③ ctrl+x を押すと「2012/07/11」とか「23:11:59」になる

みたいな感じで ctrl+a を押し続けると、日付や時間としてインクリメントされていく。

ToDo


① 日付の対応フォーマットを増やす
今、「2012/07/12」と「23:12:00」以外のフォーマットではできない。。
つまり、「2012/7/12」とか「2012-7-2」とかには対応してない。 対応できるようにする。

② もっと面白い使い方考える。
たぶん日々使っていく中で、同じようなやり方でもっと面白いこと思いつきそうな気がする。


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通信できるやり方探す。

2012年3月26日月曜日

[vim] 今日はvim pluginを作ってみようと思って、社内で使うためのcodepaste.vimってのを作った。

背景


社内で使っているソース共有サービス?Codepasteというのがある。
Codepaste.netとかGistとやってることは全く同じ。
それを社内用に使えるようになっている。
使い方も同じで、ブラウザのフォームから、
コードをポストして、URLを共有する感じ。

コピペするのがめんどいからvimで選択したものを、
Codepasteにポストするプラグインをつくろうと思った。

やったこと


似たようなの探してみると、あった。

Gistを便利に扱えるvimscript、「Gist.vim」書いた。

やりたいと思っていたこと、そのまんまだw.
まぁ、1から自分でvim plugin書いたことなかったし、
このGist.vimを参考にしながら、
社内のCodepasteポスト用のプラグインを書いてみた。

できたもの


ソースは以下。

kokukuma/codepaste-vim

Gist.vimの場合、ポストしたものを編集したり、
今までポストしたものの一覧を表示したりといろいろやっていたが、
社内で使っているものに関して、そんなに機能必要ないしポストするだけにした。

使い方①
ファイルを開いた状態で
:Codepaste
とすると、現在開いているファイルの内容が
Codepasteにポストされ、URLが表示される。

使い方②
ビジュアルモードで選択した状態で、
:Codepaste
とすると、選択した範囲が、Codepasteにポストされ、
URLが表示される。


できてみると、Gist.vimの劣化コピーみたいになってしまった。
ま、次がんばろう次。


ToDo

○ データ量が多くなると送れなくなる。。
後で調査する。

① IRCへポスト
どうせURLコピーした後IRCに貼り付けるんだから、
コマンド叩いたらIRCにポストするまで勝手にやってもらいたい。

② ヘルプ
今まで書いたことなかったが、今回はちゃんとしたヘルプ書こうと思う。

③ clipboardに保存
プラグインの実装としては簡単そうだが、
そもそもVM上のlinuxとホストのWindows間でクリップボード共有してないから、
まずそっちをやらないと。。

2012年3月20日火曜日

[vim] unite.vimのfilter周りがちょっとわかった気がする。

bzr-logの複数リビジョン対応のときに、unite.vimのfilter周りを調べた。
今回の修正で、bzr-logでマークした複数のリビジョンに対して、
actionであるbzr-deltaを実行できるようになった。

今まで、uniteのデフォ機能で、マークしたものに対して一度にアクションを実行することはできたが、
それでは、選択した複数のリビジョンで同じファイルを修正していた場合、実行したbzr-deltaの結果として、
同じファイルができてしまう。

今回filter(converter)を追加して、candidatesの中に同じファイルパスを指すものがあった場合、
それを1つにまとめ、bzr-deltaのアクション(vimdiff)の引数を修正するようにした。
通常のfilterはsource毎に呼び出されるようだったので、
sourceに設定するのではなくuniteバッファに設定するunite#set_profileを使ってfilterを設定した。
これなら、すべての候補が出力されてからfilterが実行されるので、
かぶってる候補をまとめることができた。

unite.vimでできることが広がった気がする。

kokukuma / vim-unite-bzr

[vim] NERDTreeのメニューにbzr-log をつける。

nerdtreeのメニューにbzr-logメニューを付けた。
これを実行すると、ファイルを指定してbzr-logを実行する。

uniteのソース実行するときに引数渡すやり方がわからなかったので、
もう無理やり。global変数定義して引き渡した。
コマンドもハードコーディングだし、なんかもう、ひどい。

https://gist.github.com/2133012

とりあえず、~/.vim/nerdtree_plugin/ におけば動くはず。
あと、unite-bzr入れれば。

kokukuma / vim-unite-bzr

2012年3月4日日曜日

[vim] NERDTreeのメニューにvimdiffオプションをつける。

diffとりやすくなるかなって思って、
NERDTreeのメニューにvimdiffをつけてみた。

下記、スクリプトを、「nerdtree/nerdtree_plugin/diff_menu.vim」に保存すれば、使えるようになると思う。
NERDTreeでファイル選択中に、「m」でメニューを開くと、
vimdiffが追加 されている。
実行すると、現在開いているファイルと、選択したファイルのdiffをとる。



" ============================================================================
" File:        vimdiff_menu.vim
"
" ============================================================================
if exists("g:loaded_nerdtree_vimdiff_menu")
    finish
endif
let g:loaded_nerdtree_vimdiff_menu = 1
    
call NERDTreeAddMenuItem({'text': '(v)imdiff with current node', 'shortcut': 'v', 'callback': 'NERDTreeDiffNode'})

" FUNCTION: NERDTreeDiffNode() 
function! NERDTreeDiffNode()
    let currentNode = g:NERDTreeFileNode.GetSelected()
    let confirmed = 1

    if currentNode.path.isDirectory 
        let choice =input("Diff with the current node\n" .
                         \ "==========================================================\n" .
                         \ "STOP! Cannot diff with directory\n" .
                         \ "" . currentNode.path.str() . ": ")
        let confirmed = 0
    endif

    if confirmed
        try
            execute "wincmd p"
            execute "vertical diffsplit " currentNode.path.str()
        catch /^NERDTree/
            call s:echoWarning("Could not diff") 
        endtry
    else
        call s:echo("diff aborted")     
    endif

endfunction

[vim] NERDTreeをもっと使いやすくするための設定

背景

NERDTreeは便利だけど、ファイル編集中はじゃま。
なので、NERDTree実行用のスクリプトを書いてみた。
結構便利だ。前にやった「引数なしでvimを実行したときNERDTreeを実行する」設定も修正した。

この辺のアイディアを参考にさせていただいた。
NERDTreeのウィンドウを必要なときだけ見せる。

機能

主な機能はこんな感じ。
① 「Ctrl+e」で、NERDTreeを実行する。
② もう一度「Ctrl+e」で、NERDTreeの表示/非表示を繰り返す。

以下を、vimrcに書き込むと使える。

" 引数なしで実行したとき、NERDTreeを実行する
let file_name = expand("%:p")
if has('vim_starting') &&  file_name == ""
    autocmd VimEnter * call ExecuteNERDTree()
endif

" カーソルが外れているときは自動的にnerdtreeを隠す
function! ExecuteNERDTree()
    "b:nerdstatus = 1 : NERDTree 表示中
    "b:nerdstatus = 2 : NERDTree 非表示中

    if !exists('g:nerdstatus')
        execute 'NERDTree ./'
        let g:windowWidth = winwidth(winnr())
        let g:nerdtreebuf = bufnr('')
        let g:nerdstatus = 1 

    elseif g:nerdstatus == 1 
        execute 'wincmd t'
        execute 'vertical resize' 0 
        execute 'wincmd p'
        let g:nerdstatus = 2 
    elseif g:nerdstatus == 2 
        execute 'wincmd t'
        execute 'vertical resize' g:windowWidth
        let g:nerdstatus = 1 

    endif
endfunction
noremap  ::call ExecuteNERDTree()

2012年2月25日土曜日

[vim] phpファイルを保存するとき、エラーチェックして、エラーがあったらステータスラインを赤にする設定

やったこと


最近、ステータスラインに今カーソルがどの関数にいるかを表示する方法とか、
PHPのエラーチェックやる方法とかを、会社の先輩に教えてもらった。
せっかくなので、ステータスラインの整理をした。
なかなかうまくいった。
特に、保存した時PHPにエラーがあったら、ステータスラインにエラーの内容表示するところとか。

できたもの


ステータスラインのフォーマットは
「ノーマルモード時」、「インサートモード」「PHPエラー検出時」の3パターン。

① ノーマルモード
「背景色を青」、「文字色を白」に変更する。
表示する内容は、「ファイルの絶対パス」、「現在カーソルがいる関数名」、「行数」。


② インサートモード
表示する内容は、インサートモードと同じ。
「背景色を黄色」、「文字色を黒」に変更する。



③ PHPエラー検出時(ファイルタイプがphpのときのみ)
一番やりたかったのはこれ。
ファイルを保存した時、php -lを実行して、
エラーがあった場合、「背景色を赤」、「文字色を黄色」、
「エラーの内容をステータスラインに表示」する。
エラーがない場合は、ノーマルモードと同じ。



設定


.vimrcに設定した内容は以下の通り。

"----------------------------------------------------------+
"  ステータスライン                                        |
"----------------------------------------------------------+

" ステータスラインを常に表示,色変更
set laststatus=2
hi StatusLine ctermfg=White ctermbg=Blue cterm=none
au InsertEnter * hi StatusLine ctermfg=Black ctermbg=yellow cterm=none
au InsertLeave * hi StatusLine ctermfg=White ctermbg=Blue cterm=none
set statusline=%F:\ \ %{GetFunc()}%=%l/%L%11p%%

autocmd BufEnter,BufWritePost * call ErrorCheckStatusline()

" function行の取得用の関数
function! GetFunc()
    let s:path = expand('%:p')
    let s:ext  = expand('%:e')
    if s:ext == "php"
        let start = search("function",'bn')
    elseif &filetype == "vim"
        let start = search("function!",'bn')
    elseif &filetype == "py"
        let start = search("def",'bn')
    else
        let start = ""
    endif

    let lines = getline(start)
    return lines
endfunction

" エラー時のステータスライン変更用関数
function! ErrorCheckStatusline()

  " エラー時の色定義
  let l:ecol = 'highlight StatusLine ctermfg=Yellow ctermbg=Red cterm=none'
  "
  if ! exists('g:is_error')
      " 通常時の色定義
      let s:fg = synIDattr( hlID( "StatusLine" ) , "fg" )
      let s:bg = synIDattr( hlID( "StatusLine" ) , "bg" )
      let g:ncol = 'highlight StatusLine ctermfg='.s:fg.' ctermbg='.s:bg.' cterm=none'
  endif

  " チェックを実行(PHP)
  if &filetype == "php"
    let l:tmp = system("php -l ".bufname(""))

    " エラーがあった場合
    if ! (l:tmp =~ "No syntax errors" )
       let g:is_error = 1
       let g:status = split(l:tmp, '\n')
       silent exec 'set statusline=%{g:status[0]}%=%c,%l%11p%%'
       silent exec l:ecol
       return
    else
       let g:is_error = 0
    endif


  " チェックを実行(python)
  elseif &filetype == "python"
    let l:tmp = system("pyflakes ".bufname(""))

    " エラーがあった場合
    if ! (l:tmp == "" )
       let g:is_error = 1
       let g:status = split(l:tmp, '\n')
       silent exec 'set statusline=%{g:status[0]}%=%c,%l%11p%%'
       silent exec l:ecol
       return
    else
       let g:is_error = 0
    endif

  endif

  " 通常のステータスラインを表示
  silent exec 'set statusline=%F,\ \ %{GetFunc()}%=%l/%L%11p%%'
  silent exec g:ncol
  return
endfunction


今後

① もう少し関数を汎用的にする。
② インサートモードでは、そのモードで必要な情報をステータスラインに表示したい。それが何かは検討中。
③ ビジュアルモードでも何らかの変更をやりたい。
④ vimscript, python のエラーチェックもいれたい。


設定

ここ参考になった。
Vim-Tips
[Vim] statusline

2012年2月20日月曜日

[vim] unite.vimを使って、バージョン管理ツールbazaarをvimから操作する。

bzrをvimから操作するuniteソースを書いた。
探してみると、svnで似たようなことしてる人がいた。
vimスクリプトのuniteでsvn statusとsvn diffするsource書いた
しかし、差分の確認にvimdiffを使いたいという気持ちがあったので、
ゼロから書いてみることにした。

背景


bzrを操作するとき、ほぼ決まりきったコマンドの流れがある。
特に、他の人の変更を確認するとき。

① bzr log --line --forward
② bzr diff -c [リビジョン番号]
③ bzr diff -c [リビジョン番号] [ファイル名]
④ vi [ファイル名]

他の人のコミットした内容を確認したいとき、
まず、bzr logでリビジョン番号を特定し、
bzr diffで変更されたファイルや、その変更内容を確認する。
そのあと、viで開いたりする。

これらの操作をするとき、
一回一回コマンドたたくのは慣れれば特に何も感じないけど、
この流れに沿った操作ができるインターフェースがあれば、
もっと楽になるんじゃないかと考えた。
で、uniteのソースにしようと思った。

ソース

結構長くなってしまったので、githubに置く。

kokukuma / vim-unite-bzr

以下、インストール方法。
ちなみにこれを使うには、unite.vim, vimprocが必要。
また、このインストールは、pathogen.vimを使ったやり方。

$ cd ~/.vim/bundle
$ git clone git://github.com/kokukuma/vim-unite-bzr.git

これだけ。

使い方


主な使い方は2つ。
過去のコミット内容の確認と現在修正した内容の確認。

① 過去のコミット内容を確認

1, Unite bzr_logを実行すると、bzr logを実行した結果が表示される。

2, リビジョンを選択すると、コミットされたファイル一覧が表示される。

3, ファイルを選択すると、vimdiffが起動し、差分を表示する。


② 現在修正した内容の確認

1, Unite bzr_statusを実行すると、修正されたファイルの一覧が表示される。
2, ファイルを選択すると、vimdiffが起動し、差分を表示する。
雰囲気は①と同じ。

なかなか良い感じに仕上がった気がする。


ToDo

① 複数のリビジョンを選択して、修正箇所を確認できるようにしたい。
② commit、mergeは現状、コマンドラインからやったほうが楽だけど、いずれ統合したい。

2012年2月19日日曜日

[vim] 次のfunctionへジャンプするための設定

 背景


ファイル内の特定のfunctionを探すのは地味に大変。
探すために、functionの一覧を表示したり、これかなってfunctionを読んだりするが、
1つのファイルに大量のfunctionが入っているときはめんどい。

functionの一覧を表示する方法として、フォールディングがよくつかわれるが、
個人的にフォールディングは好きじゃない。
展開するのめんどくさいし、ざっと見たいときとかはfunctionしか見えないことがやだ。

そのため今は、フォールディングの設定を使わず、
unite.vimでそのファイルにあるfunctionの行を抽出している。
[vim] 地味に便利。関数を抽出するunite sourceを作った。

ざっとfunctionを読むときは、何も考えず、j/kで移動してみてた。
しかし、長いfuncitonがいっぱい入っているファイルはきつい。
そこで、function間をジャンプするための設定を思いついた。


設定

といっても、以前やったフォールディングの設定を少し変えて流用してるだけ。
[vim][python] ふははは!読める!読めるぞ!

以下をvimrcに書き込む。
" フォールディングはしない。
set nofoldenable

" 折りたたみレベルを設定する関数
function! PHPFoldSetting(lnum)
    let l:line = getline(a:lnum)    
    if l:line =~ 'function'        
        return '>1'    
    elseif getline(a:lnum + 1) =~ ' \* '        
        return 0    
    elseif getline(a:lnum) =~ ' \* '        
        return 0    
    elseif getline(a:lnum + 1) =~ '^\[\d\{4}-\d\{2}-\d\{2} \d\{2}:\d\{2}\]'        
        return 0    
    else        
        return '='
    endif
endfunction

" PHPファイルの場合に設定 
autocmd BufEnter *.php set foldmethod=expr foldexpr=PHPFoldSetting(v:lnum)

" キーバインド
noremap J zj
noremap K zk


こうしておけば、通常ファイルを開いた場合フォールディングはされず、
shift + j / shift + k で、function間をジャンプすることができる。

2012年1月29日日曜日

[python] 修正内容をscpで転送するbzrコマンド。

背景

テスト機でプログラムの動作を確認したいとき、
trunkブランチにコミット、deployしてテスト機にソースを送る。
しかしバグがあった場合、再度コミットするため、
余計なリビジョンが増えてしまって気持ち悪い。

そのため、修正したソースをscpとかで送り、テスト機で確認するってことをよくやる。
その時修正したものいちいちbzr statusで探して、scpコマンド打つのとかマジでめんどい。

そこで、追加・修正したファイルを特定のサーバへ転送するためのbzrコマンドを作った。

探したらあった。


探してみると、やりたいこととまったく同じことしてる人がいた。

bzr scp TARGET で作業ファイルを開発サーバに転送するプラグインを作りました

でも作った。


まぁ、とりあえず作ってみる。
再発明の感は否めないけど。

import os
import subprocess
from urlparse import urlparse
from bzrlib.branch import Branch
from bzrlib.workingtree import WorkingTree
from bzrlib.commands import Command,register_command

version_info = (0, 0, 1, 'dev', 1)
__author__ = 'karino'
__date__   = '2012/01/23'

# serve list
server_list = [\
        ["m-tst","karino","karino@kokukuma.net:/home/karino/program/1"],\
        ["x-tst","karino","karino@kokukuma.net:/home/karino/program/2"],\
        ["g-tst","karino","karino@kokukuma.net:/home/karino/program/3"],\
]

class cmd_deploy(Command):
    local_branch_path = ""

    def run(self):

        # get changes
        print
        changes = self.get_changes()
        if changes==[]:
            print "There is no change."
            return
        else:
            print "--- change files ---"
            for filepath in changes:
                print filepath

        # select server
        print
        print "--- select server ---"
        snum = self.select_server()

        # confirm
        print
        print "--- dryrun ---"
        self.scp(changes, snum, True)
        ok = self.user_input("yes/no > ")

        # scp
        if ok:
            print
            print "--- execute ---"
            self.scp(changes, snum, False)
        return


    def scp(self, changes, snum, dryrun):
        for filepath in changes:

            # 
            local_path  = self.local_branch_path + filepath
            server_path = server_list[snum][2]+filepath

            # 
            if dryrun:
                print "scp -Cpr " + local_path + " " + server_path
            else:
                if subprocess.call(["scp","-Cpr", local_path, server_path]):
                    #print "scp -Cpr "+filepath+" "+path+"  ... failed"
                    pass
                else:
                    #print "scp -Cpr "+filepath+" "+path+"  ... success"
                    pass
        return


    def select_server(self):
        for i in range(len(server_list)):
            print i, ":",server_list[i][0]
        snum = raw_input("Which Server > ")
        if snum.isdigit() and int(snum) in range(len(server_list)):
            return int(snum)
        else:
            return 2


    def user_input(self, prompt):
        ok = raw_input(prompt)
        if ok in ['y','yes']: return True
        return False


    def get_changes(self):
        # get workingtree
        wt = WorkingTree.open_containing(os.getcwd())[0]
        br = Branch.open_containing(os.getcwd())[0]
        print urlparse(br.base).path
        self.local_branch_path = urlparse(br.base).path

        # get changes
        changes = wt.changes_from(wt.basis_tree(),want_unversioned=True)
        result = []
        for add in changes.added:
            result.append(add[0].encode('utf-8'))
        for mod in changes.modified:
            result.append(mod[0].encode('utf-8'))
        return result

register_command(cmd_deploy)

使い方と結果

① 例によって、~/.bazaar/plugins/にファイルを置く。

② bzr deployって打つと、追加・修正されたファイル
(bzr status、addとmodifiedが対象)を表示。

③ 送り先サーバを選ぶと、実行されるscpコマンドが表示され、
yesを打つと、そのコマンドが実行される。

[karino@localhost release]$ bzr deploy
"Key 'makuocmd' already registered"
Unable to load plugin 'makuocmd2' from '/home/karino/.bazaar/plugins'

/home/karino/bzr_test/release/
--- change files ---
123.php
k123.php
setting.yml

--- select server ---
0 : m-tst
1 : i-tst
2 : g-tst
Which Server > 0

--- dryrun ---
scp -Cpr /home/karino/bzr_test/release/123.php karino@kokukuma.net:/home/karino/program/1/123.php
scp -Cpr /home/karino/bzr_test/release/k123.php karino@kokukuma.net:/home/karino/program/1/k123.php
scp -Cpr /home/karino/bzr_test/release/setting.yml karino@kokukuma.net:/home/karino/program/1/setting.yml
yes/no > y

--- execute ---
123.php                             100%
k123.php                            100%
setting.yml                         100%
[karino@localhost release]$ 

[python] bazaarのコミット数をランキングするコマンドを作った。

朝会社に来て、bzr logすると、昨日までなかったコミットが
いっぱい増えていることがある。
そんな時ふと思った。この人一体何件コミットしてるんだろうと。

で、ブランチコミット数のランキングを出力するbzrコマンドを作ってみた。


ソース

#!/usr/bin/python
# coding=utf-8

import os
from bzrlib.branch import Branch
from bzrlib.commands import Command,register_command

version_info = (0, 0, 1, 'dev', 1)
__author__ = 'karino'
__date__   = '2012/01/23'

class cmd_rank(Command):

    def run(self):

        # ブランチオープン
        branch = Branch.open_containing(os.getcwd())[0]
        print branch
        history = branch.revision_history()

        # コミッター毎のコミット数を集計
        ranking = {}
        for log in history:
            committer  = log.split('-')
            #print committer
            if committer[0] in ranking:
                ranking[committer[0]] = ranking[committer[0]]+1
            else:
                ranking[committer[0]] = 1

        commitsum = sum(ranking.values())
        print
        print "%25s %10s %10s"  % ('committer', 'commited', 'rate')
        for k,v in reversed(sorted(ranking.items(), key=lambda x:x[1])):
            rate = v * 100/float(commitsum)
            print "%25s %10s %10s %%"  % (k, v, round(rate,2))

        # 
        print
        return

register_command(cmd_rank)

使い方と結果

例によって、~/.bazaar/plugins/にソースファイルを放り込めば使えるようになります。
使い方は、下記のような感じ。

karino@goldfish:~/bazzar/dev$ bzr rank
BzrBranch7('file:///home/karino//bazzar/dev/')

                committer   commited       rate
                   karubo        863      17.39 %
                   takeda        830      16.73 %
                   maniko        791      15.94 %
                  uemitsu        736      14.83 %
                     sato        706      14.23 %
                 morimomo        263        5.3 %
                    inoda        215       4.33 %
                tatsuhira        183       3.69 %
                   karino         72       1.45 %
                   funami         52       1.05 %
                    nitto         49       0.99 %
                    maito         42       0.85 %
                 higurasu         16       0.32 %
                     mori         16       0.32 %
                matsuhasu          9       0.18 %
                    osuga          3       0.06 %
                    kishi          3       0.06 %
                   tokuma          2       0.04 %
                 sakimoto          1       0.02 %
                      oho          1       0.02 %
最大コミット数863!。
自分の10倍以上だw。

参考になったサイト


bzrlib: bazaarをPythonから操作する

Bazaarの中央ブランチへのpushをHudsonのビルドトリガにしたい

2012年1月23日月曜日

[vim] mysql接続先をvimから変更するためのunite source

vimからテーブル操作をするためのプラグインdbextを使っているが、
接続するDBがサーバごとコロコロ変えなきゃいけないのがすごくめんどくさい。
DBSetOptionを変えるのも、sshポートフォワーディングの設定をするのも
かなりめんどくさい。

そこで、それらをuniteから一括して設定するためのsourceを書いた。

機能


① 一覧からDBを選択し、dbextのDB接続先やユーザ名パスワードを変更する。
② ローカルでない場合、sshポートフォワーディングの設定をする。
③ その際、すでにsshポートフォワーディングの設定がある場合は、
そのプロセスを停止して、新しいプロセスを立ち上げる。

ソースとインストールと使い方


☆ ソース
kokukuma / vim-unite-mysource
この中にある、mysql_con.vimってのがそれ。

☆ インストール
① gitから落として、.vim/内に配置
② vimprocを使っているので、それもインストールする。
③ .vimrcにmysqlの設定と、ポートフォワードに必要な情報を、以下のように書く。

let g:mysql_con_list = [
    \["0", "local", "root", "password", "dbkokukuma"],
    \["1", "remote1", "kokukuma", "password", "dbkokukuma_tst"],
    \["1", "remote1", "kokukuma", "password", "dbkokukuma_dev"],
    \["2", "remote2", "kokukuma", "password", "dbkokukuma_stg"],
\]
let g:port_forward = [
    \["kokukuma",  "kokukuma.dev.net"  ,"mysqlserver","/home/karino/.ssh/id_rsa" ],
    \["kokukuma",  "kokukuma.net"  ,"mysqlserver","/home/karino/.ssh/id_rsa" ],
\]

g:mysql_con_listの形式は、
[sshポートフォワード設定番号],[unite表示名],[DB接続ユーザ名],[DB接続パスワード],[接続先DB名]
sshポートフォワード設定番号は、0のときlocalhost、1以降は、g:port_forwardに対応する。

g:port_forwardの形式は、
[ssh接続ユーザ名],[sshサーバ],[DBサーバ],[秘密鍵パス]
DBサーバ名は、sshサーバから見たときの名前にする。


☆ 使い方

① vimを起動して、「:Unite mysql_con」と入力すると、以下のように、
設定した接続先mysql一覧が表示される。

② これを選択すると、dbextの設定、sshポートフォワーディングの設定が切り替わる。




引っかかった点


① sshポートフォワード
以下のように接続する環境。
ローカルホスト → sshサーバ → DBサーバ
sshサーバ :kokukuma.net
DBサーバ :mysqlserver

ssh -N -L 13306:mysqlserver:3306 karino@kokukuma.net -i /home/karino/.ssh/id_rsa
13306 : ポートフォワードに使用するローカルの任意ポート
mysqlserver : sshサーバから見たDBサーバ名
3306 : DBサーバの接続先ポート


ここらへんを参考にした。
SSH ポートフォワードで MySQL サーバにログインするときのメモ
SSHでポートフォワード


② sshポートフォワードのプロセス取得
いろいろ迷走した挙句、こんな形になるが、要検討。
ps -ax > ~/.tmpfile
cat ~/.tmpfile|grep 'ssh -N -L' && rm ~/.tmpfile
rm ~/.tmpfile

③ vimからbashコマンドを実行し、その戻値を取得

vimprocってプラグインを使わないと難しいらしい。

Shougo / vimproc
コンパイルが必要なので要注意。
cd ~/.vim/bundle/vimproc
make -f make_gcc.mak

2012年1月15日日曜日

[vim] source explorerを入れてみたときのメモ

vimをIDEっぽくするためのプラグイン。
Vimでソースコードを素早く追いかける


インストール


NERDTree.vim, srcexpl,vim, taglist.vimの3つをtrinity.vimが統合して動作する。
vim-scripts / trinity.vim
vim-scripts / Source-Explorer-srcexpl.vim
vim-scripts / taglist.vim

※ NERDTreeはtrinity.vimにも入っているらしい。

$ cd ~/.vim/bundle
$ git clone https://github.com/vim-scripts/trinity.vim
$ git clone https://github.com/vim-scripts/Source-Explorer-srcexpl.vim
$ git clone https://github.com/vim-scripts/taglist.vim


使ってみた感想

面白いけど、遅くなるのと狭くなるのがいただけない。
もうちょっと設定すれば、使い勝手よくなるのかもしれないが、
定着するかな・・・。

[vim] vimからmysqlを操作するプラグイン dbext

最近、mysqlのクエリたたくことが多くなってきた。

周りの人は、mysql workbenchというGUIクライアントを使っている。
MySQL Workbench

リレーション張りまくりのテーブル設計するにはいいかもしれないが、
現状テーブル作るときはsymfonyのコマンド使うし、
ちょっとselect実行するためにGUI立ち上げるのはめんどくさい。


そして何より、周りの人とおんなじもの使うのは負けた気がする。

で、vimからmysqlを実行するためのプラグイン dbextを入れてみた。
vim-scripts / dbext.vim
Vimから任意のデータベースを操作


インストール


pathogenを使っていれば、以下でOK。
$ cd ~/.vim/bundle
$ git clone https://github.com/vim-scripts/dbext.vim
便利だ。
[vim] pathogenを導入

設定

ユーザ名やパスワード、DB名等を.vimrcに書き込んでおく。
let dbext_default_profile=""
let dbext_default_type="MYSQL"
let dbext_default_user="root"
let dbext_default_passwd=""
let dbext_default_dbname="jobeet"
let dbext_default_host="localhost"
let dbext_default_buffer_lines=20
「let dbext_default_buffer_lines=20」は結果を表示するウインドウの行数。

ショートカットキーとかは初期設定でも使えそう。

使い方

① sqlを実行
ノーマルモードもしくはビジュアルモードで、「\ + se」
ノーマルモードでは、カーソルがある行。
ビジュアルモードでは、選択した範囲のSQLが実行される。
実行すると別ウインドウで結果が表示される。


② テーブルのselectを実行
たとえばインサート文が書いてるファイルを開き、
対象のテーブルの中身を見たいとき、
カーソルをテーブル名に合わせ「\ + st」とすればselectした結果が表示される。
いちいちselect文書く必要はない。

③ テーブルのdescを実行
あるテーブルの構造が知りたいときは、
カーソル下にテーブルがある状態で「\ + sdt」



④ show tablesを実行
DBにあるテーブルが知りたいときは、「\ + slt」
ただ、おしにくい。


そのうちやりたいこと

① 接続先を簡単に変更できる設定
接続先は頻繁に変更することになるから、絶対に必要。
unite source作るか。

② DB変更
今はあんまり使わないが、DBの変更も簡単にできるようにしておくか。

③ hook script
ヘルプを読むと、結果を表示した後に結果を適当に編集して表示できるっぽい。
今のとこ、数字と文字列に別の色つけるくらいのアイディアしか浮かばないが、
そのうちもうチョイ面白い設定をしてみたい。

④ tag設定
:DBCompleteTableを実行すれば、
テーブル名補完ができるようになる。
適当に設定して、そのまま放置していた補完回りと一緒にまとめたい。

[vim] 地味に便利。関数を抽出するunite sourceを作った。

前々から書いてみようと思っていたunite のsource。
現在編集しているファイルから関数を定義している行を抽出し、そこにジャンプできるものを書いた。


uniteソースの書き方


unite、vim、source、書き方、
とか調べたら結構出てきた。

あと、unite.vimのデフォルトソースとか参考になった。


使い方


プログラムのファイルを開いた状態で、
vimのコマンドに以下を入力すると。
:Unite get_function


こんな感じで表示される。
Sources: get_function
>
-   13   public function executeIndex(sfWebRequest $request)
-   21   public function executeShow(sfWebRequest $request)
-   27   public function executeNew(sfWebRequest $request)
-   32   public function executeCreate(sfWebRequest $request)
-   45   public function executeEdit(sfWebRequest $request)
-   51   public function executeUpdate(sfWebRequest $request)
-   62   public function executeDelete(sfWebRequest $request)
-   72   protected function processForm(sfWebRequest $request, sfForm $form)

たとえば、「- 32 public function executeCreate(sfWebRequest $request)」を選択してEnterを押すと。
  public function executeCreate(sfWebRequest $request)
  {
    $jobeet_affiliate::func_karino2();
    $this->forward404Unless($request->isMethod(sfRequest::POST));
    $this->form = new jobeet_jobForm();
    $this->processForm($request, $this->form);
    $this->setTemplate('new');
  }
この関数にジャンプする。
普通に便利だ。
これあれば、foldingとかいらないんじゃないかな。

実際使うときは、下の感じでキーバインドを設定した。
noremap  :Unite  get_function -direction=botright  


インストール


以下のソースを
.vim/bundle/unite-function/autoload/unite/sources
以下に配置。

"----------------------------------------------------------+
" get function                                             |
"----------------------------------------------------------+
let s:save_cpo = &cpo
set cpo&vim


" define source
let s:get_function = {
\   'name': 'get_function',
\   'action_table': {},
\   'default_action': {'common':'execute'},
\}


" bzr status
function! s:get_function.gather_candidates(args, context)

  let s:path = expand('%:p')
  let s:ext  = expand('%:e')
  let s:lines = getbufline('%', 1, '$')

  let s:func_list = []
  let s:line_number = 1

  for line in s:lines
    if line =~ 'function ' && s:ext == "php"
      call add(s:func_list, [s:line_number, line])

    elseif line =~ 'function! ' && s:ext == "vim"
      call add(s:func_list, [s:line_number, line])

    elseif line =~ 'def ' && s:ext == "py"
      call add(s:func_list, [s:line_number, line])

    endif
    let s:line_number += 1
  endfor

  return map(copy(s:func_list), '{
  \   "word": v:val[0]." ".v:val[1],
  \   "source": "get_function",
  \   "kind": "jump_list",
  \   "action__path": s:path,
  \   "action__line": v:val[0]
  \ }')

endfunction

"
function! unite#sources#get_function#define()
    return [ s:get_function ]
endfunction

let &cpo = s:save_cpo
unlet s:save_cpo

2012年1月7日土曜日

[vim] pathogenを導入

プラグインを入れるたび、だんだん.vimのなかがカオスになってきた。

vimプラグインの管理をpathogen.vimにした

これを入れると、.vim/bundle/以下を、.vim/以下と同じ要に読んでくれるらしい。

① インストール
tpope / vim-pathogen
から落として、.vim/autoloadに配置。

② プラグインをbundleに移動。
移動というか、一回消して、gitでbunldeいかに入れなおした。
入れなおしたのは。

+ NERD_Tree
+ neocomplecache
+ quickrun

③ 使ってみた感想。
すごく良い。
入れなおしたの3つだけなのに、.vimの中がすっきりした。
特に、NERD_Treeとかneocomplecacheとか、.vim/直下にいっぱいディレクトリ作るから、
それなくなっただけでもほんとにきれい。
しかも、プラグインのインストールが楽。
git コマンド1つでインストールが完了する。

次は、vimrcを整理したい。

[python] これは誰のせりふでしょう?→「また負けた・・・働いてないのに また負けちゃった・・・・・・ギャンブル負けちゃった・・・」

分類器とは、入力の特徴を元にクラス分けするプログラムの事。メールスパムフィルタ、テキストのカテゴリ分け、形態素解析の品詞推定などに使われている。今回調べてみた「単純ベイズ分類器」のほかにも、「決定木」、「最大エントロピー分類器」とかがあるみたい。

ここで丁寧に解説されている。ほぼほぼパクリw。
ナイーブベイズを用いたテキスト分類

統計的手法に基づくスパムフィルタの設計と実装
あと、NLTK本の6章。


分類器の動作手順

良くある分類器の処理の流れ。

① 素性抽出
まず、入力から素性を抽出する。ここでの入力は、任意の文章を想定している。素性とは、入力を分類をするための指標となる文字列で、どのように分類したいかによって変わる。たとえば、ブログのラベリングであれば、文章中の頻出語句を素性として使えるだろうし、話し手の性別を判定したいのであれば、文の語尾を素性として使えるかもしれない。ようは、分類に大きな影響を与えるものを素性として選ぶ。

② 機械学習
機械学習では、素性とラベルを対応付ける分類モデルを作成する。分類モデルの種類としては、決定木や単純ベイズ分類器のほかにも、ニューラルネットワーク、サポートベクターマシンとかかなりいろいろあるっぽい。分類モデルを作成するためには、もととなる素性とラベルの組み合わせデータが必要で人の手で作成される事が多いらしい。このデータから分類モデルを作成する事を訓練といい、訓練が必要となる分類モデルを教師あり分類と教師あり~って言われる。

③ 分類
入力から素性を抽出して、機械学習で作成した分類モデルを使って入力を分類する。


単純ベイズ分類器の仕組み


単純ベイズ分類器では、「文章Xが与えられたとき、そのラベルAである条件付き確率」を計算し、最も確率が高いラベルを文章に付与する。この条件付き確率 P(label|doc)を計算する。

① ベイズの定理より。

※ P(doc)はどのカテゴリでも共通なので無視。

② 文章を素性の集合と考え、それぞれの素性は独立に(ベイズ仮定)


③ P(label), P(word_i|label)を計算する。
P(label)は、「labelに属する文書 / 総文書数」で求められる。
P(word_i|label)は、「対象文書に含まれるword_iの数 / labelに属する文書の総単語数」で求められる。



※ ゼロ頻度問題に対応するため、P(wordi|label)の計算の際、出現回数に1を加えるラプラススムージングを行う。

④ アンダーフロー回避のため、logをとる。

※ logをとっても,P(label|doc)の大小関係は変化しないため、判定に影響はない。

⑤ ④に③を代入し、P(label|doc)を求める。


単純ベイズ分類器の実装


せっかくベイズ分類ができるので、
カイジに出てきたキャラクターの台詞を学習させ、
他のせりふが誰のものか判別できるかどうかためしてみた。

ここの台詞を学習させた。
心に刻め!!カイジの名言集

ソースコードは結構長いので最後に貼り付ける。

また、似たような事は潜在的意味解析でもできるが、
潜在的意味解析で判定できるのは、「与えた台詞が、誰のどの台詞に近いか」であって、
分類器でできる「与えた台詞が、誰の台詞に近いか」とは異なる。



まずは、カイジ君
「また負けた・・・働いてないのに また負けちゃった・・・・・・ギャンブル負けちゃった・・・」

結果
[karino@localhost simple_bayesian_classifier]$ ./simple_bayesian.py 

また負けた・・・働いてないのに また負けちゃった・・・・・・ギャンブル負けちゃった・・・

カイジ: -48.1215877591
利根川: -51.8378671316
会長  : -52.5295179405
遠藤  : -52.0766103197
大槻  : -52.5066907554

label :  カイジ
お。ちゃんとカイジと判定された!

次は、あえての大槻班長。
「大槻班長:食べ終わったら、奴はとりあえず満足してこう考えるだろう。明日からがんばろう。明日から節制だと。」

[karino@localhost simple_bayesian_classifier]$ ./simple_bayesian.py 

食べ終わったら、奴はとりあえず満足してこう考えるだろう。明日からがんばろう。明日から節制だと。

カイジ: -57.5804609164
利根川: -56.3075993467
会長  : -57.4567716257
遠藤  : -56.8765245825
大槻  : -55.7732979831

label :  大槻

おぉ。ちゃんと班長と判定されたw。かなり恣意的な気もするが。

最後を飾るのはもちろん利根川先生のこの台詞。
「ファックユー ぶち殺すぞ ゴミめら!」

[karino@localhost simple_bayesian_classifier]$ ./simple_bayesian.py 

ファックユー ぶち殺すぞ ゴミら!

カイジ: -24.2264367345
利根川: -23.9961446128
会長  : -24.4275136119
遠藤  : -23.9181559224
大槻  : -25.1875317303

label :  遠藤勇次
あぁ、遠藤さんになっちゃった。近いといえば近いが。

まぁ、こんな様な事ができるってデモでした。


#!/usr/bin/python
# coding=utf-8

import MeCab
import math
import sys
import codecs
from decimal import Decimal
from collections import defaultdict
reload(sys)
sys.setdefaultencoding('utf-8')
sys.stdout = codecs.getwriter('utf-8') (sys.stdout)

kaiji =[
        ["カイジ","おまえは100%成功しないタイプ…!"],
        ["利根川幸雄","一生迷ってろ…!そして失い続けるんだ…貴重な機会(チャンス)をっ!"],
        ["大槻","明日からがんばるんじゃない…今日…今日だけがんばるんだっ…!今日をがんばった者…今日をがんばり始めた者にのみ…明日が来るんだよ…!"],
        ["カイジ","奇跡なんて望むな!「勝つ」ってことは…そんな神頼みなんかじゃなく…具体的な勝算の彼方にある…現実だ…!勝つべくして勝つ…!"],
        ["カイジ","疑ってるうちはまだしもそれを口にしたら…戦争だろうがっ!"],
        ["利根川幸雄"," 金は命より重い…!そこの認識をごまかす輩は生涯地を這う…!"],
        ["兵藤和尊","大詰めで弱い人間は信用できぬ…!つまりそれは管理はできても勝負のできぬ男…平常時の仕事は無難にこなしても緊急時にはクソの役にも立たぬということだ!]",],
        ["兵藤和尊","剥げたな…お前の化けの皮…二流だ…しょせんお前は指示待ち人間…!",],
        ["カイジ","胸を張れっ!手痛く負けた時こそ…胸をっ…!",],
        ["利根川幸雄"," 大人は質問に答えたりしない それが基本だ",],
        ["カイジ","堂々といけっ…!やばい時ほど堂々と…",],
        ["カイジ","ピンチだけど…チャンスッ…",],
        ["遠藤勇次","おまえの毎日って今……ゴミって感じだろ?無気力で自堕落で非生産的",],
        ["岡林","友情や口約束でもらえるものは旅先からの絵ハガキや土産…あるいは思い出の品というガラクタ…そんな程度のものだ…",],
        ["利根川幸雄","なめてなどいない…熟知しているだけだ…人間の無力について…!",],
        ["カイジ","変でいい、変でなきゃダメだ…狂ってなきゃ、逸脱してなきゃ悪魔は殺せない…!常軌を逸してこそ開かれる、勝ちへの道が…!",],
        ["兵藤和尊","借金における誠意なんて、これはもう誰が考えたって一つしかないのだ…内臓を売ろうと、強盗をしでかそうと…何をしてもいいから、要するに…期限までに金を返すことだっ…!",],
        ["カイジ","敗者は失うっ…!それをねじ曲げたら…なにがなにやらわからない…受け入れるべきだっ…!負けを受け入れることが…敗者の誇り…オレは…負けをぼかさないっ…!",],
        ["カイジ","運・勘・人に頼る勝負はやめだ…そういうノータリンな振る舞いはもうやめ…自分の頭で考え…勝つべくして勝つ…",],
        ["利根川幸雄","よく戦ったからじゃない…彼らは勝った、ゆえに今、そのすべて…人格まで肯定されている…!",],
        ["カイジ","いっちゃ悪いが、奴ら正真正銘のクズ…負けたからクズってことじゃなくて可能性を追わないからクズ…",],
        ["カイジ"," 目先を追うな!いい加減気がつけ!耐えることなくして勝利はないんだっ!",],
        ["カイジ","他人なんか関係ねえんだよ…!オレだっ…!オレだっ…!オレなんだっ…!肝心なのはいつも…!オレがやると決めてやる…ただそれだけだっ…!",],
        ["遠藤勇次","一体「何」を「いつまで」待つつもりだ…?こんな薄汚いアパートで…貧しいバイトをして…半ば眠ったような意識で鬱々と「待つ」…?そういうのを無為っていうんだっ…!",],
        ["カイジ"," 落とさなきゃ落とされる…この仕組みは…この世の姿そのもの…基本も基本…大原則だっ…!",],
        ["カイジ","できるかどうかじゃない!やるんだっ!勝つために生きなくてどうするっ…!",],
        ["兵藤和尊","命はもっと粗末に扱うべきなのだ…!命は、生命は…丁寧に扱いすぎると澱み腐る",],
        ["ナレーション","いたずらに時間だけが流れていく…!まるで命そのもののような…血の時間が…",],
       ]

def get_kaiji_morpheme(sentences):
    data = []
    for title, sentence in sentences:
        data.append([title] + get_morpheme(sentence))
    return data

def get_morpheme(sentence):
    """Use Mecab"""
    t = MeCab.Tagger (" ".join(sys.argv))
    m = t.parseToNode(sentence)
    data = defaultdict(list)
    data = []

    while m:
        parse= m.feature.split(',')[0]
        if parse == "名詞" or parse == "動詞":
            #print m.surface
            data.append(m.surface)
        m = m.next
    return data

class NaiveBayes:
    """NaiveBayes"""

    def __init__(self):
        self.categories = set()
        self.vocabularies = set()
        self.wordcount = {}
        self.catcount = {}
        self.denominator = {}

    def train(self, data):
        """ナイーブベイズ分類器の訓練"""

        # 文章集合からカテゴリを抽出して辞書を初期化
        for d in data:
            cat = d[0]
            self.categories.add(cat)
        for cat in self.categories:
            #print cat
            self.wordcount[cat] = defaultdict(int)
            self.catcount[cat] = 0

        # 文章集合からカテゴリと単語をカウント
        for d in data:
            cat, doc = d[0], d[1:]
            self.catcount[cat] += 1
            for word in doc:
                self.vocabularies.add(word)
                self.wordcount[cat][word] += 1

        # 単語の条件付確率分布の分母をあらかじめ一括計算しておく
        for cat in self.categories:
            self.denominator[cat] = sum(self.wordcount[cat].values()) + len(self.vocabularies)

    def classify(self, doc):
        """事後確率の対数log(P(cat|doc))がもっとも大きなカテゴリを返す"""
        best = None
        max = -sys.maxint
        for cat in self.catcount.keys():
            p = self.score(doc, cat)
            if p > max:
                max = p
                best = cat
        return best


    def score(self, doc, cat):
        """文書が与えられたときのカテゴリの事後確率の対数"""
        total = sum(self.catcount.values())
        score = math.log(Decimal(self.catcount[cat]) / Decimal(total))
        for word in doc:
            score += math.log(self.wordprod(word, cat))

        return score

    def keywords(self):
        """単語の条件付き確率P(word|cat)を求める"""
        for cat in self.catcount.keys():
            print cat

    def wordprod(self, word, cat):
        """単語の条件付き確率P(word|cat)を求める"""
        # ラプラススムージングを適用
        return Decimal(self.wordcount[cat][word] + 1) / Decimal(self.denominator[cat])

    def __str__(self):
        total = sum(self.catcount.values())
        return "documents: %d, vocabularies: %d, categories: %d" % (total, len(self.vocabularies), len(self.categories))


# main
def main():
    #
    data = get_kaiji_morpheme(kaiji)

    # ナイーブベイズ分類器の訓練
    nb = NaiveBayes()
    nb.train(data)

    #print nb
    #nb.keywords()


    # テストデータのカテゴリを予測
    string = "また負けた・・・働いてないのに また負けちゃった・・・・・・ギャンブル負けちゃった・・・"
    string = "食べ終わったら、奴はとりあえず満足してこう考えるだろう。明日からがんばろう。明日から節制だと。"
    string = "ファックユー ぶち殺すぞ ゴミら!"

    test = get_morpheme(string)
    print
    print string
    print
    print "カイジ:", nb.score(test, "カイジ")
    print "利根川:", nb.score(test, "利根川幸雄")
    print "会長  :", nb.score(test, "兵藤和尊")
    print "遠藤  :", nb.score(test, "遠藤勇次")
    print "大槻  :", nb.score(test, "大槻")
    print
    print "label : ",nb.classify(test)


if __name__ == '__main__':
    main()

2012年1月5日木曜日

[python] mecab-pythonのインストールメモ。

mecab-pythonのインストールメモ。

① ダウンロード
mecab-python

② インストール
$ sudo python setup.py build
$ sudo python setup.py install

※ Mecab本体は別途インストールする。


③ コマンドラインから実行
[karino@localhost mecab-python-0.98]$ python
Python 2.6.6 (r266:84292, Dec  7 2011, 20:48:22) 
[GCC 4.4.6 20110731 (Red Hat 4.4.6-3)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import MeCab
>>> m=MeCab.Tagger("-Ochasen")
>>> print m.parse("命はもっと粗末に扱うべきなのだ…!命は、生命は…丁寧に扱いすぎると澱み腐る")
命      イノチ  命      名詞-一般               
は      ハ      は      助詞-係助詞             
もっと  モット  もっと  副詞-一般               
粗末    ソマツ  粗末    名詞-形容動詞語幹               
に      ニ      に      助詞-副詞化             
扱う    アツカウ        扱う    動詞-自立       五段・ワ行促音便        基本形
べき    ベキ    べし    助動詞  文語・ベシ      体言接続
な      ナ      だ      助動詞  特殊・ダ        体言接続
の      ノ      の      名詞-非自立-一般                
だ      ダ      だ      助動詞  特殊・ダ        基本形
…      …      …      記号-一般               
!      !      !      記号-一般               
命      イノチ  命      名詞-一般               
は      ハ      は      助詞-係助詞             
、      、      、      記号-読点               
生命    セイメイ        生命    名詞-一般               
は      ハ      は      助詞-係助詞             
…      …      …      記号-一般               
丁寧    テイネイ        丁寧    名詞-形容動詞語幹               
に      ニ      に      助詞-副詞化             
扱い    アツカイ        扱う    動詞-自立       五段・ワ行促音便        連用形
すぎる  スギル  すぎる  動詞-非自立     一段    基本形
と      ト      と      助詞-格助詞-引用                
澱み    ヨドミ  澱む    動詞-自立       五段・マ行      連用形
腐る    クサル  腐る    動詞-自立       五段・ラ行      基本形
EOS

>>> 

[python] バージョン管理ツールbazaarのフックスクリプトを書いてみた

今日、文末に「;」がついてないphpソースをコミットしてしまった。

2回も。

bazaarのフックスクリプトを書いた。

内容は、コミットするときに、対象となるphpファイルに対して、
「php -l」を実行し、エラーがあったら、コミットしないというもの。
pythonで簡単にフックスクリプトを書けるのはありがたい。
参考文献が少なくて結構手こずったけども。

ソース


"""this is a plugin/hook for bazaar. just add this file to ~/.bazaar/plugins/"""


import os
import subprocess
import commands
from itertools import chain
from bzrlib import branch
from bzrlib import errors

version_info = (0, 0, 1, 'dev', 1)


def get_php_files(tree_delta):
    """get php files"""
    php_files = []

    for delta_file in chain(tree_delta.added, tree_delta.modified):

        # not php file
        bzr_path, file_id, type_ = delta_file[:3]
        root, ext = os.path.splitext(bzr_path)

        # not php file
        if ext == ".php":
            php_files.append(bzr_path)
            continue

    return php_files


def pre_commit_hook(local, master, old_revno, old_revid, future_revno, future_revid, tree_delta, future_tree):
    """This hook will execute precommit script from root path of the bazaar
        branch. Commit will be canceled if precommit fails."""

    # fix flg
    errflg = 0

    # get root directory path
    root_path = commands.getoutput("bzr root")

    # check file
    for bzr_path in  get_php_files(tree_delta):
        # exe php -l
        absolute_path = root_path + "/" + bzr_path
        if subprocess.call(["php", "-l", absolute_path]):
            errflg = 1
        print ""

    # commit
    if errflg == 1:
        raise errors.BzrError("You hsve several bugs in your php files.")
    else:
        print "Congratulations! There are no errors."

branch.Branch.hooks.install_named_hook('pre_commit', pre_commit_hook, 'Check pre_commit hook')

「~/.bazaar/plugins/」以下におけば、自動で読み込まれるはず。

rootのパスを取るために「bzr root」を実行しているところがすごくいけてない・・・。
絶対、引数から持ってこれるとは思うけど、調べてもわからなかったから、
取り急ぎこれで。


結果


ダメなphpファイルをコミットしてみる。


コミット結果
karino@goldfish:~/bzr_test/bran/karino$ bzr commit -m 'karino' 
Committing to: /home/karino/bzr_test/karino/                                                                                                                          
modified karino.php
PHP Parse error:  syntax error, unexpected $end, expecting T_VARIABLE or T_DOLLAR_OPEN_CURLY_BRACES or T_CURLY_OPEN in /home/karino/bzr_test/bran/karino/karino.php on line 4
Errors parsing /home/karino/bzr_test/bran/karino/karino.php

bzr: ERROR: You hsve several bugs in your php files. 
このようにコミット拒否される。

phpファイルを修正してコミットしてみると。


karino@goldfish:~/bzr_test/bran/karino$ bzr commit -m 'karino' 
Committing to: /home/karino/bzr_test/karino/                                                                                                                          
modified karino.php
No syntax errors detected in /home/karino/bzr_test/bran/karino/karino.php                                                                                             

Congratulations! There are no errors.
Committed revision 4650.


参考になったページ


bazaarのプラグインを作る(1)

フックを利用する

how to execute tests on a bazaar pre-commit hook

jk0 / bzr-pre-commit-hook

2012年1月3日火曜日

[python] 辞書型形態素解析器の作成

NLTK本12章に形態素解析のアルゴリズムが紹介されていたので、実装してみた。
12.2 日本語形態素解析

Web+DB vol.64にも、日本語かな漢字変換器の実装として、同様の手法が紹介されているが、
これだけ読んで、実装するのはちょっと骨にひびはいるかも。
ざっくりと概略つかむにはいい。
WEB+DB PRESS vol.64に日本語入力の記事を書いたよ


辞書型形態素解析の流れ


① 辞書に基づいた形態素分解
入力した文字列に対して可能な形態素分割をすべて求める。具体的には、入力文字列の頭から抽出できる文字列で辞書を引き、ヒットしたものを列挙する。次に、抽出開始位置を1文字進めて再度文字列を抽出し、辞書を引き、ヒットしたものを列挙する。たとえば、「くるま」なら、「く(区)」「く (九)」「くる(来る)」「くるま(車)」などの辞書エントリを列挙する。これは「共通接頭辞探索」とよばれ、このアルゴリズムに適した辞書のデータ構造として「トライ構造」が使われている。


② ラティス構造の作成
共通接頭辞探索を先頭から順に行う際、得られた形態素とその位置で終わる形態素との組み合わせを考え、接続したグラフ構造を作成する。この構造は「ラティス構造」と呼ばれる。このラティス構造を作成する際、つまり、形態素同士を接続する際、日本語の文法に正しいかどうかを確認し、間違っている場合は接続しない。このように連接可能な形態素同士が接続したラティス構造が作成される。


③ もっとも適切な解析結果の取得
ラティス構造を作成した段階で、日本語の文法的に正しい解析結果を得ることができる。より日本語らしい解析結果を上位に持ってくるための手法の一つとして、「コスト最小法」がある。この方法は、「形態素と形態素の結びつきやすさ/にくさ」(連接コスト)、「形態素自体の出現のしやすさ/しにくさ」(生起コスト)を各形態素ごとに割り当て、文全体で見たときのコストの総和を計算することで、日本語らしい解析結果を得る。

※ コストそのものの導出は、機械学習で作成したり、
統計的な手法(隠れマルコフモデルや条件付き確率場)を使って作成、推定するらしい。


辞書型形態素解析の実装


とりあえず雰囲気は出ている。
#!/usr/bin/python
# coding=utf-8

import sys
from collections import defaultdict
reload(sys)
sys.setdefaultencoding('utf-8')

# 辞書
dict_entries = [
    [u"かれ",     {'pos':'N',   'lemma':u"彼",    'cost':  0}],
    [u"の",       {'pos':'J-K', 'lemma':u"の",    'cost':  0}],
    [u"く",       {'pos':'N',   'lemma':u"区",    'cost':  0}],
    [u"くる",     {'pos':'V-S', 'lemma':u"来る",  'cost':  0}],
    [u"くる",     {'pos':'V-T', 'lemma':u"来る",  'cost':  0}],
    [u"くるま",   {'pos':'N',   'lemma':u"車",    'cost':  0}],
    [u"ま",       {'pos':'N',   'lemma':u"間",    'cost':  0}],
    [u"まで",     {'pos':'J-F', 'lemma':u"まで",  'cost':  0}],
    [u"で",       {'pos':'J-K', 'lemma':u"で",    'cost':  0}],
    [u"でま",     {'pos':'N',   'lemma':u"デマ",  'cost':  0}],
    [u"まつ",     {'pos':'N',   'lemma':u"松",    'cost':  0}],
    [u"まつ",     {'pos':'V-S', 'lemma':u"待つ",  'cost':  0}],
    [u"まつ",     {'pos':'V-T', 'lemma':u"待つ",  'cost':  0}],
    [u"つ",       {'pos':'N',   'lemma':u"津",    'cost':  0}],
    [u"こ",       {'pos':'SF',  'lemma':u"個",    'cost': 10}],
    [u"この",     {'pos':'T',   'lemma':u"この",  'cost': 10}],
    [u"ひ",       {'pos':'N',   'lemma':u"日",    'cost': 40}],
    [u"ひと",     {'pos':'N',   'lemma':u"人",    'cost': 40}],
    [u"ひとこと", {'pos':'N',   'lemma':u"一言",  'cost': 40}],
    [u"と",       {'pos':'J',   'lemma':u"と",    'cost': 10}],
    [u"こと",     {'pos':'N',   'lemma':u"事",    'cost': 10}],
    [u"で",       {'pos':'V-Z', 'lemma':u"出",    'cost': 40}],
    [u"で",       {'pos':'V-Y', 'lemma':u"出",    'cost': 40}],
    [u"で",       {'pos':'J',   'lemma':u"で",    'cost': 10}],
    [u"げんき",   {'pos':'N',   'lemma':u"元気",  'cost': 40}],
    [u"に",       {'pos':'J',   'lemma':u"に",    'cost': 10}],
    [u"になっ",   {'pos':'V-Y', 'lemma':u"担っ",  'cost': 40}],
    [u"なっ",     {'pos':'V-Y', 'lemma':u"なっ",  'cost': 40}],
    [u"た",       {'pos':'A',   'lemma':u"た",    'cost': 10}]
]

# Dic class
class Dic:
    """辞書クラス"""

    # トライの作成関数
    @staticmethod
    def insert(trie, key, value):
        """trie"""
        if key:
            first, rest = key[0], key[1:]
            if first not in trie:
                trie[first] = {}
            Dic.insert(trie[first], rest, value)
        else:
            if not 'value' in trie:
                trie['value'] = []
            trie['value'].append(value)

    # トライの作成
    @staticmethod
    def mktrie(dict_entries):
        matrie = {}
        for entry in dict_entries:
            entry[1]['length'] = len(entry[0])
            Dic.insert(matrie, entry[0].encode('utf-8'), entry[1])

        return matrie

# Node class
class Node:
    """ラティス構造を構築するためのノード"""
    def __init__(self, lemma, pos, node_cost, begin):
        self.lemma = lemma
        self.pos   = pos
        self.node_cost = node_cost
        self.length = len(lemma)
        self.next = []
        self.cost = 0
        self.begin = begin

# 形態素解析
def analyze(trie, sent, connect_func=lambda x,y: True, cost_func=lambda x,y: 0):

    # 共通接頭辞探索
    def common_prefix_search(trie, key):
        ret = []
        if 'value' in trie:
            ret += trie['value']
        if key:
            first, rest = key[0], key[1:]
            if first in trie:
                ret += common_prefix_search(trie[first], rest)
        return ret

    # 結果表示用
    def enum_solutions(node):
        results = []
        if node.lemma == u'EOS':
            return [[u'EOS']]
        for nnode in node.next:
            for solution in enum_solutions(nnode):
                results.append([node.lemma]+solution)
        return results


    # BOSノードの定義
    bos_node = Node('BOS', 'BOS', 0, -1)
    end_node_list = defaultdict(list)
    end_node_list[0].append(bos_node)


    # 文頭から
    for i in range(0, len(sent)+1):

        # 共通接頭辞探索
        if i < len(sent):
            cps_results = common_prefix_search(trie, sent[i:].encode('utf-8'))
        else:
            # EOS
            cps_results = [{'lemma':u'EOS','length':3,'pos':'EOS','cost':0}]

        # 各形態素の連接可能性、コストを計算
        for centry in cps_results:
            cnode = Node(centry['lemma'], centry['pos'], centry['cost'], i)
            min_cost = -1
            min_bnodes = []

            # 連接可能性を確認
            for bnode in end_node_list[i]:

                # 連接可能性を確認
                if connect_func(bnode, cnode):

                    # 生起コスト+連接コスト
                    cost = bnode.cost + cost_func(bnode, cnode)

                    # コストが最小のものを選択
                    if min_cost < 0 or cost < min_cost:
                        min_cost = cost
                        min_bnodes = [bnode]
                    elif cost == min_cost:
                        min_bnodes.append(bnode)

            if len(min_bnodes) > 0:
                for bnode in min_bnodes:
                    cnode.cost = min_cost
                    bnode.next.append(cnode)

                end_nodes = end_node_list[i + centry['length']]
                if not cnode in end_nodes: end_nodes.append(cnode)

    return enum_solutions(bos_node)

# main
def main():
    """main"""

    # 連接可能判定関数
    def is_connectable(bnode, cnode):
        """is_connectable"""
        ctable = set([
            ('BOS', 'N'),('BOS', 'V'),('BOS','T'),
            ('T', 'N'),('N', 'J'),('J','N'),('J','V'),
            ('V-T', 'N'),('V-T', 'J-F'),('V-Y', 'A'),
            ('V-S', 'EOS'),('A', 'EOS'),
        ])

        bpos = bnode.pos
        bpos_s = bpos.split('-')[0]
        cpos = cnode.pos
        cpos_s = cpos.split('-')[0]

        #return True
        return ((bpos, cpos) in ctable) or ((bpos_s, cpos) in ctable) \
            or ((bpos, cpos_s) in ctable) or ((bpos_s, cpos_s) in ctable)

    # 連接コスト計算関数
    def cost_minimum(bnode, cnode):
        ctable = {
            ('BOS', 'T'): 10,
            ('T', 'N'):   10,
            ('N', 'J'):   10,
            ('J', 'N'):   10,
            ('N', 'N'):   10,
            ('N', 'V-Z'): 40,
            ('N', 'V-Y'): 40,
            ('V-Z', 'N'): 40,
            ('V-Y', 'N'): 40,
            ('J', 'V-Z'): 10,
            ('J', 'V-Y'): 10,
            ('V-Y', 'A'): 10,
            ('A', 'EOS'): 10
        }
        pos_2gram = (bnode.pos, cnode.pos)
        return cnode.cost + (ctable[pos_2gram] if pos_2gram in ctable else 100)


    # 辞書のtrieを作成
    matrie = Dic.mktrie(dict_entries)

    # 解析
    res = analyze(matrie, u"かれのくるまでまつ", is_connectable, cost_minimum)
    print "解析1"
    print '\n'.join('/'.join(sent) for sent in res)
    print

    # 解析
    res = analyze(matrie, u"このひとことでげんきになった", is_connectable, cost_minimum)
    print "解析2"
    print '\n'.join('/'.join(sent) for sent in res)
    print

if __name__ == '__main__':
    main()

実行結果
[karino@localhost jisyo]$ python analyze_fix.py 
解析1
BOS/彼/の/車/で/待つ/EOS

解析2
BOS/この/一言/で/元気/に/なっ/た/EOS



コストをすべてゼロにした場合の実行結果。
考えられるすべての組み合わせが出力される。
解析1
BOS/彼/の/来る/間/で/間/津/EOS
BOS/彼/の/来る/間/で/松/EOS
BOS/彼/の/来る/間/で/待つ/EOS
BOS/彼/の/来る/間/で/待つ/EOS
BOS/彼/の/来る/間/出/間/津/EOS
BOS/彼/の/来る/間/出/松/EOS
BOS/彼/の/来る/間/出/待つ/EOS
BOS/彼/の/来る/間/出/待つ/EOS
BOS/彼/の/来る/間/出/間/津/EOS
BOS/彼/の/来る/間/出/松/EOS
BOS/彼/の/来る/間/出/待つ/EOS
BOS/彼/の/来る/間/出/待つ/EOS
BOS/彼/の/来る/間/で/間/津/EOS
BOS/彼/の/来る/間/で/松/EOS
BOS/彼/の/来る/間/で/待つ/EOS
BOS/彼/の/来る/間/で/待つ/EOS
BOS/彼/の/来る/間/デマ/津/EOS
BOS/彼/の/来る/まで/間/津/EOS
BOS/彼/の/来る/まで/松/EOS
BOS/彼/の/来る/まで/待つ/EOS
BOS/彼/の/来る/まで/待つ/EOS
BOS/彼/の/来る/間/で/間/津/EOS
BOS/彼/の/来る/間/で/松/EOS
BOS/彼/の/来る/間/で/待つ/EOS
BOS/彼/の/来る/間/で/待つ/EOS
BOS/彼/の/来る/間/出/間/津/EOS
BOS/彼/の/来る/間/出/松/EOS
BOS/彼/の/来る/間/出/待つ/EOS
BOS/彼/の/来る/間/出/待つ/EOS
BOS/彼/の/来る/間/出/間/津/EOS
BOS/彼/の/来る/間/出/松/EOS
BOS/彼/の/来る/間/出/待つ/EOS
BOS/彼/の/来る/間/出/待つ/EOS
BOS/彼/の/来る/間/で/間/津/EOS
BOS/彼/の/来る/間/で/松/EOS
BOS/彼/の/来る/間/で/待つ/EOS
BOS/彼/の/来る/間/で/待つ/EOS
BOS/彼/の/来る/間/デマ/津/EOS
BOS/彼/の/来る/まで/間/津/EOS
BOS/彼/の/来る/まで/松/EOS
BOS/彼/の/来る/まで/待つ/EOS
BOS/彼/の/来る/まで/待つ/EOS
BOS/彼/の/車/で/間/津/EOS
BOS/彼/の/車/で/松/EOS
BOS/彼/の/車/で/待つ/EOS
BOS/彼/の/車/で/待つ/EOS
BOS/彼/の/車/出/間/津/EOS
BOS/彼/の/車/出/松/EOS
BOS/彼/の/車/出/待つ/EOS
BOS/彼/の/車/出/待つ/EOS
BOS/彼/の/車/出/間/津/EOS
BOS/彼/の/車/出/松/EOS
BOS/彼/の/車/出/待つ/EOS
BOS/彼/の/車/出/待つ/EOS
BOS/彼/の/車/で/間/津/EOS
BOS/彼/の/車/で/松/EOS
BOS/彼/の/車/で/待つ/EOS
BOS/彼/の/車/で/待つ/EOS
BOS/彼/の/車/デマ/津/EOS

解析2
BOS/個/の/日/と/個/と/で/元気/に/なっ/た/EOS
BOS/個/の/日/と/個/と/で/元気/担っ/た/EOS
BOS/個/の/日/と/個/と/出/元気/に/なっ/た/EOS
BOS/個/の/日/と/個/と/出/元気/担っ/た/EOS
BOS/個/の/日/と/個/と/出/元気/に/なっ/た/EOS
BOS/個/の/日/と/個/と/出/元気/担っ/た/EOS
BOS/個/の/日/と/個/と/で/元気/に/なっ/た/EOS
BOS/個/の/日/と/個/と/で/元気/担っ/た/EOS
BOS/個/の/日/と/事/で/元気/に/なっ/た/EOS
BOS/個/の/日/と/事/で/元気/担っ/た/EOS
BOS/個/の/日/と/事/出/元気/に/なっ/た/EOS
BOS/個/の/日/と/事/出/元気/担っ/た/EOS
BOS/個/の/日/と/事/出/元気/に/なっ/た/EOS
BOS/個/の/日/と/事/出/元気/担っ/た/EOS
BOS/個/の/日/と/事/で/元気/に/なっ/た/EOS
BOS/個/の/日/と/事/で/元気/担っ/た/EOS
BOS/個/の/人/個/と/で/元気/に/なっ/た/EOS
BOS/個/の/人/個/と/で/元気/担っ/た/EOS
BOS/個/の/人/個/と/出/元気/に/なっ/た/EOS
BOS/個/の/人/個/と/出/元気/担っ/た/EOS
BOS/個/の/人/個/と/出/元気/に/なっ/た/EOS
BOS/個/の/人/個/と/出/元気/担っ/た/EOS
BOS/個/の/人/個/と/で/元気/に/なっ/た/EOS
BOS/個/の/人/個/と/で/元気/担っ/た/EOS
BOS/個/の/人/事/で/元気/に/なっ/た/EOS
BOS/個/の/人/事/で/元気/担っ/た/EOS
BOS/個/の/人/事/出/元気/に/なっ/た/EOS
BOS/個/の/人/事/出/元気/担っ/た/EOS
BOS/個/の/人/事/出/元気/に/なっ/た/EOS
BOS/個/の/人/事/出/元気/担っ/た/EOS
BOS/個/の/人/事/で/元気/に/なっ/た/EOS
BOS/個/の/人/事/で/元気/担っ/た/EOS
BOS/個/の/一言/で/元気/に/なっ/た/EOS
BOS/個/の/一言/で/元気/担っ/た/EOS
BOS/個/の/一言/出/元気/に/なっ/た/EOS
BOS/個/の/一言/出/元気/担っ/た/EOS
BOS/個/の/一言/出/元気/に/なっ/た/EOS
BOS/個/の/一言/出/元気/担っ/た/EOS
BOS/個/の/一言/で/元気/に/なっ/た/EOS
BOS/個/の/一言/で/元気/担っ/た/EOS
BOS/この/日/と/個/と/で/元気/に/なっ/た/EOS
BOS/この/日/と/個/と/で/元気/担っ/た/EOS
BOS/この/日/と/個/と/出/元気/に/なっ/た/EOS
BOS/この/日/と/個/と/出/元気/担っ/た/EOS
BOS/この/日/と/個/と/出/元気/に/なっ/た/EOS
BOS/この/日/と/個/と/出/元気/担っ/た/EOS
BOS/この/日/と/個/と/で/元気/に/なっ/た/EOS
BOS/この/日/と/個/と/で/元気/担っ/た/EOS
BOS/この/日/と/事/で/元気/に/なっ/た/EOS
BOS/この/日/と/事/で/元気/担っ/た/EOS
BOS/この/日/と/事/出/元気/に/なっ/た/EOS
BOS/この/日/と/事/出/元気/担っ/た/EOS
BOS/この/日/と/事/出/元気/に/なっ/た/EOS
BOS/この/日/と/事/出/元気/担っ/た/EOS
BOS/この/日/と/事/で/元気/に/なっ/た/EOS
BOS/この/日/と/事/で/元気/担っ/た/EOS
BOS/この/人/個/と/で/元気/に/なっ/た/EOS
BOS/この/人/個/と/で/元気/担っ/た/EOS
BOS/この/人/個/と/出/元気/に/なっ/た/EOS
BOS/この/人/個/と/出/元気/担っ/た/EOS
BOS/この/人/個/と/出/元気/に/なっ/た/EOS
BOS/この/人/個/と/出/元気/担っ/た/EOS
BOS/この/人/個/と/で/元気/に/なっ/た/EOS
BOS/この/人/個/と/で/元気/担っ/た/EOS
BOS/この/人/事/で/元気/に/なっ/た/EOS
BOS/この/人/事/で/元気/担っ/た/EOS
BOS/この/人/事/出/元気/に/なっ/た/EOS
BOS/この/人/事/出/元気/担っ/た/EOS
BOS/この/人/事/出/元気/に/なっ/た/EOS
BOS/この/人/事/出/元気/担っ/た/EOS
BOS/この/人/事/で/元気/に/なっ/た/EOS
BOS/この/人/事/で/元気/担っ/た/EOS
BOS/この/一言/で/元気/に/なっ/た/EOS
BOS/この/一言/で/元気/担っ/た/EOS
BOS/この/一言/出/元気/に/なっ/た/EOS
BOS/この/一言/出/元気/担っ/た/EOS
BOS/この/一言/出/元気/に/なっ/た/EOS
BOS/この/一言/出/元気/担っ/た/EOS
BOS/この/一言/で/元気/に/なっ/た/EOS
BOS/この/一言/で/元気/担っ/た/EOS