훅
Git 훅은 .git/hooks/의 실행 가능한 스크립트로, git 작업의 특정 시점에 실행됩니다. Pitmaster는 PHP의 프로세스 제어(proc_open)를 사용해 훅을 감지하고 호출합니다.
훅은 기본적으로 실행됩니다
저장소 작업을 수행하면 훅Pitmaster에 나열된 훅이 자동으로 실행합니다. 저장소 핸들에서 훅을 끄려면 핸들을 열 때 옵션을 전달하세요:
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는 무시하며, 연결된 워크트리에서는 .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 훅은 stdin으로 ref 정보를 받습니다.
성공 여부 확인
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을 반환합니다. 프로세스를 시작할 수 없으면 stderr에 Failed to start hook을 출력하고 종료 코드 1을 반환합니다.
훅 설치
$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() |
업스트림 리비전 | 예 |
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 셰뱅은 시스템 기본 경로에서만 인터프리터를 찾습니다.
훅은 dirname($gitDir)에서 실행됩니다. 일반 .git 디렉터리의 경우 이곳은 작업 트리입니다.
훅 스크립트 형식
훅은 어떤 언어로든 작성할 수 있습니다. 첫 줄은 셰뱅이어야 합니다:
#!/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)