Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Lefthook: 多機能Gitフックマネージャ

概要

原著者の許諾を得て翻訳・公開いたします。

※日本語タイトルは内容に即したものにしました。画像は元記事からの引用です。

以下は続編記事です。

参考: Lefthook: refactoring the Git Hooks automation tool back into shape—Martian Chronicles, Evil Martians’ team blog

まえがき

最速を誇るポリグロットGitフックマネージャ、Lefthookの登場です。自由すぎて手に負えないコードをproductionレベルに持っていきましょう。Lefthookのインストールは驚くほど簡単で、最近だとDiscourseLoguxOpenstaxも採用しています。ほとんどのフロントエンド環境やバックエンド環境で、チームの開発者全員が単一の柔軟なツールを頼りにできるようになります。Lefthookにはこんな絵文字🥊もあります。

訳注: ポリグロット(polyglot)は本来「数か国語を話せる人」といった意味ですが、本記事では言語やフレームワークや環境などを複数扱えるという意味で捉えています。

Lefthook: 多機能Gitフックマネージャ

かつて、数百万人ものユーザーが頼りにしている単独のソフトウェアといえば、象牙の塔にこもった1人の開発者が作り上げるものでしたが、それも今や遠い昔の話です。GitですらLinux Torvaldsが一人で独創性を発揮してこしらえたものであると広く信じられていますが、これもやはり多くのコントリビュータの助けがあってのことであり、何十人ものチームによって今もメンテナンスされています。

あなたが世界を股にかけたオープンソースのプロジェクトに取り組んでいようと、プロプライエタリな商用ソフトウェアの閉じた庭園で花を咲かせていようと、チームで仕事をしていることに変わりはありません。そして、プルリクやコードレビューがどれほど巧妙に組み上げられていようと、多くのコントリビュータが参加している巨大なコードベース全体に渡ってコードの品質を担保するのは、容易なことではありません。

フックを一発お見舞い

Gitフックは、コミットやプッシュといった特定の重要な操作が発生したときにカスタムスクリプトを発動させるしくみのことであり、Gitに組み込まれています。あなたが、Bashの操作と世界で最も広く使われているバージョンコントロールシステム(つまりGit)の内部に通じているのであれば、ツールをまったく追加せずにやれます。./.git/hooks/pre-commitを編集し、形式の整った手頃なスクリプトを配置すれば、たとえばコミットする前にファイルにlintをかけたりできるようになります。

しかし、今あなたが携わっているプロジェクトでの関心事は、もっぱらプロジェクトのコードを書くことであり、コードをチェックするコードの方ではないでしょう。現代のWeb開発ツールの世界にはそれこそ何でも揃っていますが、このように無数のツールが存在する理由はたったひとつ、オーバーヘッドと複雑性を削減することです。Gitフックも例外ではありません。JavaScriptのコミュニティで好んで使われる武器といえば、HuskyWebpackBabelを組み合わせるとか、Nodeベースのツールに依存するcreate-react-appです。しかしRailsを中心に据えるバックエンド世界はというと、Ruby gemとして提供されるOvercommitにほぼ支配されています。

Lefthookとその他のツールの詳しい比較については、プロジェクトのWikiをご覧ください。

どちらのツールもそれぞれよくできていますが、Evil Martiansのようにフロントエンドとバックエンドの混成チームともなると、フロントエンドのlintとバックエンドのlintがRubyとJavaScriptに分断され、それぞれ独自の方法でlintをかけるはめになることもしょっちゅうです。

Lefthookなら同じことを二度も考える必要はもうありません。Lefthookは単独のGoバイナリで、JavaScriptRubyそれぞれのラッパーを内包しています。しかもその他の環境でも単独のツールとして使えます。

ポイント: Lefthookはほとんどのユースケースでセットアップなしでやれます。

LefthookはGo言語のおかげで驚異的な速さを誇り、すぐに使えるスクリプトのコンカレント実行をサポートします。実行ファイルが単独のマシンコードバイナリなので、外部の依存性にわずらわされることもなくなります(Husky + lint-stagedでは500個ほどの依存ファイルがnode_modulesに追加されます)。しかも、開発環境が更新されるたびに依存ファイルを再インストールするという頭の痛い作業からも解放されるのです(試しにグローバルにインストールされたgemを別バージョンのRubyで動かしてみればわかります)。

