45395 - シコウサクゴ -

pre-commit Hookの正規表現を8ケース実機テスト:誤検出ゼロを担保する設計

2026-04-26
インフラ
インフラ運用
Git
pre-commit
正規表現
ヒアリング
Last updated:2026-05-07
7 Minutes
1382 Words

pre-commit hook で 「マージ衝突マーカーが残ったままコミットされる事故」 を防ぎたい。簡単そうに見えて、正規表現を雑に書くと通常コードを誤検出します。

そこで、8ケースの実機テストを設計してから hook を実装した結果、誤検出ゼロ + 過去事故パターンの完全検出を達成しました。本記事は、その正規表現設計と境界条件の詰め方を共有します。

防ぎたい事故:衝突マーカー残置コミット

git mergegit cherry-pick で衝突解決中、一部のブロックを直し忘れてコミットしてしまう事故。

1
def fetch_data():
2
<<<<<<< HEAD
3
return api.get('/v1/data')
4
=======
5
return api.get('/v2/data')
6
>>>>>>> feature/v2

これがコミットされて push されると、構文エラー or import エラーで CI が落ちます。レビューで気づけばまだいいですが、squash merge してから気づくと取り返しがつきません。

ナイーブな正規表現の罠

最初にこう書きたくなります。

Terminal window
1
git diff --cached | grep -E "(<<<<|====|>>>>)"

しかしこれは通常コードを誤検出します。

コード誤検出される?
=== "test" (Python の比較演算)される
>>> (Python REPL のプロンプト出力)される
<<<< (テンプレートエンジンの記号)される
==== (Markdown の区切り線)される

通常コードに含まれる ===>>> で hook が落ちると、開発者が hook を切ってしまう(hook 自殺現象)。

正しい正規表現:行頭アンカー + 完全一致 + 行末アンカー

衝突マーカーは Git が生成するため、形式が固定です。

1
<<<<<<< (7文字) ブランチ名 ← 行頭から
2
======= (7文字) ← 7文字、行末
3
>>>>>>> (7文字) ブランチ名 ← 行頭から

これに合わせた正規表現は、

Terminal window
1
# 各マーカーごとに別個の正規表現
2
^<{7} # 行頭の <<<<<<<
3
^={7}$ # 行頭から行末まで完全一致の =======
4
^>{7} # 行頭の >>>>>>>

ポイントは3つ。

  1. 行頭アンカー ^: 行頭に限定。インデント付きの <<< は除外(衝突マーカーは必ず行頭)
  2. 回数指定 {7}: 7文字ぴったり。Markdown の ===== (5〜6文字) や ======== (8文字超) を除外
  3. 行末アンカー $: ======= だけは行末まで完全一致。後続にブランチ名が付く <<<<<<< >>>>>>> には付けない

8ケース実機テスト

設計の正しさを担保するため、8ケースのテストファイルを作って実機検証しました。

ケースコード例期待値
1<<<<<<< HEAD検出する
2======= (7文字、行末)検出する
3>>>>>>> feature/v2検出する
4if a == "test":検出しない
5>>> import os (REPL)検出しない
6=== (3文字)検出しない
7======== (8文字)検出しない
8 <<<<<<< HEAD (インデント付き)検出しない

ケース8は意図的に外す設計。インデント付きの <<< は、コメントやテンプレート文字列内にしか現れない(実衝突は必ず行頭)。

バイナリファイル除外

正規表現以外の落とし穴として、バイナリファイルがあります。画像やバイナリログに偶然 ======= が含まれていると hook が誤検出します。

対策は git diff --numstat の出力を活用。

Terminal window
1
git diff --cached --numstat | while read added removed file; do
2
if [ "$added" = "-" ] && [ "$removed" = "-" ]; then
3
continue # バイナリファイルはスキップ
4
fi
5
# 通常ファイルのみチェック
6
done

- - がバイナリの目印。これで .png.zip を除外します。

hook の最終形

.git/hooks/pre-commit
1
#!/bin/bash
2
ERRORS=0
3
4
while IFS=$'\t' read -r added removed file; do
5
# バイナリファイル除外
6
[ "$added" = "-" ] && [ "$removed" = "-" ] && continue
7
8
# 衝突マーカー検出
9
if grep -nE '^(<{7}|>{7})' "$file" > /dev/null 2>&1; then
10
echo "ERROR: 衝突マーカー残置 in $file"
11
ERRORS=$((ERRORS + 1))
12
fi
13
if grep -nE '^={7}$' "$file" > /dev/null 2>&1; then
7 collapsed lines
14
echo "ERROR: 衝突区切り行残置 in $file"
15
ERRORS=$((ERRORS + 1))
16
fi
17
done < <(git diff --cached --numstat)
18
19
[ "$ERRORS" -gt 0 ] && exit 1
20
exit 0

過去事故パターンの再現テスト

実装後、過去にコミットしてしまった衝突マーカー残置の実例で再現テストしました。

  • ケース実例: 1ファイル中に複数衝突ブロックがあり、最後のブロックだけ残っていた事例
  • 期待値: hook が検出する
  • 結果: 検出成功

「過去の事故再現テスト」は、hook の信頼性を担保する強力な手段です。hook を導入する前に、過去にやらかした失敗を必ず再現させます

教訓:正規表現を書く前にテストケースを書く

この経験から得た教訓です。

教訓1: 正規表現の境界条件は実機テストで詰める

頭で考えるだけでは、必ず例外ケースを見逃します。8ケース以上のテーブルを作って、機械的に検証します。

教訓2: 既存コードに含まれる「似たパターン」を必ず探す

grep -E "patterns" . でリポジトリ全体を検索し、通常コードで誤検出するケースを発見します。

教訓3: hook は「false positive を起こさない」設計を最優先

false positive を出すと、開発者が hook を切ってしまいます。多少 false negative があっても、false positive ゼロを優先します。

教訓4: 過去事故の再現テストを必ず添える

hook の意義は「過去の事故を防ぐこと」。過去事故再現テストがない hook は、将来の事故も防げません

まとめ

  • 衝突マーカー検出 hook は、行頭アンカー + 7文字完全一致が肝
  • 8ケースの実機テストで誤検出ゼロを担保
  • バイナリファイルは numstat- - 判定で除外
  • 過去事故の再現テストを必ず添える

正規表現1行の hook でも、設計プロセスを丁寧に踏むかどうかで、運用後の信頼度が決まります

Article title:pre-commit Hookの正規表現を8ケース実機テスト:誤検出ゼロを担保する設計
Article author:45395
Release time:2026-04-26

記事へのご質問・ご感想をお聞かせください

フィードバックを送る