diff --git a/scripts/repository/commit_hook.php b/scripts/repository/commit_hook.php index 6bf3c6e844..77a3653c58 100755 --- a/scripts/repository/commit_hook.php +++ b/scripts/repository/commit_hook.php @@ -88,6 +88,7 @@ if ($repository->isHg()) { } $engine->setStdin($stdin); +$engine->setOriginalArgv(array_slice($argv, 2)); $remote_address = getenv(DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS); if (strlen($remote_address)) { diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php index 2b4a9ac33a..2bff7a998d 100644 --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -19,6 +19,7 @@ final class DiffusionCommitHookEngine extends Phobject { private $viewer; private $repository; private $stdin; + private $originalArgv; private $subversionTransaction; private $subversionRepository; private $remoteAddress; @@ -84,6 +85,15 @@ final class DiffusionCommitHookEngine extends Phobject { return $this->stdin; } + public function setOriginalArgv(array $original_argv) { + $this->originalArgv = $original_argv; + return $this; + } + + public function getOriginalArgv() { + return $this->originalArgv; + } + public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; @@ -141,7 +151,8 @@ final class DiffusionCommitHookEngine extends Phobject { $this->applyHeraldContentRules($content_updates, $all_updates); - // TODO: Fire external hooks. + // Run custom scripts in `hook.d/` directories. + $this->applyCustomHooks($all_updates); // If we make it this far, we're accepting these changes. Mark all the // logs as accepted. @@ -551,6 +562,74 @@ final class DiffusionCommitHookEngine extends Phobject { return $content_updates; } +/* -( Custom )------------------------------------------------------------- */ + + private function applyCustomHooks(array $updates) { + $args = $this->getOriginalArgv(); + $stdin = $this->getStdin(); + $console = PhutilConsole::getConsole(); + + $env = array( + 'PHABRICATOR_REPOSITORY' => $this->getRepository()->getCallsign(), + self::ENV_USER => $this->getViewer()->getUsername(), + self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(), + self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(), + ); + + $directories = $this->getRepository()->getHookDirectories(); + foreach ($directories as $directory) { + $hooks = $this->getExecutablesInDirectory($directory); + sort($hooks); + foreach ($hooks as $hook) { + // NOTE: We're explicitly running the hooks in sequential order to + // make this more predictable. + $future = id(new ExecFuture('%s %Ls', $hook, $args)) + ->setEnv($env, $wipe_process_env = false) + ->write($stdin); + + list($err, $stdout, $stderr) = $future->resolve(); + if (!$err) { + // This hook ran OK, but echo its output in case there was something + // informative. + $console->writeOut("%s", $stdout); + $console->writeErr("%s", $stderr); + continue; + } + + // Mark everything as rejected by this hook. + foreach ($updates as $update) { + $update->setRejectCode( + PhabricatorRepositoryPushLog::REJECT_EXTERNAL); + $update->setRejectDetails(basename($hook)); + } + + throw new DiffusionCommitHookRejectException( + pht( + "This push was rejected by custom hook script '%s':\n\n%s%s", + basename($hook), + $stdout, + $stderr)); + } + } + } + + private function getExecutablesInDirectory($directory) { + $executables = array(); + + if (!Filesystem::pathExists($directory)) { + return $executables; + } + + foreach (Filesystem::listDirectory($directory) as $path) { + $full_path = $directory.DIRECTORY_SEPARATOR.$path; + if (is_executable($full_path)) { + $executables[] = $full_path; + } + } + + return $executables; + } + /* -( Mercurial )---------------------------------------------------------- */ diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index ef55da4374..c0ee14ae76 100755 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -103,6 +103,10 @@ final class PhabricatorRepositoryPullEngine } else if ($is_hg) { $this->installMercurialHook(); } + + foreach ($repository->getHookDirectories() as $directory) { + $this->installHookDirectory($directory); + } } } catch (Exception $ex) { @@ -173,6 +177,17 @@ final class PhabricatorRepositoryPullEngine Filesystem::changePermissions($path, 0755); } + private function installHookDirectory($path) { + $readme = pht( + "To add custom hook scripts to this repository, add them to this ". + "directory.\n\nPhabricator will run any executables in this directory ". + "after running its own checks, as though they were normal hook ". + "scripts."); + + Filesystem::createDirectory($path, 0755); + Filesystem::writeFile($path.'/README', $readme); + } + /* -( Pulling Git Working Copies )----------------------------------------- */ @@ -311,15 +326,15 @@ final class PhabricatorRepositoryPullEngine */ private function installGitHook() { $repository = $this->getRepository(); - $path = $repository->getLocalPath(); + $root = $repository->getLocalPath(); if ($repository->isWorkingCopyBare()) { - $path .= '/hooks/pre-receive'; + $path = '/hooks/pre-receive'; } else { - $path .= '/.git/hooks/pre-receive'; + $path = '/.git/hooks/pre-receive'; } - $this->installHook($path); + $this->installHook($root.$path); } @@ -438,14 +453,17 @@ final class PhabricatorRepositoryPullEngine execx('svnadmin create -- %s', $path); } + /** * @task svn */ private function installSubversionHook() { $repository = $this->getRepository(); - $path = $repository->getLocalPath().'/hooks/pre-commit'; + $root = $repository->getLocalPath(); - $this->installHook($path); + $path = '/hooks/pre-commit'; + + $this->installHook($root.$path); } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index bcfd48c2b6..37e021c529 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -945,6 +945,35 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } } + public function getHookDirectories() { + $directories = array(); + if (!$this->isHosted()) { + return $directories; + } + + $root = $this->getLocalPath(); + + switch ($this->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + if ($this->isWorkingCopyBare()) { + $directories[] = $root.'/hooks/pre-receive-phabricator.d/'; + } else { + $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/'; + } + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $directories[] = $root.'/hooks/pre-commit-phabricator.d/'; + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + // NOTE: We don't support custom Mercurial hooks for now because they're + // messy and we can't easily just drop a `hooks.d/` directory next to + // the hooks. + break; + } + + return $directories; + } + public function canDestroyWorkingCopy() { if ($this->isHosted()) { // Never destroy hosted working copies.