プロジェクトのルートディレクトリ(後述の例を参照)のpackage.jsonGemfileのいずれか、そしてlefthook.ymlをLefthookに認識させればこれらのツールがインストールされ、次回のgit pull、次回のyarn installや bundle install、次回の git addgit commitで自動的にコードに対して実行されます。プロジェクトに新たに参加するコントリビュータを一切わずらわせません。

詳細番のREADMEに、あらゆる利用シナリオが記載されています。設定の構文は素直で、Lefthookによって実際に実行されるコマンドを隠蔽しません。「うっかり金的攻撃」のようなハプニングもなくなります。

Discourseのハートにもクリーンヒット

Discourseといえば、フォーラムスタイルの議論を行えるオープンソースプラットフォームとしてとても有名ですが、先ごろ不退転の決意でOvercommitからLefthookに完全に移行しました。700人近いコントリビューターが34,000件ものコミットを扱うので、新しいコントリビューションすべてにlintをかけるのは優先順位が高くなります。しかしOvercommitの場合だと、チームのメンバーが新規参入者に必要なツールをインストールするよう毎回リマインドしなければなりませんでした。

@arkweid/lefthookがプロジェクトのpackage.jsonでめでたくdev dependencyとなったことにより、新規コントリビューターのセットアップは不要になりました。

ポイント: Lefthookを使うことで、localhostで実行されるpre-commitスクリプトの実行時間が半分になります。

Discourseのプルリク#7826では、必要なGitフックマネージャが.overcommit.ymlからlefthook.ymlに変更されました。両者の設定を比較してみれば、Overcommitの設定の多くがプラグインのマジックに依存していますが、Lefthookの設定はずっと明示的な書式になっていることがわかります。

DiscourseでLefthookを導入する前と後のCI出力
単に出力方法が変わっただけではありません。Lefthookが行ったことすべてがいい感じにサマリーとして出力されています。Lefthookを使うことで、localhostで実行されるpre-commitスクリプトの実行時間が半分になり、CIの実行速度が20%増加します(パラレル実行のサポートがより手厚いCI環境であればさらによい結果になるでしょう)。

うれしいボーナス

「第1ラウンド」

lefthookバイナリがシステムのどこか(ローカルでもグローバルでもよい)にインストールされていて、プロジェクトのルートディレクトリにlefthook.ymlが配置されていれば、Lefthookが機能するのに必要なものはすべて揃います。

Lefthookのバイナリはグローバルにインストールする(macOSならHomebrew、Ubuntuならsnappy、Arch LinuxならAUR、Go言語のgo getを使えばどの環境でもOKです)ことも、RubyのGemfileやNode.jsのpackage.jsonで開発用の依存関係リストに加えることもできます。

Lefthookをプロジェクトで最初に設定する場合は、好みに応じていくつかのオプションの中から選択する必要があります。

Gemfilelefthookを追加したり@arkweid/lefthookpackage.jsonに追加する場合の主なメリットは、コントリビューターのシステムレベルにlefthookをインストールしなくても済むようにできることです。bundle installまたはyarn installを実行すればバイナリが配置されます。

lefthookをシステムレベルでインストールした後で、プロジェクトのルートディレクトリでlefthook installを実行してlefthook.ymlを生成し、Lefthookのリポジトリで構文の使い方などを学びます。完全なサンプルlefthook.ymlはここにあります。

pre-commitのたびに行うアクションを記述するコードの例は以下のような感じになります(pre-commitは、git commit -m "new feature"を入力した直後、かつそれがコミットされる直前のタイミングとなります)。

pre-commit:
  commands:
    stylelint:
      tags: frontend style
      glob: "*.{js}"
      run: yarn stylelint {staged_files}
    rubocop:
      tags: backend style
      glob: "*.{rb}"
      exclude: "application.rb|routes.rb"
      run: bundle exec rubocop {all_files}
  scripts:
    "good_job.js":
      runner: node

続いてlefthook.ymlをリポジトリにコミットしてお気に入りのリモートリポジトリにプッシュすればあら不思議、チームのどのメンバーもコードをコミットするたびに動くようになります。

もっと詳しく

訳注: 上の見出しの「blow by blow」は「詳しく説明する」というイディオムで、「ボクシングの1打1打を中継する」が起源です。

Lefthookをデモプロジェクトで急いでチェックしたい方にはevil_chatリポジトリをgit cloneすることをおすすめします。このリポジトリは、Evil Martiansの以下の人気記事に関連して構築したプロジェクトです。

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

