@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add 'hook.d/' directories to SVN and Git repositories for custom hooks

Summary:
Fixes T4189. Ref T4151. Allows repositories to have additional custom hooks for operations which can't be expressed with Herald (one such operation is lint).

This adds only local hook directories, since they're easier to use with existing hooks than global directories. I might add global directories eventually.

This doesn't support Mercurial since we have no demand for it and it's more complicated (we lose compatibility and power by just dropping a `hooks.d/` somewhere).

Test Plan:
- Pulled hosted SVN and Git repos to verify the hook directories generate correctly.
- Added a variety of hooks to the hook directories (echo + pass, fail).
- Pushed commits and verified the hooks fired (output expected info, or failed).
- Verified push log reflected the correct error code ("3", external) and detail ("nope.sh") when rejecting.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T4151, T4189

Differential Revision: https://secure.phabricator.com/D7884

+134 -7
+1
scripts/repository/commit_hook.php
··· 88 88 } 89 89 90 90 $engine->setStdin($stdin); 91 + $engine->setOriginalArgv(array_slice($argv, 2)); 91 92 92 93 $remote_address = getenv(DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS); 93 94 if (strlen($remote_address)) {
+80 -1
src/applications/diffusion/engine/DiffusionCommitHookEngine.php
··· 19 19 private $viewer; 20 20 private $repository; 21 21 private $stdin; 22 + private $originalArgv; 22 23 private $subversionTransaction; 23 24 private $subversionRepository; 24 25 private $remoteAddress; ··· 84 85 return $this->stdin; 85 86 } 86 87 88 + public function setOriginalArgv(array $original_argv) { 89 + $this->originalArgv = $original_argv; 90 + return $this; 91 + } 92 + 93 + public function getOriginalArgv() { 94 + return $this->originalArgv; 95 + } 96 + 87 97 public function setRepository(PhabricatorRepository $repository) { 88 98 $this->repository = $repository; 89 99 return $this; ··· 141 151 142 152 $this->applyHeraldContentRules($content_updates, $all_updates); 143 153 144 - // TODO: Fire external hooks. 154 + // Run custom scripts in `hook.d/` directories. 155 + $this->applyCustomHooks($all_updates); 145 156 146 157 // If we make it this far, we're accepting these changes. Mark all the 147 158 // logs as accepted. ··· 549 560 } 550 561 551 562 return $content_updates; 563 + } 564 + 565 + /* -( Custom )------------------------------------------------------------- */ 566 + 567 + private function applyCustomHooks(array $updates) { 568 + $args = $this->getOriginalArgv(); 569 + $stdin = $this->getStdin(); 570 + $console = PhutilConsole::getConsole(); 571 + 572 + $env = array( 573 + 'PHABRICATOR_REPOSITORY' => $this->getRepository()->getCallsign(), 574 + self::ENV_USER => $this->getViewer()->getUsername(), 575 + self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(), 576 + self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(), 577 + ); 578 + 579 + $directories = $this->getRepository()->getHookDirectories(); 580 + foreach ($directories as $directory) { 581 + $hooks = $this->getExecutablesInDirectory($directory); 582 + sort($hooks); 583 + foreach ($hooks as $hook) { 584 + // NOTE: We're explicitly running the hooks in sequential order to 585 + // make this more predictable. 586 + $future = id(new ExecFuture('%s %Ls', $hook, $args)) 587 + ->setEnv($env, $wipe_process_env = false) 588 + ->write($stdin); 589 + 590 + list($err, $stdout, $stderr) = $future->resolve(); 591 + if (!$err) { 592 + // This hook ran OK, but echo its output in case there was something 593 + // informative. 594 + $console->writeOut("%s", $stdout); 595 + $console->writeErr("%s", $stderr); 596 + continue; 597 + } 598 + 599 + // Mark everything as rejected by this hook. 600 + foreach ($updates as $update) { 601 + $update->setRejectCode( 602 + PhabricatorRepositoryPushLog::REJECT_EXTERNAL); 603 + $update->setRejectDetails(basename($hook)); 604 + } 605 + 606 + throw new DiffusionCommitHookRejectException( 607 + pht( 608 + "This push was rejected by custom hook script '%s':\n\n%s%s", 609 + basename($hook), 610 + $stdout, 611 + $stderr)); 612 + } 613 + } 614 + } 615 + 616 + private function getExecutablesInDirectory($directory) { 617 + $executables = array(); 618 + 619 + if (!Filesystem::pathExists($directory)) { 620 + return $executables; 621 + } 622 + 623 + foreach (Filesystem::listDirectory($directory) as $path) { 624 + $full_path = $directory.DIRECTORY_SEPARATOR.$path; 625 + if (is_executable($full_path)) { 626 + $executables[] = $full_path; 627 + } 628 + } 629 + 630 + return $executables; 552 631 } 553 632 554 633
+24 -6
src/applications/repository/engine/PhabricatorRepositoryPullEngine.php
··· 103 103 } else if ($is_hg) { 104 104 $this->installMercurialHook(); 105 105 } 106 + 107 + foreach ($repository->getHookDirectories() as $directory) { 108 + $this->installHookDirectory($directory); 109 + } 106 110 } 107 111 108 112 } catch (Exception $ex) { ··· 171 175 172 176 Filesystem::writeFile($path, $hook); 173 177 Filesystem::changePermissions($path, 0755); 178 + } 179 + 180 + private function installHookDirectory($path) { 181 + $readme = pht( 182 + "To add custom hook scripts to this repository, add them to this ". 183 + "directory.\n\nPhabricator will run any executables in this directory ". 184 + "after running its own checks, as though they were normal hook ". 185 + "scripts."); 186 + 187 + Filesystem::createDirectory($path, 0755); 188 + Filesystem::writeFile($path.'/README', $readme); 174 189 } 175 190 176 191 ··· 311 326 */ 312 327 private function installGitHook() { 313 328 $repository = $this->getRepository(); 314 - $path = $repository->getLocalPath(); 329 + $root = $repository->getLocalPath(); 315 330 316 331 if ($repository->isWorkingCopyBare()) { 317 - $path .= '/hooks/pre-receive'; 332 + $path = '/hooks/pre-receive'; 318 333 } else { 319 - $path .= '/.git/hooks/pre-receive'; 334 + $path = '/.git/hooks/pre-receive'; 320 335 } 321 336 322 - $this->installHook($path); 337 + $this->installHook($root.$path); 323 338 } 324 339 325 340 ··· 438 453 execx('svnadmin create -- %s', $path); 439 454 } 440 455 456 + 441 457 /** 442 458 * @task svn 443 459 */ 444 460 private function installSubversionHook() { 445 461 $repository = $this->getRepository(); 446 - $path = $repository->getLocalPath().'/hooks/pre-commit'; 462 + $root = $repository->getLocalPath(); 463 + 464 + $path = '/hooks/pre-commit'; 447 465 448 - $this->installHook($path); 466 + $this->installHook($root.$path); 449 467 } 450 468 451 469
+29
src/applications/repository/storage/PhabricatorRepository.php
··· 945 945 } 946 946 } 947 947 948 + public function getHookDirectories() { 949 + $directories = array(); 950 + if (!$this->isHosted()) { 951 + return $directories; 952 + } 953 + 954 + $root = $this->getLocalPath(); 955 + 956 + switch ($this->getVersionControlSystem()) { 957 + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 958 + if ($this->isWorkingCopyBare()) { 959 + $directories[] = $root.'/hooks/pre-receive-phabricator.d/'; 960 + } else { 961 + $directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/'; 962 + } 963 + break; 964 + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 965 + $directories[] = $root.'/hooks/pre-commit-phabricator.d/'; 966 + break; 967 + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 968 + // NOTE: We don't support custom Mercurial hooks for now because they're 969 + // messy and we can't easily just drop a `hooks.d/` directory next to 970 + // the hooks. 971 + break; 972 + } 973 + 974 + return $directories; 975 + } 976 + 948 977 public function canDestroyWorkingCopy() { 949 978 if ($this->isHosted()) { 950 979 // Never destroy hosted working copies.