git hooksを使って、確認しながらコミットする話。

# はじめに
　gitを使って開発しているときに、commitやpushをしてしまったあとに、「また、var_dumpを残してしまった」,　「DEBUGコメントが残ってしまってる...」,「なんだこの差分は！」 となることが結構あった。そこで、git commitする前に、var_dumpやDEBUGコメントが残っていないかを確認する仕組みがあれば良いと考え、git hooksを用いて実装することにした。また、pushする前にも、余分な差分が入っていないか親ブランチとのdiffをチェックができ、もし問題があったらpushを中断することも試みた。

# GitHooks
詳しくは[gitのドキュメント](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA-Git-%E3%83%95%E3%83%83%E3%82%AF)を見ていただきたいが、簡単に説明すると、gitでpushしたり、commitした時などに、あるスクリプトを実行することができる仕組みのことである。
.git/hooksの中に決められた名前のスクリプトを用意しておくと、実行することができる。
たとえば、.git/hooks/pre-commitというファイルを用意しておくと、commitメッセージが入力される前にpre-commitに書かれているスクリプトを実行することができる。
pre-commitフックに関するドキュメントの説明では、
> これは、いまからコミットされるスナップショットを検査したり、何かし忘れた事がないか確認したり、テストが実行できるか確認したり、何かしらコードを検査する目的で使用されます。

とあり、commit前の確認に利用することができる。その他にも、gitコマンドを利用する側(クライアント)だけでなく、gitでpushされる先(サーバーサイド)で役に立つHooksもあるようだが、ここでは割愛する。

今回は、
1. gitでcommitする前に、var_dumpや//DEBUGの文字列がcommitするファイルに含まれていないかを確認する
2. １で、もし含まれていたら、ユーザーへ確認の上、該当の行を削除する
3. commit メッセージを入力する画面に入る前に、処理を続行するか確認する
4. pushを実行する前に、現在のbranch名と親のbranch名を表示して確認する
5. 4の後に、親branchとの差分をdiffで表示してから、pushを続行するか確認する

の処理をそれぞれ、commitもしくはpushの際に行う。

# pre-commitで、commit前の確認と余計なDEBUG文の削除を行う
まず、commit前に確認を行うため、hooks/pre-commitに処理を記述していく。ここで、commit対象のファイルに問題がないかをチェックするためには、git addした(ステージングされた)ファイルを特定する必要がある。ここでは、以下のように、素直にgit statusの結果を"modified"と"new file"でgrepして取得している。

```bash
# addされたファイル一覧を取得
files=`git status|grep -e "modified" -e "new file"|sed "s/new file:\(.*\)/\1/g"|sed "s/modified:\(.*\)/\1/g"|cut -f 2`
```

次に各ファイル（ここでは*.phpに限定している）に対して、grepして、var_dumpや//DEBUGが含まれていないかを確認した。
もし含まれていたら、ユーザーに、`[warn] delete, continue? [y/N]`と削除してもいいか尋ねるようにしている。ここで、標準入力からyが入力されたら、sedのコマンドで、その行を削除する仕組みである。このとき、削除前のファイルがcommitされてしまうので、再度addしておく必要がある。もしy以外のものが入力されたら、`[error] delete abort.`と表示して処理を中断する。その後、同じような仕組みで、`[warn] commit, continue? [y/N]`と表示され、commitを中断したいときは中断することができるようになっている。以下に実装を載せておく。

