pre-commit gemでgit commitに連動してRubocopを実施する

2015.07.08

丹内です。

gitのcommit直前に自動でrubocopによるスタイルチェックが行われるように設定します。

なぜスタイルチェックが必要なのか

GitFlowあるいはGitHub Flowにのっとり複数人で開発する場合、Pull Requestベースのコードレビューが重要です。

Pull Requestでは実装が仕様を満たしているか、クラスやメソッドの設計が適切か、ナレッジを共有できているか、といった観点から、議論を交わし、チームとしてコードを良くしていきます。

その際に、

  • 「1行の長さが長過ぎませんか?」
  • 「classとdefの間にスペースが開いています」
  • 「1メソッド内での分岐やメソッド呼び出しが多すぎます」
  • と言った、コーディング規約についての議論があると、下手をすると宗教戦争になり、生産性が高いとは言えません。

    事前にコーディング規約を決めていても、つい油断して規約違反のままコミットし、指摘を受けてしまうこともあるでしょう。

    これを防ぐため、スタイルチェックを自動で行うライブラリが、メジャーな各言語には存在します。

    Rubyの場合、それがrubocopです。

    コミットのたびにスタイルチェックを実施する理由

    設定したRubocopは、コミット前に手動で実行するか、あるいはCIにセットするなどの運用が考えられます。

    しかし、手動だと忘れるかもしれませんし、pushしてからCIでスタイルチェックに引っかかってまた修正をpushするというのも、勢いをそがれてしまい、個人的には不十分だと思います。

    そこで、git commitに連動してスタイルチェックを走らせる設定を行います。

    これにより、スタイルチェックを通過したコードのみがコミットされるため、コーディング規約の確実な運用が期待できます。

    Railsアプリにpre-commit gemを設定する

    サンプルのディレクトリを作成し、適当なクラスを作っておきます。

    $ mkdir rubocopsample
    $ cd rubocopsample; touch sample.rb
    $ git init
    $ rbenv exec bundle init

    例えば、sample.rbは以下のとおりです。

    class Sample
      def hoge
      end
    end

    次に、bundlerでpre-commit gemを導入し、初期設定を行います。

    gem 'pre-commit' と gem 'rubocop' の2行をGemfileに追記し、bundle installしてください。

    そして、pre-commitのファイルを生成します。

    $ rbenv exec bundle exec pre-commit install
    Installed /Users/tannaiyuki/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/pre-commit-0.23.0/templates/hooks/default to .git/hooks/pre-commit

    最後に、.git以下の設定ファイルを編集し、git commitコマンドに連動するようにします。

    .git/hooks/pre-commit というファイルを以下のように編集してください。

    #!/usr/bin/env sh
    
    # This hook has a focus on portability.
    # This hook will attempt to setup your environment before running checks.
    #
    # If you would like `pre-commit` to get out of your way and you are comfortable
    # setting up your own environment, you can install the manual hook using:
    #
    #     pre-commit install --manual
    #
    
    cmd=`git config pre-commit.ruby 2>/dev/null`
    if   test -n "${cmd}"
    then true
    elif which rvm   >/dev/null 2>/dev/null
    then cmd="rvm default do ruby"
    elif which rbenv >/dev/null 2>/dev/null
    then cmd="rbenv exec ruby"                <=== 修正前
    then cmd="rbenv exec bundle exec ruby"    <=== 修正後
    else cmd="ruby"
    fi
    
    export rvm_silence_path_mismatch_check_flag=1
    
    ${cmd} -rrubygems -e '
      begin
        require "pre-commit"
        true
      rescue LoadError => e
        $stderr.puts <<-MESSAGE
    pre-commit: WARNING: Skipping checks because: #{e}
    pre-commit: Did you set your Ruby version?
    MESSAGE
        false
      end and PreCommit.run
    '

    ここで、rubocopを動かしてみます。

    $ bundle exec rubocop
    Inspecting 2 files
    CC
    
    Offenses:
    
    Gemfile:2:8: C: Prefer single-quoted strings when you don't need string interpolation or special symbols.
    source "https://rubygems.org"
           ^^^^^^^^^^^^^^^^^^^^^^
    sample.rb:1:1: C: Missing top-level class documentation comment.
    class Sample
    ^^^^^
    
    2 files inspected, 2 offenses detected

    Gemfile内の、変数展開の無いダブルクォートが引っかかってしまいました。

    もちろんGemfileのダブルクォートをシングルクォートに変えても良いのですが、Gemfileはチェック対象から外すように設定してみましょう。

    .rubocop.ymlを作成し、以下の内容を記述します。

    AllCops:
      Exclude:
        - Gemfile

    これで再び実行してみます。

    $ bundle exec ubocop
    Inspecting 1 file
    C
    
    Offenses:
    
    sample.rb:1:1: C: Missing top-level class documentation comment.
    class Sample
    ^^^^^
    
    1 file inspected, 1 offense detected

    今度はクラスの上にコメントがないと言われました。

    修正を忘れたという前提で、このままコミットしてみましょう。

    ただ、その前に、pre-commitがrubocopを実行するように設定を行い、動作確認を行います。

    $ git config pre-commit.checks "rubocop"
    $ bundle exec pre-commit run all
    pre-commit: Stopping commit because of errors.
    Inspecting 1 file
    C
    
    Offenses:
    
    sample.rb:1:1: C: Missing top-level class documentation comment.
    class Sample
    ^^^^^
    
    1 file inspected, 1 offense detected
    
    pre-commit: You can bypass this check using `git commit -n`

    良さそうです。では、実際にコミットしてみます。

    $ git add .
    $ git status
    On branch master
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
            new file:   sample.rb
    
    $ git commit -m 'add sample'
    pre-commit: Stopping commit because of errors.
    Inspecting 1 file
    C
    
    Offenses:
    
    sample.rb:1:1: C: Missing top-level class documentation comment.
    class Sample
    ^^^^^
    
    1 file inspected, 1 offense detected
    
    pre-commit: You can bypass this check using `git commit -n`
    
    $ git status
    On branch master
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
            new file:   sample.rb

    このように、コミットしようとするとpre-commitでrubocopが走り、引っかかったらコミットが中断されるようになりました。

    pre-commitの設定ポリシーについて

    pre-commit gemはrubocop以外にも様々なhookを設定できます。

    $ bundle exec pre-commit list
    Available providers: default(0) git(10) git_old(11) yaml(20) env(30)
    Available checks   : before_all ci closure coffeelint common console_log csslint debugger gemfile_path go jshint jslint json local merge_conflict migration nb_space pry rails rspec_focus rubocop ruby ruby_symbol_hashrockets scss_lint tabs whitespace yaml
    Default   checks   : rubocop
    Enabled   checks   : rubocop
    Evaluated checks   : rubocop
    Default   warnings :
    Enabled   warnings :
    Evaluated warnings :

    もちろんRSpecも設定できるのですが、その場合はテストに通らなければコミットできない、ということになります。

    私はテストはpre-commit hookせず、rubocopだけhookするように設定しています。

    理由は、以下のとおりです。

  • テストに通らない段階でも、やむを得ずコミット&pushする場合がある(わからない時にコードを見てもらうため)
  • しかしrubocopのスタイルチェックは常にクリアして欲しいし、ABC Metricのように最低限のリファクタリングを促してくれるチェックは見ておきたい
  • まとめ

    pre-commit gemにより、git commitの前にrubocopによるスタイルチェックを行うよう設定し、通らなければコミットできないようにしました。

    プロジェクトの初期から設定しておくと、有意義なコードレビューができてとても良いのでオススメです。