新しいRailsフロントエンド開発(2)コンポーネントベースでアプリを書く(翻訳)

新しいRailsフロントエンド開発(3)Webpackの詳細、ActionCableの実装とHerokuへのデプロイ(翻訳)

このプロジェクトではLefthookでpre-commitフックを設定することでJavaScriptファイルやCSSファイルをPrettierで整形し、ESlintstylelintでlintをかけています。

Lefthookが動くところをざっと見てみましょう。まず、上のリポジトリをgit cloneしてパッケージマネージャを実行します。

$ git clone git@github.com:demiazz/evil_chat.git
$ bundle && yarn

それでは.pcssファイルや.jsファイルを適当にぶっ壊してみてください。

$ git add . && git commit -m "我は世界を滅ぼすものなり"

後は待つだけ!

うまくいけば(=うまくコードをぶっ壊せば)以下が表示されるでしょう。

ダメなコミット
失敗が発生したスクリプトの出力にはボクシンググローブの絵文字が表示されます。あなたに本当の左フックを食らわして注意を引きつけるわけです。

今のlint出力はアプリのフロントエンド部分しかカバーしていません。皆さんご存知のRubocop出力も追加してみたらどうなるでしょうか?

lefthook.ymlを編集して以下を追加しましょう。

# lefthook.yml

pre-commit:
  parallel: true # tell Lefthook to utilise all cores
  commands:
    js:
      glob: "*.js"
      run: yarn prettier --write {staged_files} && yarn eslint {staged_files} && git add {staged_files}
    css:
      glob: "*.{css,pcss}"
      run: yarn prettier --write {staged_files} && yarn stylelint --fix {staged_files} && git add {staged_files}
    # Add these lines
    rubocop:
      glob: "*.{rb}"
      run: rubocop {staged_files} --parallel

なお{staged_files}は、現在のコミットでstagedになっているファイルだけを対象にする便利なショートカットです。

それではさっきぶっ壊したJSファイルやCSSファイルを元に戻し、続いて適当なRubyファイルに「許されないスタイルを含むコミット」をかましてみましょう(私たちが許しますので存分にやってください)。適当なコミットメッセージを付けて、先ほどとは違うファイルタイプの変更をgitに食わせてみます。

おぉっとRubocopの乱入です!

おぉっとRubocopの乱入です!
今度はCSSとJSは問題なしとなり、Rubyの部分にチェックが必要となりました。痛烈な左フックを受けました!

「奴のパンチをかわせ」

ここでは、ワークフローに柔軟性をもたらすLefthookがライバルを上回っている機能を簡単にまとめます。完全なリストについては完全版ガイドをご覧ください。

スピード

Lefthookはコンピュータ(つまりCIサーバーも)の並列性を一滴も余さず活用します。そのために必要な設定はparallel: trueだけです。

以下はさまざまな種類のlintコマンドを記述する設定ファイルであり、コマンドラインでlefthook run lintを入力すれば実行できます。これらは、DiscourseがTravisで使っているものと同じです。

Lefthookは、このようなカスタムタスクを実行することもできます。pre-commitpre-pushpost-checkoutpost-mergeなど、Gitフックで利用可能などのフックにも同じコマンドを設定できます。

# lefthook.yml

lint:
  # parallel: true
  commands:
    rubocop:
      run: bundle exec rubocop --parallel
    prettier:
      run: yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"
    eslint-assets:
      run: yarn eslint --ext .es6 app/assets/javascripts
    eslint-test:
      run: yarn eslint --ext .es6 test/javascripts
    eslint-plugins-assets:
      run: yarn eslint --ext .es6 plugins/**/assets/javascripts
    eslint-plugins-test:
      run: yarn eslint --ext .es6 plugins/**/test/javascripts
    eslint-assets-tests:
      run: yarn eslint app/assets/javascripts test/javascripts

自分のシステムではparallel: trueをコメントアウトしているので、このタスクの実行には30秒そこそこかかります。

ポイント: このparallel: trueをオンにすると15.5秒で終わります -- 倍速です!

柔軟性

  • ダイレクトコントロール

Gitアクションを待たずにフックを直接実行したい場合は以下を使います。

$ lefthook run pre-commit
  • 柔軟なファイル指定

{staged_files}{all_files}といった組み込みのショートカットを使うことも、特定のファイルを選択する独自のリストを定義することもできます。