```bash:hooks/pre-commit
# addされたファイル一覧を取得
files=`git status|grep -e "modified" -e "new file"|sed "s/new file:\(.*\)/\1/g"|sed "s/modified:\(.*\)/\1/g"|cut -f 2`
# それぞれのファイルをscanして、var_dump, //DEBUGを含む行を削除して、addし直してからcommitする
for file in $files
do
    echo name:$file;

    # PHPファイルのみ対象とする
    if [ ${file##*.}="php" ]; then
       echo check... var_dump, //DEBUG in $file;
       result=`less $file | grep -e "var_dump" -e "//DEBUG"`;

       if [ -z $result ]; then
           echo "OK\n";
       else
           echo $result;
           echo '[warn] delete, continue? [y/N]'
 
           exec < /dev/tty
           read answer
 
           case $answer in
               'y' | 'yes') sed -i "/var_dump/d" $file; sed -i "/\/\/DEBUG/d;" $file;;
                * ) echo '[error] delete abort.';exit 1;
            esac
            git add $file;
            echo $file;
        fi
    fi
done

echo '[warn] commit, continue? [y/N]';
exec < /dev/tty
read answer
 
case $answer in
    'y' | 'yes') echo 'OK.';exit 0;;
    * ) echo '[error] commit abort.'; exit 1;;
esac 
```

# pre-pushで、現在のbranch名と親ブランチとの差分を表示してからpushできるようにする
ここでは、リモートリポジトリへのpush前に、現在のbranch名と、その親ブランチ名を取得して、git diffを表示した。そのあとは、前述したcheck機構でpushの中断を可能にした。下に示したようにgit branchの*がついているbranch名を取得することで現在のbranchを取得している。他にも方法はありそうだが......
一方で、親ブランチ名の取得は、git show-branch --current で現在のbranchを含んで表示させ、一番上のbranch名を取得している。この親branch名の取得は、[ここ](https://qiita.com/upinetree/items/0b74b08b64442f0a89b9)を参考にしている。
以下にコード全体を示しておく。

```bash:hooks/pre-push
# 親branchと現在のbranchとの差分を表示
current_branch=`git branch|grep "*"|sed "s/* //g"`;
parent_branch=`git show-branch --current |head -n 1| sed "s/\[ \(.*\) \]/\1/g"|cut -d" " -f 2| sed "s/\[\(.*\)\]/\1/g"`;
git diff $parent_branch $current_branch;

# 現在のbranchのcommit logをonelineで表示
git log $current_branch --oneline;

# push確認
echo 'parent branch is '$parent_branch;
echo 'current branch is '$current_branch;
echo '[warn] push to remote, continue? [y/N]'

exec < /dev/tty
read answer

case $answer in
    'y' | 'yes') echo '[info] OK. push start.';;
    * ) echo '[error] push failed.';exit 1;;
esac
exit 0
```

# 設定
ホームディレクトリ直下に.git_template/hooksというディレクトリを作成し、その中に今回作成したHookのスクリプトを保存する。同じくホームディレクトリ直下にある.gitconfigにtemplatedirとして(以下のファイル参照)登録すれば、次回から、git initするたびに、自動的にhooksディレクトリ下のファイルがそれぞれの.git/hooks配下にコピーされて有効になるはずだ。また、現在のlocal repositoryに対して有効にするには、git initをもう一度おこなうだけでよい。[この記事](https://qiita.com/quattro_4/items/59fdf8b9aa9ef48ecbdf)によれば、
>
既存のプロジェクトの場合は.git_template/hooksの内容をそれぞれ反映させる必要があります。
再度git initを実行するとhooksが再インストールされます。ただし、すでに同ファイル名のものが存在する場合は上書きはされないようなので、

.git/hooks/のファイルを削除してからgit initする必要があるらしい。
少し前のことなのでうろ覚えだが、[こちら](https://qiita.com/noraworld/items/c562de68a627ae792c6c)を参考に初期設定を行なったと思うので参考にするといいと思う。

```bash:.gitconfig
[init]
        templatedir = ~/.git_template
```

# まとめ
git hooksを用いて、確認しながらcommitしてみた。branch情報の取得だったり、ubuntuで書いたsedを含むコードがmacではうまく動かなかったり([ここ](https://qiita.com/narumi_888/items/e425f29b84da6b72ad62)を参考に修正した)で、少し苦労したが、なんとかなってよかった。