Gitコミット時にphpcsチェックを実行する を作ってみた。

■git commit時にphpcsを実行

・変更した行にphpcsエラーがある場合、phpcsエラーを出力し、コミットをキャンセルします
・phpcsエラー箇所が既にコミット済みの場合はスルーします
・「PhpcsCheckIgnore.txt」に記載されたphpファイルは、phpcsチェックを行いません

■インストール

1)[プロジェクト]/.git/hooks配下に下記ファイルを格納
    ・commit-msg
    ・PhpcsCheck.php
    ・PhpcsCheckIgnore.txt
2)commit-msgに実行権限を付与
    chmod +x commit-msg
  • commit-msg
#!/bin/sh

# ■git commit時にphpcsを実行
# ・変更した行にphpcsエラーがある場合、phpcsエラーを出力し、コミットをキャンセルします
# ・phpcsエラー箇所が既にコミット済みの場合はスルーします
# ・「PhpcsCheckIgnore.txt」に記載されたphpファイルは、phpcsチェックを行いません
#
# ■インストール
# 1)[プロジェクト]/.git/hooks配下に下記ファイルを格納
#     ・commit-msg
#     ・PhpcsCheck.php
#     ・PhpcsCheckIgnore.txt
# 2)commit-msgに実行権限を付与
#     chmod +x commit-msg

# 親ディレクトリ取得
# ※/XXX/XXX/XXX/.git/hookを/XXX/XXX/XXX/に変更
currentDir=$(cd $(dirname $0); pwd)
parentDir=${currentDir/.*/}

# 変更ファイル一覧を取得
isError=false
for file in $(git diff --cached --name-only)
do
    # phpcsでチェック実行
    echo "${file}"
    result=$(php ${currentDir}/PhpcsCheck.php ${parentDir} ${file} 2>/dev/null)

    # phpcsでエラーがある場合はフラグを立てる
    if [ $? -ne 0 ]; then
        isError=true
    fi

    # phpcs結果を出力
    if [ -n "$result" ]; then
        echo "${result}"
    fi
    
done

# phpcsでエラーがある場合は異常終了
if  $isError ; then
    echo "[PHPCS ERROR] has not passed"
    exit 1
fi

# 正常終了
echo "[PHPCS SUCCESS] passed all"
exit 0
  • PhpcsCheck.php
<?php

/**
 * 変更行のphpcsエラー情報を出力します。
 * 正常終了時は「0」、変更行のphpcsエラーがある場合および異常終了時は「1」を返却します。
 * 
 * PHP Version >= 5.2
 *
 * @category Tool
 * @package  Tool
 * @author   Your Name <yourname@example.com>
 * @license  MIT License
 * @link     https://yoursite.com
 */

// 引数取得
$dir = $argv[1];
$file = $argv[2];

// デバッグログファイル設定
$debugLogFile = "${dir}.git/hooks/PhpcsCheck.log";

// 引数チェック
if (checkParameter($dir, $file) === false) {

    // 異常終了
    exit(1);
}

// 除外対象ファイルチェック
if (checkIgnore($dir, $file) === false) {

    // 正常終了
    exit(0);
}

// phpcsのエラー行番号リスト取得
$phpcsErrorList = getPhpcsErrorList($dir, $file);

// 変更箇所の行番号リスト取得
$diffList = getDiffList($dir, $file);

// 変更箇所のphpcsエラー箇所を標準出力
$phpcsErrorInfo = outputPhpcsError($file, $phpcsErrorList, $diffList);

// 変更行のphpcsエラーがある場合は
if ($phpcsErrorInfo !== "") {

    // 変更行のphpcsエラーを標準出力
    echo $phpcsErrorInfo;

    // 異常終了
    exit(1);
}

// 正常終了
exit(0);

/**
 * 引数チェック
 * 
 * @param string $dir  親ディレクトリ
 * @param string $file 対象ファイル
 * 
 * @return boolean true:正常、false:異常
 */
function checkParameter($dir, $file)
{
    $ret = true;

    // ファイルパスの存在チェック
    if (file_exists("${dir}${file}") === false) {
        echo "${dir}${file} は存在しないファイルパスです\n";
        $ret = false;
    }

    // デバッグログ出力
    debugLog("[checkParameter] dir=${dir} file=${file} ${ret}");

    return $ret;
}

/**
 * 除外対象チェック
 * 
 * @param string $dir  親ディレクトリ
 * @param string $file 対象ファイル
 * 
 * @return boolean true:除外対象でない、false:除外対象である
 */
function checkIgnore($dir, $file)
{
    $ret = true;

    // 除外対象ファイルをオープン
    $lines = file("${dir}.git/hooks/PhpcsCheckIgnore.txt");
    foreach ($lines as $line) {
 
        // コメント行はスキップ
        if (substr($line, 0, 1) === "#") {
            continue;
        }

        // 除外対象ファイルの場合
        if (trim($line) === $file) {
            echo "${file} は除外対象のため、phpcsチェックを行いません";
            $ret = false;
            break;
        }
    }

    // デバッグログ出力
    debugLog("[checkIgnore] file=${file} ${ret}");

    return $ret;
}

