Gitフックを仕込むgrunt-githooksについて紹介するよ

# 本投稿について
Gruntプラグインを紹介していくGrunt Plugins Advent Calendar 2013の12/1の投稿です。
さっき作ったばかりなので、まだすっきりさっぱりしたカレンダーになってますので、どうぞよろしくお願いします。
http://qiita.com/advent-calendar/2013/grunt-plugins

# grunt-githooksとは
GitフックでGruntタスクを実行する仕込みをしてくれるGruntプラグインです。

https://npmjs.org/package/grunt-githooks
https://github.com/rhumaric/grunt-githooks

# Gitフックとは
Gitではcommitやpush, rebase, checkoutなどいくつかの操作ができますが、それらの特定の操作を行ったときに、スクリプトを実行することができる仕組みがあり、それをGitフックと呼びます。

## どんな種類のフックがあるのか
まずはソースコードをGitバージョン管理するために、`git init`で初期化してください。
`% git init`
すると、カレントディレクトリに`.git`ディレクトリが作られますが、その中の`.git/hooks`ディレクトリにGitのクライアントサイドフックのサンプルが提供されています。

```
% tree -L 3 -a
.
|-- .git
|   |-- HEAD
|   |-- config
|   |-- description
|   |-- hooks
|   |   |-- applypatch-msg.sample
|   |   |-- commit-msg.sample
|   |   |-- post-update.sample
|   |   |-- pre-applypatch.sample
|   |   |-- pre-commit.sample
|   |   |-- pre-push.sample
|   |   |-- pre-rebase.sample
|   |   |-- prepare-commit-msg.sample
|   |   `-- update.sample
|   |-- info
|   |   `-- exclude
|   |-- objects
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       `— tags
```

ファイル名を見ると想像できるように、`pre-commit.sample`は`commitする直前にフックされるスクリプト`のサンプル、`pre-push.sample`は`pushする直前にフックされるスクリプト`のサンプル(Git 1.8.2以降対応)になります。
`pre-commit`や`pre-push`という`.sample`を除いた名前のファイルを作ると、`commit`や`push`の各操作から呼ばれます。

# 導入
## インストール
npm installでgrunt-githooksをインストールします。package.jsonのdevDependenciesに追加されるように`--save-dev`オプションを忘れずに。
`% npm install grunt-githooks --save-dev`

## Gruntfile.js
`grunt-githooks` タスクをロードするように追記し、githooksタスクを定義します。
以下はシェルスクリプトのテンプレートを使ったタスク定義の例です。
他にも`template`にはNodeで定義できる`node_modules/grunt-githooks/templates/node.js.hb`テンプレートを指定可能です。

```
module.exports = function(grunt) {
  grunt.initConfig({
    githooks: {
      options: {
        dest: '.git/hooks', //省略可
        hashbang: '#!/bin/sh',
        template: './node_modules/grunt-githooks/templates/shell.hb',
        startMarker: '## GRUNT-GRUNTHOOKS START',
        endMarker: '## GRUNT-GRUNTHOOKS END'
      },
      setup: {
        'pre-commit': 'git-pre-commit'
      }
    }
  });
  grunt.registerTask('git-pre-commit', [“jshint"]); // jshintタスクの定義は省略
  grunt.loadNpmTasks('grunt-githooks');
};  
```

# 実行

```
% grunt githooks
Running "githooks:setup" (githooks) task

Binding `git-pre-commit` to `pre-commit` Git hook.
OK

Done, without errors.


% tree -L 3 -a
.
|-- .git
|   |-- HEAD
|   |-- config
|   |-- description
|   |-- hooks
|   |   |-- applypatch-msg.sample
|   |   |-- commit-msg.sample
|   |   |-- post-update.sample
|   |   |-- pre-applypatch.sample
|   |   |-- pre-commit
|   |   |-- pre-commit.sample
|   |   |-- pre-push.sample
|   |   |-- pre-rebase.sample
|   |   |-- prepare-commit-msg.sample
|   |   `-- update.sample
|   |-- info
|   |   `-- exclude
|   |-- objects
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       `— tags
```

すると`.git/hooks/pre-commit` ファイルが生成されています。

```
% less .git/hooks/pre-commit
#!/bin/sh
## GRUNT-GRUNTHOOKS START
(cd /Users/shoito/workspaces/gac2013 && grunt git-pre-commit)

## GRUNT-GRUNTHOOKS END
```

pre-commitフックが仕込まれたので、さっそく`git commit`して動作確認してみます。

```
% git commit
Running "jshint:gruntfile" (jshint) task
>> 1 file lint free.

Running "jshint:lib_test" (jshint) task
Linting lib/htmlparser.js ...ERROR
[L65:C35] W116: Expected '===' and instead saw '=='.
        if ( html.indexOf("<!--") == 0 ) {
[L70:C15] W116: Expected '{' and instead saw 'handler'.

              handler.comment( html.substring( 4, index ) );

(略)
Warning: Task "jshint:lib_test" failed. Use --force to continue.


Aborted due to warnings.
```

このケースでは、`git commit`したがpre-commitフックで呼ばれるGruntタスクの`jshint`タスクによるコーディングルールのチェックをパスできず、`git commit`が失敗に終わりました。
警告に従い、コードを修正した後に再度`git commit`しましょう。

# まとめ 
このように、grunt-githooksプラグインを使い、チーム内で共通のGitフックを仕込んでおくことができます。
例えば、pre-commitフックにコーディングルールのチェックやテストのGruntタスクが呼ばれるように仕込んでおくと「誰だよ、テスト通ってないコードをpushしたやつは！？」という事態が防止できます。

# Tips
コーディングルールのチェックやテスト(軽いもの)のGruntタスクの実行はファイル変更の監視を行っているwatchタスクに仕込んでおいて、PhantomJS/CasperJSなどを使ったUIテストのタスク(重いもの)はpre-commitフックから呼び出すようにしておくなどすると良いと思います。
 
# Q&A
Q. なぜ、githooksタスクからわざわざ`git-pre-commit`を呼ぶようにしているか？

```
    setup: {
      'pre-commit': 'git-pre-commit'
    } 
(略)
grunt.registerTask('git-pre-commit', ['jshint']); 
```

A. このようにしておくと、pre-commitタスクで実行したいGruntタスクに変更があっても、`Gruntfile.js`の定義を更新すればよく、`grunt githooks`を実行して`.git/hooks/pre-commit`を更新する必要がないためです。この例の`jshint`だと軽すぎますが、うちでは`casperjs`が含まれた定義になってます。

#Appendix
Grunt Plugins
http://gruntjs.com/plugins