フック
Gitフックは、.git/hooks/内にある実行可能スクリプトで、git操作の特定のタイミングで実行されます。PitmasterはPHPのプロセス制御(proc_open)を使ってフックを検出し、実行します。
フックはデフォルトで実行される
リポジトリ操作では、Hooks Pitmaster runsに記載されたフックが自動的に実行されます。リポジトリハンドルでフックを無効にするには、開くときにオプションを渡します。
use Pitmaster\Pitmaster;
// Skip .git/hooks scripts for this handle
$repo = Pitmaster::open('/path/to/project', ['hooks' => false]);
// Start no processes at all: hooks, the fsmonitor hook, clone/fetch/push, and SSH are disabled
$repo = Pitmaster::open('/path/to/project', Pitmaster::processFreeOptions());
$repo->hooksEnabled(); // false
$repo->processesEnabled(); // false for the second handle
Pitmaster::init()とPitmaster::clone()は同じオプションを受け取ります。フックの実行にはproc_open()が必要です。これを無効にしているホストでは、['hooks' => false]を指定してリポジトリを開きます。CLIは常にフックを有効にしてリポジトリを開きます。
Pitmasterは<gitDir>/hooks内のフックだけを探します。core.hooksPathは無視され、リンクされたworktreeでは.git/worktrees/<name>/hooks内を探すため、共有の.git/hooksスクリプトは実行されません(#12)。
以下で説明するHookRunnerは、これらのオプションを無視します。直接呼び出すと、常にフックが実行されます。
HookRunner
use Pitmaster\Hooks\HookRunner;
$hooks = new HookRunner($repo->gitDir());
フックの有無を確認する
if ($hooks->exists('pre-commit')) {
echo "pre-commit hook is installed\n";
}
.git/hooks/<name>にファイルが存在し、実行可能であれば、そのフックは存在します。
フックを実行する
$result = $hooks->run('pre-commit');
echo $result['exitCode']; // 0 = success
echo $result['stdout']; // Hook's standard output
echo $result['stderr']; // Hook's standard error
引数を使う
$result = $hooks->run('commit-msg', ['/path/to/COMMIT_EDITMSG']);
stdinを使う
$result = $hooks->run('pre-push', ['origin', 'https://github.com/user/repo.git'], $stdinData);
pre-pushフックは、ref情報をstdinから受け取ります。
成功を確認する
if ($hooks->runAndCheck('pre-commit')) {
// Hook passed (exit code 0), proceed with commit
} else {
// Hook rejected the operation
}
runAndCheck()はフックを実行する便利なメソッドで、終了コードが0の場合にtrueを返します。フックが存在しない場合はtrueを返します(フックがなければ拒否されないため)。
インストール済みフックを一覧表示する
$installed = $hooks->listHooks();
// ['pre-commit', 'commit-msg', 'post-checkout']
.git/hooks/内の実行可能なファイルをすべて一覧表示します(.sampleファイルを除く)。
フックがない、または実行できない場合、run()は終了コード0と空の出力を返します。プロセスを起動できない場合は、終了コード1を返し、stderrにFailed to start hookを出力します。
フックをインストールする
$hooks->install('pre-commit', "#!/bin/bash\nphp vendor/bin/phpcs --standard=PSR12 .\n");
スクリプトの内容を.git/hooks/<name>に書き込み、実行権限(chmod 0755)を設定します。
// Install a PHP hook
$hooks->install('pre-commit', <<<'HOOK'
#!/usr/bin/env php
<?php
// Run static analysis before commit
$output = [];
$returnCode = 0;
exec('php vendor/bin/phpstan analyse --no-progress 2>&1', $output, $returnCode);
if ($returnCode !== 0) {
echo "PHPStan found errors:\n";
echo implode("\n", $output) . "\n";
exit(1);
}
HOOK);
Pitmasterが実行するフック
HookRunnerは名前を指定して任意のフックを実行できます。Repositoryは次のフックを自動的に実行します。
| フック | 実行元 | 引数 | 処理を中断できるか |
|---|---|---|---|
pre-commit |
commit() |
なし | はい(終了コードが0以外) |
prepare-commit-msg |
commit() |
メッセージファイル、その後message(マージ完了時はmerge) |
はい |
commit-msg |
commit() |
メッセージファイル(.git/COMMIT_EDITMSG)。ファイルへの編集内容が使われます |
はい(終了コードが0以外) |
post-commit |
commit() |
なし | いいえ(情報通知のみ) |
pre-rebase |
rebase() |
upstreamのリビジョン | はい |
post-checkout |
checkout() |
古いHEAD、新しいHEAD、1 |
いいえ(情報通知のみ) |
post-merge |
merge()、mergeOctopus()、mergeContinue() |
0 |
いいえ(情報通知のみ) |
pre-push |
push()とその派生形 |
リモート名、URL。ref行はstdinから受け取る | はい(終了コードが0以外) |
フックが失敗すると、操作はRuntimeException('<name> hook failed')をスローします。フックの出力は含まれません。コミットフックはcommit()からのみ実行されます。merge()が作成したマージコミットでは実行されません。post-checkoutはaddWorktree()、reset()、restore()では実行されません。
サーバー側のフック(pre-receive、update、post-receive、post-update)、pre-auto-gc、post-rewriteは自動実行されません。
フックの環境
Pitmasterがフックを実行するとき、次の環境を渡します。
$env = [
'GIT_DIR' => $this->gitDir,
'GIT_WORK_TREE' => dirname($this->gitDir),
];
フックに渡される変数はこれだけです。PATH、HOME、およびPHPプロセスのその他すべての変数は引き継がれません(#13)。フックスクリプトでは絶対パスを使うか、スクリプト内でPATHを設定してください。#!/usr/bin/env phpのshebangでは、システムのデフォルトパスにあるインタープリターしか見つかりません。
フックはdirname($gitDir)で実行されます。通常の.gitディレクトリでは、ここが作業ツリーになります。
フックスクリプトの形式
フックは任意の言語で記述できます。1行目にはshebangが必要です。
#!/bin/bash
# Bash hook
#!/usr/bin/env php
<?php
// PHP hook
#!/usr/bin/env python3
# Python hook
例:Pitmasterを使ったpre-commit
commit()はすでにpre-commit、prepare-commit-msg、commit-msg、post-commitを実行するため、アプリケーションコードでは失敗時の処理だけが必要です。
try {
$commitId = $repo->commit('Add feature');
} catch (\RuntimeException $e) {
echo $e->getMessage(); // "pre-commit hook failed"
}
HookRunnerを自分で呼び出すのは、['hooks' => false]を指定して開いたハンドルを使う場合、またはPitmasterが実行しないフックの場合に限ります。
フックの例
Gitはリポジトリの初期化時に.sampleフックファイルを作成します。Pitmaster::init()は空のhooks/ディレクトリを作成します。PitmasterのlistHooks()では.sampleファイルは対象外です。
.git/hooks/
pre-commit.sample (excluded from listHooks)
commit-msg.sample (excluded from listHooks)
pre-commit (included - installed by user)