「Enter」キーの押し間違いから学んだ手痛い教訓
半年前のある金曜日の夜、私たちのチームは初歩的なミスで一週間分の成果を失いかけました。ある新人開発者が、競合の解決に不慣れだったため、誤ってgit push --forceをmainブランチに直接実行してしまったのです。その結果、48個のクリーンなコミットがわずか2秒で消え去り、代わりに彼個人の作りかけのコードが入り込んでしまいました。
実際、信頼や口頭での注意だけでプロジェクトを管理することは不可能です。15〜20人のチームでは、ミスは避けられません。その時、私は気づきました。サーバー側に強力な「関門」が必要だと。クライアント側のフック(Client-side hooks)も便利ですが、--no-verifyフラグで簡単に回避できてしまいます。私たちは、より強制力のある仕組みを必要としていました。
なぜGitHub ActionsやCI/CDだけでは不十分なのか?
「GitHub ActionsやGitLab CIでエラーチェックをすれば十分じゃないか?」と疑問に思う方もいるでしょう。確かに有効ですが、そこには大きな落とし穴があります。
第一に、クライアント側のフック(Client-side hooks)は、個人のPCの.git/hooksディレクトリに存在します。コミット時に-nフラグを付けるだけで、すべての障壁は消え去ります。これは、警備員に鍵をかけるよう頼んでも、彼らが面倒だと思えば無視できてしまうようなものです。
第二に、CI/CDパイプラインは通常、コードがサーバーに上がった後に実行されます。CIが真っ赤なエラーを吐いたとしても、その「ゴミ」コードはすでにGitの履歴に残ってしまいます。それをクリーンアップするには、リバートやリセットが必要になり、不必要なコミットが増えてしまいます。
根本的な解決策は、サーバー側のフック(Server-side Hooks)です。これはリポジトリをホストしているサーバー上で直接実行されるスクリプトです。スクリプトがエラー(0以外の終了コード)を返すと、pushコマンドは即座に拒否されます。問題のあるコードは、サーバーのディスクに触れることさえできません。
なぜチームのルールは頻繁に破られるのか?
チームの動きを観察した結果、リポジトリが混乱する理由は主に3つあることがわかりました。
- 権限が広すぎる:誰でも
mainやdevelopに直接コードをプッシュできてしまう。 - 適当なコミットメッセージ:「fix bug」や「.」だけで済ませる人がいて、後から履歴を追うのが非常に苦痛になる。
- ローカルテストの実行忘れ:飲み会や早帰りを急ぐあまり、ロジックエラーのあるコードをプッシュしてしまう。
最強のコンビ:pre-receiveと updateフック
Gitサーバーにおいて、マスターすべき最も重要な2つの「衛兵」がこちらです:
1. Pre-receiveフック
このスクリプトは、サーバーがプッシュ命令を受け取った直後に実行されます。プッシュされるブランチの数に関わらず、プッシュ1回につき1回だけ起動します。stdinからの入力データは、<old-rev> <new-rev> <ref-name>の形式です。
私は通常、これを共通ルールのチェックに使用します。例えば、5MB以上の重いファイルのプッシュ禁止や、.envやnode_modulesなどの機密・不要なファイルが混入するのを阻止します。
2. Updateフック
前述のフックとは異なり、updateフックはブランチごとに個別に実行されます。3つのブランチを同時にプッシュした場合、スクリプトは3回実行されます。この特性は、ブランチごとに異なるルールを設定するのに非常に便利です。例えば、mainではforce pushを禁止しつつ、feature/ブランチでは許可するといった運用が可能です。
本番環境での実践的な導入
/home/git/project.gitにベアリポジトリ(Bare Repository)があると仮定します。その中のhooks/ディレクトリに入って作業を開始しましょう。
ステップ1:updateフックで重要ブランチのForce Pushを阻止する
hooks/updateファイルを作成し、chmod +xで実行権限を付与します:
#!/bin/bash
refname=$1
oldrev=$2
newrev=$3
# mainブランチのみ保護
if [ "$refname" == "refs/heads/main" ]; then
# 1. ブランチの削除を禁止
if [ "$newrev" == "0000000000000000000000000000000000000000" ]; then
echo "[エラー] 止まってください!mainブランチは削除できません。"
exit 1
fi
# 2. 履歴の継続性をチェックしてForce Pushを阻止
if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
base=$(git merge-base $oldrev $newrev)
if [ "$base" != "$oldrev" ]; then
echo "[エラー] mainブランチでのForce pushは禁止されています。やり直す前にpullとmergeを行ってください!"
exit 1
fi
fi
fi
exit 0
ステップ2:pre-receiveフックでコミットメッセージを標準化する
Gitの履歴をよりプロフェッショナルに見せるため、FEAT:、FIX:、CHORE:などのプレフィックスの使用を強制します:
#!/bin/bash
while read oldrev newrev refname; do
# 新しいコミットのリストを取得(サーバーに既に存在するものは除外)
commits=$(git rev-list $oldrev..$newrev)
for commit in $commits; do
subject=$(git log -1 --format=%s $commit)
if [[ ! $subject =~ ^(FEAT|FIX|CHORE|DOCS):\ .+ ]]; then
echo "[エラー] コミットメッセージが標準に準拠していません: '$subject'"
echo "正しい例: 'FEAT: MoMo決済機能を追加'"
exit 1
fi
done
done
exit 0
運用のコツ:スピードを最優先にする
私の苦い経験から言えることは、「サーバー側のフックでユニットテストを実行してはいけない」ということです。なぜなら、フックは同期的に実行されるからです。プッシュが成功したかどうかを知るために5分も待たされるようでは、開発者はストレスを感じ、すぐにルールを回避する方法を探し始めるでしょう。
最適な戦略:
- サーバーフック(Server Hooks):コミットメッセージの形式、ブランチ名、ファイルサイズなど、非常に高速(2秒以内)に終わるチェックのみを行う。
- CI/CD (GitHub/GitLab):リンターの実行、ロジックテスト、Dockerイメージのビルドなど、重い処理を担当させる。
導入から6か月後の成果
導入当初、メンバーからはサーバーに何度も「拒否」されることへの不満もありました。しかし、1か月も経つと、丁寧なコミットメッセージを書く習慣が身に付きました。今ではGitの履歴は教科書のようにきれいです。何より、誰かが誤ってforce pushしたために夜通しデータを復旧させる必要がなくなったことが、私にとって最大のメリットです。
もしあなたが重要なプロジェクトを率いているなら、今日からでもこれらの「検問所」を設置してみてください。コードを保護するだけでなく、チームに自然と規律をもたらしてくれるはずです。

