
# 훅

Git 훅은 `.git/hooks/`의 실행 가능한 스크립트로, git 작업의 특정 시점에 실행됩니다. Pitmaster는 PHP의 프로세스 제어(`proc_open`)를 사용해 훅을 감지하고 호출합니다.

## 훅은 기본적으로 실행됩니다

저장소 작업을 수행하면 [훅Pitmaster에 나열된 훅이 ](#hooks-pitmaster-runs) 자동으로 실행합니다. 저장소 핸들에서 훅을 끄려면 핸들을 열 때 옵션을 전달하세요:

```php
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](https://github.com/inline0/pitmaster/issues/12)).

아래에서 설명하는 `HookRunner`는 이러한 옵션을 무시합니다. 직접 호출하면 항상 훅을 실행합니다.

## HookRunner

```php
use Pitmaster\Hooks\HookRunner;

$hooks = new HookRunner($repo->gitDir());
```

## 훅 존재 여부 확인

```php
if ($hooks->exists('pre-commit')) {
    echo "pre-commit hook is installed\n";
}
```

`.git/hooks/<name>`에 파일이 있고 실행 권한이 있으면 훅이 존재합니다.

## 훅 실행

```php
$result = $hooks->run('pre-commit');

echo $result['exitCode'];  // 0 = success
echo $result['stdout'];    // Hook's standard output
echo $result['stderr'];    // Hook's standard error
```

### 인수 사용

```php
$result = $hooks->run('commit-msg', ['/path/to/COMMIT_EDITMSG']);
```

### stdin 사용

```php
$result = $hooks->run('pre-push', ['origin', 'https://github.com/user/repo.git'], $stdinData);
```

`pre-push` 훅은 stdin으로 ref 정보를 받습니다.

### 성공 여부 확인

```php
if ($hooks->runAndCheck('pre-commit')) {
    // Hook passed (exit code 0), proceed with commit
} else {
    // Hook rejected the operation
}
```

`runAndCheck()`는 훅을 실행하고 종료 코드가 0이면 `true`을 반환하는 편의 기능입니다. 훅이 없으면 `true`를 반환합니다(훅이 없으면 거부할 이유도 없습니다).

## 설치된 훅 나열

```php
$installed = $hooks->listHooks();
// ['pre-commit', 'commit-msg', 'post-checkout']
```

`.git/hooks/`에 있는 파일 중 실행 가능한 파일을 모두 나열합니다(`.sample` 파일은 제외).

훅이 없거나 실행할 수 없는 경우 `run()`은 빈 출력과 종료 코드 0을 반환합니다. 프로세스를 시작할 수 없으면 stderr에 `Failed to start hook`을 출력하고 종료 코드 1을 반환합니다.

## 훅 설치

```php
$hooks->install('pre-commit', "#!/bin/bash\nphp vendor/bin/phpcs --standard=PSR12 .\n");
```

스크립트 내용을 `.git/hooks/<name>`에 쓰고 실행 권한(`chmod 0755`)을 설정합니다.

```php
// 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가 훅을 실행할 때 다음 환경을 전달합니다:

```php
$env = [
    'GIT_DIR' => $this->gitDir,
    'GIT_WORK_TREE' => dirname($this->gitDir),
];
```

훅이 받는 변수는 이것뿐입니다. `PATH`, `HOME` 및 PHP 프로세스의 다른 모든 변수는 전달되지 않습니다([#13](https://github.com/inline0/pitmaster/issues/13)). 훅 스크립트에서는 절대 경로를 사용하거나 스크립트 안에서 `PATH`을 설정하세요. `#!/usr/bin/env php` 셰뱅은 시스템 기본 경로에서만 인터프리터를 찾습니다.

훅은 `dirname($gitDir)`에서 실행됩니다. 일반 `.git` 디렉터리의 경우 이곳은 작업 트리입니다.

## 훅 스크립트 형식

훅은 어떤 언어로든 작성할 수 있습니다. 첫 줄은 셰뱅이어야 합니다:

```bash
#!/bin/bash
# Bash hook
```

```php
#!/usr/bin/env php
<?php
// PHP hook
```

```python
#!/usr/bin/env python3
# Python hook
```

## 예시: Pitmaster을 사용하는 pre-commit

`commit()`는 이미 `pre-commit`, `prepare-commit-msg`, `commit-msg` 및 `post-commit`를 실행하므로 애플리케이션 코드는 실패를 처리하기만 하면 됩니다:

```php
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)
```
