Hooks
Git hooks are executable scripts in .git/hooks/ that run at specific points in git operations. Pitmaster detects and invokes hooks using PHP's process control (proc_open).
Hooks run by default
Repository operations run the hooks listed under Hooks Pitmaster runs automatically. To turn them off for a repository handle, pass options when opening it:
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() and Pitmaster::clone() accept the same options. Running a hook needs proc_open(); on hosts that disable it, open repositories with ['hooks' => false]. The CLI always opens the repository with hooks enabled.
Pitmaster looks for hooks only in <gitDir>/hooks. It ignores core.hooksPath, and in a linked worktree it looks in .git/worktrees/<name>/hooks, so the shared .git/hooks scripts do not run there (#12).
HookRunner, described below, ignores these options: calling it directly always runs the hook.
HookRunner
use Pitmaster\Hooks\HookRunner;
$hooks = new HookRunner($repo->gitDir());
Check if a hook exists
if ($hooks->exists('pre-commit')) {
echo "pre-commit hook is installed\n";
}
A hook exists if the file is present at .git/hooks/<name> and is executable.
Run a hook
$result = $hooks->run('pre-commit');
echo $result['exitCode']; // 0 = success
echo $result['stdout']; // Hook's standard output
echo $result['stderr']; // Hook's standard error
With arguments
$result = $hooks->run('commit-msg', ['/path/to/COMMIT_EDITMSG']);
With stdin
$result = $hooks->run('pre-push', ['origin', 'https://github.com/user/repo.git'], $stdinData);
The pre-push hook receives ref information on stdin.
Check success
if ($hooks->runAndCheck('pre-commit')) {
// Hook passed (exit code 0), proceed with commit
} else {
// Hook rejected the operation
}
runAndCheck() is a convenience that runs the hook and returns true if the exit code is 0. If the hook does not exist, it returns true (no hook means no objection).
List installed hooks
$installed = $hooks->listHooks();
// ['pre-commit', 'commit-msg', 'post-checkout']
Lists all files in .git/hooks/ that are executable (excludes .sample files).
run() on a missing or non-executable hook returns exit code 0 with empty output. If the process cannot be started, it returns exit code 1 with Failed to start hook on stderr.
Install a hook
$hooks->install('pre-commit', "#!/bin/bash\nphp vendor/bin/phpcs --standard=PSR12 .\n");
Writes the script content to .git/hooks/<name> and sets the executable permission (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);
Hooks Pitmaster runs
HookRunner can run any hook by name. Repository runs these automatically:
| Hook | Run by | Arguments | Can abort? |
|---|---|---|---|
pre-commit |
commit() |
none | Yes (non-zero exit) |
prepare-commit-msg |
commit() |
message file, then message (or merge when completing a merge) |
Yes |
commit-msg |
commit() |
message file (.git/COMMIT_EDITMSG); edits to the file are used |
Yes (non-zero exit) |
post-commit |
commit() |
none | No (informational) |
pre-rebase |
rebase() |
the upstream revision | Yes |
post-checkout |
checkout() |
old HEAD, new HEAD, 1 |
No (informational) |
post-merge |
merge(), mergeOctopus(), mergeContinue() |
0 |
No (informational) |
pre-push |
push() and its variants |
remote name, URL; ref lines on stdin | Yes (non-zero exit) |
A failing hook makes the operation throw RuntimeException('<name> hook failed'); the hook's output is not included. The commit hooks run only from commit(): merge commits created by merge() do not run them. post-checkout does not run for addWorktree(), reset(), or restore().
Server-side hooks (pre-receive, update, post-receive, post-update), pre-auto-gc, and post-rewrite are never run automatically.
Hook environment
When Pitmaster runs a hook, it passes this environment:
$env = [
'GIT_DIR' => $this->gitDir,
'GIT_WORK_TREE' => dirname($this->gitDir),
];
These are the only variables the hook gets: PATH, HOME, and every other variable of the PHP process are not passed through (#13). Use absolute paths in hook scripts, or set PATH inside the script; a #!/usr/bin/env php shebang only finds interpreters in the system default path.
The hook runs in dirname($gitDir), which is the working tree for a normal .git directory.
Hook script format
Hooks can be written in any language. The first line must be a shebang:
#!/bin/bash
# Bash hook
#!/usr/bin/env php
<?php
// PHP hook
#!/usr/bin/env python3
# Python hook
Example: pre-commit with Pitmaster
commit() already runs pre-commit, prepare-commit-msg, commit-msg, and post-commit, so application code only needs to handle the failure:
try {
$commitId = $repo->commit('Add feature');
} catch (\RuntimeException $e) {
echo $e->getMessage(); // "pre-commit hook failed"
}
Call HookRunner yourself only on a handle opened with ['hooks' => false], or for hooks Pitmaster does not run.
Sample hooks
Git initializes repositories with .sample hook files; Pitmaster::init() creates an empty hooks/ directory. Pitmaster's listHooks() excludes .sample files:
.git/hooks/
pre-commit.sample (excluded from listHooks)
commit-msg.sample (excluded from listHooks)
pre-commit (included - installed by user)