概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Lefthook: Knock your team’s code back into shape — Martian Chronicles, Evil Martians’ team blog
- 原文公開日: 2019/07/30
- 著者: Abroskin Alexander、Andy Barnov
- サイト: Evil Martians — ニューヨークやロシアを中心に拠点を構えるRuby on Rails開発会社です。良質のブログ記事を多数公開し、多くのgemのスポンサーでもあります。
※日本語タイトルは内容に即したものにしました。画像は元記事からの引用です。
まえがき
最速を誇るポリグロットGitフックマネージャ、Lefthookの登場です。自由すぎて手に負えないコードをproductionレベルに持っていきましょう。Lefthookのインストールは驚くほど簡単で、最近だとDiscourseやLogux、Openstaxも採用しています。ほとんどのフロントエンド環境やバックエンド環境で、チームの開発者全員が単一の柔軟なツールを頼りにできるようになります。Lefthookにはこんな絵文字もあります。
訳注: ポリグロット(polyglot)は本来「数か国語を話せる人」といった意味ですが、本記事では言語やフレームワークや環境などを複数扱えるという意味で捉えています。
Lefthookでチームのコードをゼロコンフィグで引き締めよう(翻訳)
かつて、数百万人ものユーザーが頼りにしている単独のソフトウェアといえば、象牙の塔にこもった1人の開発者が作り上げるものでしたが、それも今や遠い昔の話です。GitですらLinux Torvaldsが一人で独創性を発揮してこしらえたものであると広く信じられていますが、これもやはり多くのコントリビュータの助けがあってのことであり、何十人ものチームによって今もメンテナンスされています。
あなたが世界を股にかけたオープンソースのプロジェクトに取り組んでいようと、プロプライエタリな商用ソフトウェアの閉じた庭園で花を咲かせていようと、チームで仕事をしていることに変わりはありません。そして、プルリクやコードレビューがどれほど巧妙に組み上げられていようと、多くのコントリビュータが参加している巨大なコードベース全体に渡ってコードの品質を担保するのは、容易なことではありません。
フックを一発お見舞い
Gitフックは、コミットやプッシュといった特定の重要な操作が発生したときにカスタムスクリプトを発動させるしくみのことであり、Gitに組み込まれています。あなたが、Bashの操作と世界で最も広く使われているバージョンコントロールシステム(つまりGit)の内部に通じているのであれば、ツールをまったく追加せずにやれます。./.git/hooks/pre-commit
を編集し、形式の整った手頃なスクリプトを配置すれば、たとえばコミットする前にファイルにlintをかけたりできるようになります。
しかし、今あなたが携わっているプロジェクトでの関心事は、もっぱらプロジェクトのコードを書くことであり、コードをチェックするコードの方ではないでしょう。現代のWeb開発ツールの世界にはそれこそ何でも揃っていますが、このように無数のツールが存在する理由はたったひとつ、オーバーヘッドと複雑性を削減することです。Gitフックも例外ではありません。JavaScriptのコミュニティで好んで使われる武器といえば、HuskyにWebpackやBabelを組み合わせるとか、Nodeベースのツールに依存するcreate-react-appです。しかしRailsを中心に据えるバックエンド世界はというと、Ruby gemとして提供されるOvercommitにほぼ支配されています。
Lefthookとその他のツールの詳しい比較については、プロジェクトのWikiをご覧ください。
どちらのツールもそれぞれよくできていますが、Evil Martiansのようにフロントエンドとバックエンドの混成チームともなると、フロントエンドのlintとバックエンドのlintがRubyとJavaScriptに分断され、それぞれ独自の方法でlintをかけるはめになることもしょっちゅうです。
Lefthookなら同じことを二度も考える必要はもうありません。Lefthookは単独のGoバイナリで、JavaScriptとRubyそれぞれのラッパーを内包しています。しかもその他の環境でも単独のツールとして使えます。
ポイント: Lefthookはほとんどのユースケースでセットアップなしでやれます。
LefthookはGo言語のおかげで驚異的な速さを誇り、すぐに使えるスクリプトのコンカレント実行をサポートします。実行ファイルが単独のマシンコードバイナリなので、外部の依存性にわずらわされることもなくなります(Husky + lint-stagedでは500個ほどの依存ファイルがnode_modules
に追加されます)。しかも、開発環境が更新されるたびに依存ファイルを再インストールするという頭の痛い作業からも解放されるのです(試しにグローバルにインストールされたgemを別バージョンのRubyで動かしてみればわかります)。
プロジェクトのルートディレクトリ(後述の例を参照)のpackage.json
とGemfile
のいずれか、そしてlefthook.yml
をLefthookに認識させればこれらのツールがインストールされ、次回のgit pull
、次回のyarn install
や bundle install
、次回の git add
やgit 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の設定はずっと明示的な書式になっていることがわかります。
「第1ラウンド」
lefthook
バイナリがシステムのどこか(ローカルでもグローバルでもよい)にインストールされていて、プロジェクトのルートディレクトリにlefthook.yml
が配置されていれば、Lefthookが機能するのに必要なものはすべて揃います。
Lefthookのバイナリはグローバルにインストールする(macOSならHomebrew、Ubuntuならsnappy、Arch LinuxならAUR、Go言語のgo get
を使えばどの環境でもOKです)ことも、RubyのGemfile
やNode.jsのpackage.json
で開発用の依存関係リストに加えることもできます。
Lefthookをプロジェクトで最初に設定する場合は、好みに応じていくつかのオプションの中から選択する必要があります。
Gemfile
にlefthook
を追加したり@arkweid/lefthook
をpackage.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フロントエンド開発(3)Webpackの詳細、ActionCableの実装とHerokuへのデプロイ(翻訳)
このプロジェクトではLefthookでpre-commit
フックを設定することでJavaScriptファイルやCSSファイルをPrettierで整形し、ESlintとstylelintで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に食わせてみます。
「奴のパンチをかわせ」
ここでは、ワークフローに柔軟性をもたらすLefthookがライバルを上回っている機能を簡単にまとめます。完全なリストについては完全版ガイドをご覧ください。
スピード
Lefthookはコンピュータ(つまりCIサーバーも)の並列性を一滴も余さず活用します。そのために必要な設定はparallel: true
だけです。
以下はさまざまな種類のlintコマンドを記述する設定ファイルであり、コマンドラインでlefthook run lint
を入力すれば実行できます。これらは、DiscourseがTravisで使っているものと同じです。
Lefthookは、このようなカスタムタスクを実行することもできます。pre-commit
、pre-push
、post-checkout
、post-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ワークフローを自動化する戦いが終わる前に、決して、決してタオルを投げないでください!