Git Hooks:開発ワークフローを自動化する

Shunku

はじめに

Git hooksは、Gitワークフローの特定のポイントで自動的に実行されるスクリプトです。コード品質の強制、コミットの検証、反復的なタスクの自動化を、コードがリポジトリに到達する前に行うことができます。

この記事では、プロジェクトでGit hooksを効果的に使用する方法を解説します。

Git Hooksの動作

flowchart LR
    subgraph Commit["git commit"]
        A["pre-commit"] --> B["prepare-commit-msg"]
        B --> C["commit-msg"]
        C --> D["post-commit"]
    end

    subgraph Push["git push"]
        E["pre-push"] --> F["リモートにプッシュ"]
    end

    Commit --> Push

    style A fill:#f59e0b,color:#fff
    style C fill:#f59e0b,color:#fff
    style E fill:#f59e0b,color:#fff

利用可能なフック

クライアントサイドフック

フック トリガー 一般的な用途
pre-commit コミット作成前 リント、フォーマット、テスト
prepare-commit-msg エディタが開く前 テンプレートメッセージ
commit-msg メッセージ入力後 フォーマット検証
post-commit コミット作成後 通知
pre-push リモートへのプッシュ前 フルテストスイート
pre-rebase リベース開始前 特定ブランチでの防止

サーバーサイドフック

フック トリガー 一般的な用途
pre-receive プッシュ受け入れ前 全コミットの検証
update 各ref更新前 ブランチ別ポリシー
post-receive プッシュ受け入れ後 デプロイ、通知

基本的なフックの作成

フックの場所

フックは.git/hooks/に配置されます:

ls .git/hooks/
# applypatch-msg.sample  pre-push.sample
# commit-msg.sample      pre-rebase.sample
# post-update.sample     prepare-commit-msg.sample
# pre-applypatch.sample  update.sample
# pre-commit.sample

シンプルなpre-commitフック

#!/bin/sh
# .git/hooks/pre-commit

# リンティングを実行
npm run lint
if [ $? -ne 0 ]; then
    echo "リンティングが失敗しました。コミット前にエラーを修正してください。"
    exit 1
fi

# テストを実行
npm test
if [ $? -ne 0 ]; then
    echo "テストが失敗しました。コミット前に修正してください。"
    exit 1
fi

exit 0

実行可能にします:

chmod +x .git/hooks/pre-commit

コミットメッセージの検証

#!/bin/sh
# .git/hooks/commit-msg

commit_msg_file=$1
commit_msg=$(cat "$commit_msg_file")

# Conventional Commit形式をチェック
pattern="^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}"

if ! echo "$commit_msg" | grep -qE "$pattern"; then
    echo "エラー: コミットメッセージがConventional形式に従っていません。"
    echo "期待される形式: <type>(<scope>): <subject>"
    echo "例: feat(auth): add login functionality"
    exit 1
fi

exit 0

Huskyの使用(推奨)

HuskyはGit hooksの管理を簡単にし、チーム間で共有できるようにします。

インストール

npm install husky --save-dev
npx husky init

これにより.husky/ディレクトリが作成されます:

.husky/
├── _/
│   └── husky.sh
└── pre-commit

pre-commitフックの設定

# .husky/pre-commit
npm run lint
npm test

commit-msgフックの設定

npx husky add .husky/commit-msg 'npx commitlint --edit $1'
# .husky/commit-msg
npx commitlint --edit $1

pre-pushフックの設定

npx husky add .husky/pre-push 'npm run test:e2e'

lint-staged:ステージされたファイルのみにリンターを実行

すべてのファイルでリンターを実行すると遅くなります。lint-stagedはステージされたファイルのみに実行します。

インストール

npm install lint-staged --save-dev

設定

// package.json
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss}": [
      "stylelint --fix"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}

Huskyとの連携

# .husky/pre-commit
npx lint-staged

commitlint:コミットメッセージ規約の強制

インストール

npm install @commitlint/cli @commitlint/config-conventional --save-dev

設定

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',     // 新機能
        'fix',      // バグ修正
        'docs',     // ドキュメント
        'style',    // フォーマット
        'refactor', // リファクタリング
        'test',     // テスト
        'chore',    // メンテナンス
        'perf',     // パフォーマンス
        'ci',       // CI変更
        'build',    // ビルド変更
        'revert'    // コミット取り消し
      ]
    ],
    'subject-max-length': [2, 'always', 72],
    'body-max-line-length': [2, 'always', 100]
  }
};

コミット形式

<type>(<scope>): <subject>

<body>

<footer>

例:

feat(auth): add Google OAuth login

Implement Google OAuth 2.0 authentication flow.
Users can now sign in with their Google accounts.

Closes #123
fix(api): handle null response from payment gateway

The payment gateway sometimes returns null for declined cards.
Added null check to prevent crashes.

