mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-15 19:32:40 +01:00
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
This commit is contained in:
parent
2cfc3acf32
commit
972dfa7bfc
4 changed files with 134 additions and 7 deletions
|
@ -88,6 +88,7 @@ if ($repository->isHg()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$engine->setStdin($stdin);
|
$engine->setStdin($stdin);
|
||||||
|
$engine->setOriginalArgv(array_slice($argv, 2));
|
||||||
|
|
||||||
$remote_address = getenv(DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS);
|
$remote_address = getenv(DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS);
|
||||||
if (strlen($remote_address)) {
|
if (strlen($remote_address)) {
|
||||||
|
|
|
@ -19,6 +19,7 @@ final class DiffusionCommitHookEngine extends Phobject {
|
||||||
private $viewer;
|
private $viewer;
|
||||||
private $repository;
|
private $repository;
|
||||||
private $stdin;
|
private $stdin;
|
||||||
|
private $originalArgv;
|
||||||
private $subversionTransaction;
|
private $subversionTransaction;
|
||||||
private $subversionRepository;
|
private $subversionRepository;
|
||||||
private $remoteAddress;
|
private $remoteAddress;
|
||||||
|
@ -84,6 +85,15 @@ final class DiffusionCommitHookEngine extends Phobject {
|
||||||
return $this->stdin;
|
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) {
|
public function setRepository(PhabricatorRepository $repository) {
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -141,7 +151,8 @@ final class DiffusionCommitHookEngine extends Phobject {
|
||||||
|
|
||||||
$this->applyHeraldContentRules($content_updates, $all_updates);
|
$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
|
// If we make it this far, we're accepting these changes. Mark all the
|
||||||
// logs as accepted.
|
// logs as accepted.
|
||||||
|
@ -551,6 +562,74 @@ final class DiffusionCommitHookEngine extends Phobject {
|
||||||
return $content_updates;
|
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 )---------------------------------------------------------- */
|
/* -( Mercurial )---------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,10 @@ final class PhabricatorRepositoryPullEngine
|
||||||
} else if ($is_hg) {
|
} else if ($is_hg) {
|
||||||
$this->installMercurialHook();
|
$this->installMercurialHook();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($repository->getHookDirectories() as $directory) {
|
||||||
|
$this->installHookDirectory($directory);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
|
@ -173,6 +177,17 @@ final class PhabricatorRepositoryPullEngine
|
||||||
Filesystem::changePermissions($path, 0755);
|
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 )----------------------------------------- */
|
/* -( Pulling Git Working Copies )----------------------------------------- */
|
||||||
|
|
||||||
|
@ -311,15 +326,15 @@ final class PhabricatorRepositoryPullEngine
|
||||||
*/
|
*/
|
||||||
private function installGitHook() {
|
private function installGitHook() {
|
||||||
$repository = $this->getRepository();
|
$repository = $this->getRepository();
|
||||||
$path = $repository->getLocalPath();
|
$root = $repository->getLocalPath();
|
||||||
|
|
||||||
if ($repository->isWorkingCopyBare()) {
|
if ($repository->isWorkingCopyBare()) {
|
||||||
$path .= '/hooks/pre-receive';
|
$path = '/hooks/pre-receive';
|
||||||
} else {
|
} 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);
|
execx('svnadmin create -- %s', $path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @task svn
|
* @task svn
|
||||||
*/
|
*/
|
||||||
private function installSubversionHook() {
|
private function installSubversionHook() {
|
||||||
$repository = $this->getRepository();
|
$repository = $this->getRepository();
|
||||||
$path = $repository->getLocalPath().'/hooks/pre-commit';
|
$root = $repository->getLocalPath();
|
||||||
|
|
||||||
$this->installHook($path);
|
$path = '/hooks/pre-commit';
|
||||||
|
|
||||||
|
$this->installHook($root.$path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
public function canDestroyWorkingCopy() {
|
||||||
if ($this->isHosted()) {
|
if ($this->isHosted()) {
|
||||||
// Never destroy hosted working copies.
|
// Never destroy hosted working copies.
|
||||||
|
|
Loading…
Reference in a new issue