/**
 * Phpcsエラーリスト取得
 * 
 * @param string $dir  親ディレクトリ
 * @param string $file 対象ファイル
 * 
 * @return array phpcsエラー情報を格納したリスト
 */
function getPhpcsErrorList($dir, $file)
{
    $result = array();

    // phpcs実行
    $command = "phpcs ${dir}${file} 2>/dev/null";
    $outputs = execCommand($command);

    // 全てのphpcsエラーについて
    foreach ($outputs as $output) {

        // 行番号が記載された行であれば、リストに追加する
        if (isLineNumber($output) === true) {
            $itemList = explode("|", $output);
            $result[trim($itemList[0])] = $output;
        }
    }

    // デバッグログ出力
    debugLog("[getPhpcsErrorList] ".implode("\n", $result));

    return $result;
}

/**
 * 変更箇所の行番号リスト取得
 * 
 * @param string $dir  親ディレクトリ
 * @param string $file 対象ファイル
 * 
 * @return array 変更箇所の行番号を格納したリスト
 */
function getDiffList($dir, $file)
{
    $result = array();

    // 変更箇所リストを取得
    $command .= "git --no-pager diff --cached --no-ext-diff ";
    $command .= "-U1000000 ${file} 2>/dev/null";
    $outputs = execCommand($command);

    // 全ての変更箇所について
    $lineNumber = -1;
    foreach ($outputs as $output) {

        // 「<?php」で行番号カウント開始
        if (trim($output) === "<?php") {
            $lineNumber = 0;
        }

        // 行番号カウント
        if ($lineNumber >= 0) {

            // 「-」で始まる場合、インクリメントしない
            if (substr($output, 0, 1) === "-") {
                continue;
            }

            // 行番号を更新
            $lineNumber += 1;

            // 「+」で始まる場合、リストに格納
            if (substr($output, 0, 1) === "+") {
                $result[$lineNumber] = $lineNumber;
            }
        }
    }

    // デバッグログ出力
    debugLog("[getDiffList] ".implode(",", $result));

    return $result;
}

/**
 * 変更箇所のphpcsエラー箇所を標準出力
 * 
 * @param string $file           対象ファイル
 * @param array  $phpcsErrorList phpcsエラー情報を格納したリスト
 * @param array  $diffList       変更箇所の行番号を格納したリスト
 * 
 * @return string $phpcsErrorInfo phpcsエラー情報
 */
function outputPhpcsError($file, $phpcsErrorList, $diffList)
{    
    $result = array();
    $phpcsErrorInfo = "";

    // 変更行のphpcsエラーを抽出し、リストに格納
    foreach ($diffList as $diff) {
        if (array_key_exists($diff, $phpcsErrorList)) {
            $result[] = $phpcsErrorList[$diff];
        }
    }

    // phpcsエラーリストから、phpcsエラー情報を生成
    if (empty($result) === false) {
        foreach ($result as $item) {
            $phpcsErrorInfo .= $item."\n";
        }
    }

    // デバッグログ出力
    debugLog("[outputPhpcsError] ${phpcsErrorInfo}");

    // phpcsエラー情報を返却
    return $phpcsErrorInfo;
}

/**
 * コマンド実行
 * 
 * @param string $command コマンド文字列
 * 
 * @return string コマンド実行結果
 */
function execCommand($command)
{
    $outputs = "";
    $status = "";

    // コマンド実行
    exec($command, $outputs, $status);

    // デバッグログ出力
    debugLog("[execCommand] ${command} ${status}");

    return $outputs;
}

/**
 * デバッグログ出力
 * 
 * @param string $output 出力文字列
 * 
 * @return void
 */
function debugLog($output)
{
    $date = date("Y/m/d H:i:s");
    global $debugLogFile;
    $ret = error_log("${date} ${output}\n", 3, $debugLogFile);
}

/**
 * 行番号記載有無の判定
 * 
 * @param string $target 判定対象文字列
 * 
 * @return boolean true:行番号の記載がある、false:行番号の記載がない
 */
function isLineNumber($target)
{
    // 空行はスキップ
    if ($target === "") {
        return false;
    }
    
    // コメント行はスキップ
    if (substr($target, 0, 1) === "-") {
        return false;
    }

    // 「FOUND ~」行、「Time: ~」行、「FILE: ~」行はスキップ
    $skipList = array("/^FOUND /", "/^Time: /", "/^FILE: /");
    foreach ($skipList as $skip) {
        if (preg_match($skip, $target) === true) {
            return false;
        }
    }

    // 行番号の記載がある
    return true;
}
  • PhpcsCheckIgnore.txt
# 除外対象ファイルを記載します(phpcsチェックを行いません)
# 例)test/app/action/Main/CRUD.php