世界を動かす技術を、日本語で。

プリコミットフックが壊れています

2025年12月27日原文(jyn.dev)

概要

  • Rustプロジェクトの開始から pre-commitフック の作成までの流れ
  • pre-commitフック が抱える根本的な問題点の指摘
  • pre-pushフック の推奨理由とその運用ポイント
  • 具体的な フックスクリプト の改良例
  • フック導入時の 注意点とベストプラクティス

Rustプロジェクトの初期セットアップとフォーマット管理

  • 新規ディレクトリ best-fizzbuzz-ever 作成、 main.rs ファイル作成
  • Gitリポジトリ の初期化と main.rs のコミット
  • 他言語FizzBuzz集への追加時、 コードフォーマット統一 の要求
  • rustfmt によるフォーマットチェック用 pre-commitフック 導入
    • 例:
      #!/bin/sh
      set -eu
      for f in *.rs; do rustfmt --check "$f"; done
      
  • ステージングされていない変更を フックで検出できない 問題発生

pre-commitフックの改良と限界

  • インデックス上のファイルを 一時ディレクトリ に展開しチェックする方式へ改良
    • 例:
      tmpdir=$(mktemp -d --tmpdir "$(basename "$(realpath .)")-pre-commit.XXXX")
      trap 'rm -r "$tmpdir"' EXIT
      git checkout-index --all --prefix="$tmpdir/"
      for f in $tmpdir/*.rs; do rustfmt --check "$f"; done
      
  • 変更ファイルのみ チェックするようさらに改良
    • 例:
      files=$(git diff --name-only --cached --no-ext-diff --diff-filter=d)
      ...
      
  • 既存コードが未整形 だとコミットできない問題
  • rebase時Rustファイルがないコミット でフックが失敗するケース

pre-commitフックの根本的な問題

  • 他人のブランチ古いフック には適用できない
  • rebasemerge 時に意図せずフックが走る
  • コミット単位 での品質担保が困難
  • CIでの検証 と異なり、 ローカルのみ で完結してしまう
  • 作業効率低下ワークフロー破壊 のリスク
  • 作業ツリー全体 のチェックは大規模リポジトリで非現実的

pre-pushフック推奨理由と運用のポイント

  • pre-pushフック なら、 push前 にまとめてチェック可能
  • インデックス上 のファイルを対象にすることで、 一貫性確保
  • 高速・信頼性重視のチェック のみ実装推奨
    • ネットワークアクセスやビルド必須のチェックは非推奨
  • 静かな出力 でユーザー体験向上
  • 自動セットアップ禁止手動設定手順をドキュメント化
  • コミット前フック は機密情報(例: 認証情報)流出防止用途に限定

pre-pushフック作成時の具体的アドバイス

  • インデックス上 のファイルでのみチェック実施
  • 変更ファイル検出 は信頼できないため、 明示的なファイル指定 が望ましい
  • CONTRIBUTING.md 等に 手動導入手順 を明記
  • pre-commitフック は原則使用しない方針

まとめ

  • pre-commitフック は多くの欠点があり、 pre-pushフック の使用が推奨
  • 品質担保 はCIやpre-pushフックで実施
  • 認証情報漏洩防止 以外でpre-commitフックは避けるべき
  • 運用ドキュメント の整備が重要

Hackerたちの意見

これは本当に面白い読み物だった。開発者のためにプレコミットワークフローを設定している人や、すでに維持している人にはぜひおすすめしたい。もう一つ付け加えたいことがあるんだけど、大きな組織では、開発者が予測できない方法でツールを使うことがあるんだよね。Gitもその一つ。特定のワークフローを強制しようとしない方がいいよ、必須のフックや自動的に有効になるフックも含めてね。代わりに、オプションのプレプッシュフックに必要なことを書いて、プルリクエストチェッカーの早いCI/CDステップにも入れておくといいよ。最終的には同じ結果が得られるし、面倒くさい開発者もハッピーになるはず。

それに同意するよ。複数のコミットがある場合は、https://github.com/tummychow/git-absorb が便利で、すでに行われたコミットにフォーマット変更を追加できる。

これにはGitも含まれる。特定のワークフローを強制しようとしない方がいい、必須のフックや自動的に有効になるフックも含めてね。そして、Gitでは、開発マシンで起こることを必須にすることもできる。必須にしたいことはすべてCIに入れなきゃダメ。プレコミットやプレプッシュフックは、CIの無駄を減らすためにあるだけで、何かを保証するためではないよ。(秘密を誤ってプッシュする人を除いてね。CIでは遅すぎるし、プレプッシュフックは良いアイデアだよ。)

もう一つ言いたいことがあるんだけど、大きな組織では、開発者が予測できない方法でツールを使うことがある。これにはGitも含まれる。特定のワークフローを強制しようとしない方がいいよ、必須のフックや自動で有効にするフックも含めてね。もし強制すると、組織にとって多くの痛みを避けられるから。フォーマットスタイルを強制するのと同じで、誰かが好きなようにやるのを許す方がいい。うまくいかない部分があれば話し合って変更することはできるけど、一貫性があれば失敗が減るからね。

これがその例だよ: https://www.jj-vcs.dev/

現在のプロジェクトで変なことが起きてる。たまにメインを自分のブランチにマージすると失敗するんだ。失敗するのはマージコミットのpre-commitフック。メインの変更がリントチェックで引っかかるんだけど、なぜかそれがメインに入ってしまう。だから、PRのチェックはpre-commitフックのチェックほど厳しくないみたい。結果的に、多くの開発者が--no-verifyでコミットすることに慣れてしまった。そうなると、pre-commitフックの意味ってなんなんだろう?進行中の作業をコミットしたいときもあるし、そうすれば変更を戻しやすくなるんだよね。このチェックはpre-pushの方がいいし、PRパイプラインにも絶対に必要だと思う。そうしないと、スキップされちゃうから。とにかく、これを主張するための材料をくれてありがとう。

両方の良いところを取り入れたらどう?クライアントサイドのバリデーションにはpre-commitフックを使って、CIでも同じチェックを実行するっていうの。自分はこの設定を何年も問題なく使ってる。自分の設定の重要な要件は、すべてのフックがヘルメティックで冪等性があること。生産環境でRustは使ってないから、詳しくは言えないけど、他のほとんどの言語—clang-formatからswift-formatまで—では、信頼できるソース(例えば、チームのS3ストレージ)から事前コンパイルされたバイナリをダウンロードするようにしてる。これで、ツールが制御された環境で動作して、一貫して同じ結果を出すことができるんだ。

私はいつも全然違うやり方で仕事するタイプの開発者なんだ。プレコミットフックが大嫌いで、プレプッシュと早めのステップをCIにするのが正しいやり方だと思う。

フックが単なる早いチェック以上のことをし始めると、似たような問題を見たことがある。状態を持ったり外部のコンテキストに依存する瞬間、ガードレールとしての役割を果たさなくなって、摩擦の原因になっちゃうんだよね。実際には、つまらなくて決定論的なものを保つ方が、すべてを早期にキャッチするよりも重要な気がする。

これに関しては、問題のないより良いgitコマンドを見つけた気がする。完璧ではないけど、私には合ってる。プレコミットスクリプト(https://github.com/ThomasHabets/rustradio/blob/main/extra/pr...)が実行されて、プレコミット環境をこうやって設定する: https://github.com/ThomasHabets/rustradio/blob/main/tickbox/... これを毎回のコミットで実行してる。確かにやりすぎかもしれないけど、問題を防いでくれてるし、壊れたHEADがないことにこだわりすぎかも。でも、貢献したいなら、プレコミットを実行する必要はないよ。PRでも同じように動くから。自分でPRを送ることはないから、これでうまくいってる。もちろん、ワークフローを改善するための提案や批評はいつでも歓迎だよ。少なくとも何も状態を持ってないし(ビルドアーティファクトはキャッシュするけど)、"cargo deny"以外は外部依存もない。

小さな提案だけど、git worktreesは最近の追加機能で、あなたのgitアーカイブの設定よりも良いかもしれないよ。

ワークフローのハッピーパスから外れると、フックが単純な環境変数を観察できないの?例えば、GIT_HOOK_BYEBYE = フックスクリプト内で早期リターン。この記事はちょっとドラマチックすぎる気がする。自分自身にとって面倒なプレコミットフックを書いたら、それが根本的に壊れてるってどういうこと?

Hacker Newsで議論の続きを見る