pre-commit:
  commands:
    frontend-linter:
      run: yarn eslint {staged_files}
    backend-linter:
      run: bundle exec rubocop {all_files}
    frontend-style:
      files: git diff --name-only HEAD @{push}
      run: yarn stylelint {files}
  • ワイルドカードや正規表現によるフィルタ

ファイルリストをその場でワイルドカード(glob)や正規表現でフィルタする場合は以下のようにします。

pre-commit:
  commands:
    backend-linter:
      glob: "*.{rb}" # glob filter
      exclude: "application.rb|routes.rb" # regexp filter
      run: bundle exec rubocop {all_files}
  • 独自スクリプトの実行

ワンライナーで足りなくなったら、Lefthookでカスタムスクリプトを実行することもできます。

commit-msg:
  scripts:
    "good_job":
      runner: bash
  • タグ付けやローカル設定でさらに柔軟に

タスクをタグでグループ化すれば、フックをローカル実行するときにグループを除外できます(バックエンド開発者なのでフロントエンド用のタスクを回したくない、など)。

Lefthookではプロジェクトのルートディレクトリにlefthook-local.ymlを作成して、メインのlefthook.ymlにあるあらゆる設定をオーバーライドできます(lefthook-local.yml.gitignoreに追加しておくことをお忘れなく!)。こうすることで、次のように別の一連のコマンドにタグを割り当てられます。

# lefthook.yml

pre-push:
  commands:
    stylelint:
      tags: frontend-style # a tag
      files: git diff --name-only master
      glob: "*.{js}"
      run: yarn stylelint {files}
    rubocop:
      tags: backend-style # a tag
      files: git diff --name-only master
      glob: "*.{rb}"
      run: bundle exec rubocop {files}

次のようにローカルでの実行から除外することもできます。

# lefthook-local.yml

pre-push:
  exlude_tags:
    - frontend-style

そしてK.O.

皆さんはローカル開発でDockerを使っていますか?既に使っている方はもちろん、そうでない方にとってもチームでDockerをローカル開発に使うまたとないチャンスです。開発作業全体を完全にDockerコンテナに閉じ込めるのが好みの人もいますし、ローカル環境をキレイに整頓する目的でDockerを用いるのが好みの人もいます。

あなたがメインで使うlefthook.ymlに以下が含まれているとします。

post-push:
  scripts:
    "good_job.js":
      runner: bash

さて、これと同じタスクをDockerコンテナの中でも実行したい、しかし他の人のセットアップをぶっ壊したくないとしたらどうしますか?そこで、バージョン管理システムにチェックインしないようにしたlefthook-local.ymlファイルを用いれば、このコマンドを{cmd}ショートカットで次のようにちょっぴり改変してローカル専用のセットアップにできます。

# lefthook-local.yml

pre-commit:
  scripts:
    "good_job.js":
      runner: docker exec -it --rm <container_id_or_name> {cmd}

{cmd}は、メインの設定のコマンドで置き換えられます。

その結果、コマンドは以下のような感じになります。

docker exec -it --rm <container_id_or_name> node good_job.js

エイト、ナイン、テン...ノックアウトです🥊!


私たちは、Lefthookが銀河系で最速かつ最も柔軟性に富んだGitフックマネージャであると自負しています。皆さんにもぜひ、Discourseの事例に学んでLefthookを業務プロジェクトに追加するなり、自分の愛するオープンソースリポジトリにプルリクを送るなりしていただければと思います。

Lefthookは本質的にポリグロットなので、「純粋なフロントエンド開発チーム」「純粋なバックエンド開発チーム」「フロントエンドとバックエンドの混成フルスタックチーム」を問わず利用できる開発用セットアップであり、Windowsなどのあらゆる主要なOS環境で共通に使えます。

最適なインストール方法を皆さんの要件に応じてお探しいただけます。ぜひお試しください!私たちがLefthookをCrystalballと組み合わせて回している商用プロジェクトについては、Dev.toのこちらの記事でお読みいただけます。

Lefthookを見て「また絵に書いたような車輪の再発明かよ?」とうんざりしかかっている方も、ぜひお試しください。Lefthookが単なる新手のジェットパックではなく、Gitフック管理におけるもうひとつの「古き良き車輪」であることにお気づきいただけるかと思います。

GitやGitHubワークフローを自動化する戦いが終わる前に、決して、決してタオルを投げないでください!

関連記事

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(更新翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。