Fixes #456

完全なセットアップ例

package.json

{
  "name": "my-project",
  "scripts": {
    "lint": "eslint src/",
    "format": "prettier --write src/",
    "test": "jest",
    "test:e2e": "cypress run",
    "prepare": "husky"
  },
  "devDependencies": {
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0",
    "@commitlint/cli": "^18.0.0",
    "@commitlint/config-conventional": "^18.0.0",
    "eslint": "^8.0.0",
    "prettier": "^3.0.0"
  },
  "lint-staged": {
    "*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,yml}": ["prettier --write"]
  }
}

Huskyフック

# .husky/pre-commit
npx lint-staged

# .husky/commit-msg
npx commitlint --edit $1

# .husky/pre-push
npm run test

高度なフックの例

保護ブランチへのコミット防止

#!/bin/sh
# .husky/pre-commit

branch=$(git symbolic-ref --short HEAD)
protected_branches="main master develop"

for protected in $protected_branches; do
    if [ "$branch" = "$protected" ]; then
        echo "エラー: $branchへの直接コミットは許可されていません。"
        echo "featureブランチを作成してください。"
        exit 1
    fi
done

npx lint-staged

シークレットのチェック

#!/bin/sh
# .husky/pre-commit

# 潜在的なシークレットをチェック
if git diff --cached --name-only | xargs grep -l -E "(api[_-]?key|password|secret|token|private[_-]?key)" 2>/dev/null; then
    echo "警告: ステージされたファイルにシークレットの可能性があります。"
    echo "コミット前に確認してください。"
    read -p "それでも続行しますか? (y/n) " answer
    if [ "$answer" != "y" ]; then
        exit 1
    fi
fi

npx lint-staged

ファイルに基づいて異なるチェックを実行

#!/bin/sh
# .husky/pre-commit

# ステージされたファイルのリストを取得
staged_files=$(git diff --cached --name-only)

# TypeScriptファイルが変更されたかチェック
if echo "$staged_files" | grep -q '\.tsx\?$'; then
    echo "TypeScriptファイルが変更されました。型チェックを実行..."
    npx tsc --noEmit
fi

# テストファイルが変更されたかチェック
if echo "$staged_files" | grep -q '\.test\.[jt]sx\?$'; then
    echo "テストファイルが変更されました。テストを実行..."
    npm test
fi

npx lint-staged

Issue番号をコミットに追加

#!/bin/sh
# .husky/prepare-commit-msg

commit_msg_file=$1
branch=$(git symbolic-ref --short HEAD)

# ブランチ名からissue番号を抽出(例:feature/PROJ-123-description)
issue=$(echo "$branch" | grep -oE '[A-Z]+-[0-9]+')

if [ -n "$issue" ]; then
    # コミットメッセージの先頭にissue番号を追加
    sed -i.bak "1s/^/[$issue] /" "$commit_msg_file"
fi

チーム間でのフック共有

問題:.git/hooksは追跡されない

.git/hooks/内のフックはバージョン管理されません。

解決策1:Husky(推奨)

Huskyは追跡される.husky/にフックを保存します:

.husky/
├── pre-commit
├── commit-msg
└── pre-push

解決策2:カスタムフックディレクトリ

# カスタムフックパスを設定
git config core.hooksPath .githooks

# または.gitconfigですべてのリポジトリに
[core]
    hooksPath = ~/.git-hooks

解決策3:npm postinstall

{
  "scripts": {
    "postinstall": "cp -r hooks/* .git/hooks/ && chmod +x .git/hooks/*"
  }
}

フックのバイパス

時にはフックをスキップする必要があります:

# pre-commitとcommit-msgフックをスキップ
git commit --no-verify -m "WIP: temporary commit"

# pre-pushフックをスキップ
git push --no-verify

必要な場合にのみ控えめに使用してください。

トラブルシューティング

フックが実行されない

# フックが実行可能かチェック
ls -la .git/hooks/pre-commit

# 実行可能にする
chmod +x .git/hooks/pre-commit

フックがサイレントに失敗

# デバッグを追加
#!/bin/sh
set -x  # コマンドを実行時に出力
set -e  # エラー時に終了

Windowsの改行コード

# Windowsでフックが失敗する場合、改行コードを確認
# CRLFではなくLFであるべき

まとめ

ツール 目的
Git hooks Gitイベントでスクリプトを実行
Husky フックを管理、チームと共有
lint-staged ステージされたファイルのみにリンター実行
commitlint コミットメッセージ形式を強制

Git hooksは問題がリポジトリに到達する前にキャッチし、時間を節約してコード品質を維持します。

参考資料

  • O'Reilly - Version Control with Git, Chapter 14
  • Git Documentation - Git Hooks
  • Husky Documentation
  • Conventional Commits Specification