🐶

localでreviewdog×rubocopしてみる

2023/02/27に公開

背景

reviewdog、便利ですよね。reviewdog製のGitHubActionsはたくさん用意されており、それらを使うことでPRを作成したタイミングでコードの差分に対してのみLintをかけたり、脆弱性診断をしたりできるので大変助かっています。中でも普段お世話になっているのは reviewdog/action-rubocop で、これをGitHubActionsで使って自分が書いたコードに対してのみrubocopをかけています。
https://github.com/reviewdog/action-rubocop

しかしコード量が多いリポジトリで開発していると、数分間に渡ってrubocopを実行することになるので結構待つことになります。GitHubActionsは実行時間が長いとその分お金もかかるので[1]、あまり長い時間動かしたくないのが正直な気持ちです。

モチベーション

またpushして数分間待ってPRを確認して見ると怒られて直して、、、みたいな感じになっています。理想はpushしたタイミングやcommitするタイミングで「お前の書いたコードのここ、rubocop違反やで」と怒ってほしいです。「いやpush前にrubocopかけろよ」と言われればそれまでなのですが、正直面倒ですよね。

なので GitHubActionsでやっていることをlocalでできたら実行結果を待たなくていいし、お金もかからないのでは? と思い色々試行錯誤してみました。

要件

要件を以下のように定義しました。

  1. commit時にrubocopを実行する
  2. 差分があるファイルのみrubocopを実行する
  3. 怒られるのは自分が変更した箇所のみ(元々存在していた違反については怒らない)

1. commit時にrubocopを実行する

パッと思いついたのは以下の2つの方法でした。

  • Gitのpre-commitフックを自前で定義して使う
  • overcommitlefthook などを使ってpre-commitフックを走らせる

チーム開発の場合、リポジトリの開発者全員にrubocopをちゃんと実行してほしいですよね。なので今回はGemを使って設定をGit管理させる方法を採用しました。今回は overcommit というGemを使ってpre-commitフックを発火させるようにしました。執筆時点ではovercommitはv0.59.1を使っています。

$ overcommit -v
overcommit 0.59.1

https://github.com/sds/overcommit

README.mdにある通りにセットアップしていきます。セットアップ手順はここでは割愛しますが、自分の場合は最終的にtopディレクトリに以下のような .overcommit.yml を作成しています。

.overcommit.yml
CommitMsg:
  ALL:
    enabled: false

PreCommit:
  CustomScript:
    enabled: true
    include:
      - '**/*.rake'
      - '**/*.rb'
      - '**/*.ru'
      - '**/Gemfile'
      - '**/Rakefile'
    required_executable: 'scripts/pre_commit.sh'

これでcommit時に指定したshellscript(scripts/pre_commit.sh)が実行されるようになりました。ここからはscripts/pre_commit.shを作成し、ここに実行したい処理を書いていきます。

2. 変更があったファイルのみrubocopを実行する

変更があったファイルのみrubocopを実行します。git diffで変更があったファイル名のみを出力し、xargsでrubocopコマンドに渡します。

pre-commit.sh
git diff --cached --name-only | xargs bundle exec rubocop

これで変更があったファイルのみrubocopが実行されるようになりました。

ここまでの実装でpre-commitフックが発動するかテストしてみます。例えば以下のような実装が元々あったとします。

波線がついている箇所はrubocop違反をしている箇所です。違反内容としては Style/MethodDefParentheses というもので、要するに引数を受け取るメソッドには () をつけましょうというものです。

ここに新しいメソッドを追加したとします。実装者は違反をせずに () をつけて新しいメソッドを追加しました。

ちゃんと () をつけているので自分が書いたコードに関してはrubocop違反はしていなさそうですね。commitしてみましょう。

元々存在していた、自分が実装していないコードに対して怒られてしまいました。

3. 怒られるのは自分が変更した箇所のみにする

このままではcommitできません。「いやいや、これ自分が書いたコードちゃうのに...」となってしまいモチベーションが下がってしまいますよね。これが1,2個の違反であれば大きな心で対応してあげよう、という気になるかもですが、その違反がたくさんある場合は疲れてしまいます。

なのでここでついにreviewdogの力を借ります。まずはreviewdogをlocalで動かせるようにします。homebrewでインストールする場合は以下。

$ brew install reviewdog/tap/reviewdog
$ reviewdog -version # => 0.14.1

あとは前述のrubocopの出力結果をreviwdogコマンドに渡してあげればokです。めっちゃ簡単ですね。reviewdogはfilter-modeなどのオプションがいくつかあるので、自分の開発環境に合うものを選択してみてください。今回自分は以下のように設定しました。

pre-commit.sh
git diff --cached --name-only \
  | xargs bundle exec rubocop \
  | reviewdog -f=rubocop -diff='git diff --cached' -filter-mode=added -fail-on-error

これで実行してみます。

今度は元々存在していた、自分が実装していないコード違反は無視することができました!

では次はワザと違反をしてみます。以下のように () を外してみました。

この状態でcommitしてみます。

自分が変更した箇所のみ怒られていますね、ちゃんと検知できていそうです。

これで

  1. commit時にrubocopを実行する
  2. 変更があったファイルのみrubocopを実行する
  3. 怒られるのは自分が変更した箇所のみ

ができるようになりました🎉 これでCIの結果を待ってその都度直して、ということをしなくても良くなりますね。

しかしこのままではlocalにreviewdogが入っていない開発者がcommitした場合はreviewdogコマンが実行できないためエラーになってしまいます。なのでreviewdogがinstallされていない開発者がcommitした場合はrubocopを実行せずに終了するようにしました。

pre-commit.sh
# localにreveiwdogが存在しない場合, rubocopを実行せずに終了する.
if [ -z $(reviewdog -version) ]; then exit 0; fi

git diff --cached --name-only \
  | xargs bundle exec rubocop \
  | reviewdog -f=rubocop -diff='git diff --cached' -filter-mode=added -fail-on-error

pre-commitフックを走らせたくない場合

場合によってがpre-commitフックを走らせたくないときもあると思います。例えば障害対応などですぐにコードを修正してリリースしたい時など、多少のLintエラーは許してほしい場合もあります。そんな時は環境変数に OVERCOMMIT_DISABLE という環境変数をセットするとpre-commitフックが発動しなくなります[2]

$ OVERCOMMIT_DISABLE=1 git commit -m '...'

まとめ

これでコードの変更部分に対してだけrubocopをかけられるようになりました。そのためCIで怒られることがなくなったり、そもそもCIを廃止してコスト削減できるようになったかと思います。

また今回はrubocopを例にlocalで差分実行を実装してみましたが、将来的にはスペルチェックや脆弱性解析なども同じように実装しようかと思います。

脚注
  1. publicリポジトリは無料です。詳しくは GitHubActionsの課金について を参照してください。 ↩︎

  2. https://github.com/sds/overcommit#disabling-overcommit ↩︎

Discussion