@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

Install pre-commit hooks in Git repositories

Summary:
Ref T4189. T4189 describes most of the intent here:

- When updating hosted repositories, sync a pre-commit hook into them instead of doing a `git fetch`.
- The hook calls into Phabricator. The acting Phabricator user is sent via PHABRICATOR_USER in the environment. The active repository is sent via CLI.
- The hook doesn't do anything useful yet; it just veifies basic parameters, does a little parsing, and exits 0 to allow the commit.

Test Plan:
- Performed Git pushes and pulls over SSH and HTTP.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T4189

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

+189 -7
+1
bin/commit-hook
··· 1 + ../scripts/repository/commit_hook.php
+56
scripts/repository/commit_hook.php
··· 1 + #!/usr/bin/env php 2 + <?php 3 + 4 + $root = dirname(dirname(dirname(__FILE__))); 5 + require_once $root.'/scripts/__init_script__.php'; 6 + 7 + $username = getenv('PHABRICATOR_USER'); 8 + if (!$username) { 9 + throw new Exception(pht('usage: define PHABRICATOR_USER in environment')); 10 + } 11 + 12 + $user = id(new PhabricatorPeopleQuery()) 13 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 14 + ->withUsernames(array($username)) 15 + ->executeOne(); 16 + if (!$user) { 17 + throw new Exception(pht('No such user "%s"!', $username)); 18 + } 19 + 20 + if ($argc < 2) { 21 + throw new Exception(pht('usage: commit-hook <callsign>')); 22 + } 23 + 24 + $repository = id(new PhabricatorRepositoryQuery()) 25 + ->setViewer($user) 26 + ->withCallsigns(array($argv[1])) 27 + ->requireCapabilities( 28 + array( 29 + // This capability check is redundant, but can't hurt. 30 + PhabricatorPolicyCapability::CAN_VIEW, 31 + DiffusionCapabilityPush::CAPABILITY, 32 + )) 33 + ->executeOne(); 34 + 35 + if (!$repository) { 36 + throw new Exception(pht('No such repository "%s"!', $callsign)); 37 + } 38 + 39 + if (!$repository->isHosted()) { 40 + // This should be redundant too, but double check just in case. 41 + throw new Exception(pht('Repository "%s" is not hosted!', $callsign)); 42 + } 43 + 44 + $stdin = @file_get_contents('php://stdin'); 45 + if ($stdin === false) { 46 + throw new Exception(pht('Failed to read stdin!')); 47 + } 48 + 49 + $engine = id(new DiffusionCommitHookEngine()) 50 + ->setViewer($user) 51 + ->setRepository($repository) 52 + ->setStdin($stdin); 53 + 54 + $err = $engine->execute(); 55 + 56 + exit($err);
+2
src/__phutil_library_map__.php
··· 474 474 'DiffusionCommitChangeTableView' => 'applications/diffusion/view/DiffusionCommitChangeTableView.php', 475 475 'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php', 476 476 'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php', 477 + 'DiffusionCommitHookEngine' => 'applications/diffusion/engine/DiffusionCommitHookEngine.php', 477 478 'DiffusionCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionCommitParentsQuery.php', 478 479 'DiffusionCommitQuery' => 'applications/diffusion/query/DiffusionCommitQuery.php', 479 480 'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php', ··· 2797 2798 'DiffusionCommitChangeTableView' => 'DiffusionView', 2798 2799 'DiffusionCommitController' => 'DiffusionController', 2799 2800 'DiffusionCommitEditController' => 'DiffusionController', 2801 + 'DiffusionCommitHookEngine' => 'Phobject', 2800 2802 'DiffusionCommitParentsQuery' => 'DiffusionQuery', 2801 2803 'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 2802 2804 'DiffusionCommitTagsController' => 'DiffusionController',
+1
src/applications/diffusion/controller/DiffusionServeController.php
··· 318 318 'PATH_INFO' => $request_path, 319 319 320 320 'REMOTE_USER' => $viewer->getUsername(), 321 + 'PHABRICATOR_USER' => $viewer->getUsername(), 321 322 322 323 // TODO: Set these correctly. 323 324 // GIT_COMMITTER_NAME
+76
src/applications/diffusion/engine/DiffusionCommitHookEngine.php
··· 1 + <?php 2 + 3 + final class DiffusionCommitHookEngine extends Phobject { 4 + 5 + private $viewer; 6 + private $repository; 7 + private $stdin; 8 + 9 + public function setStdin($stdin) { 10 + $this->stdin = $stdin; 11 + return $this; 12 + } 13 + 14 + public function getStdin() { 15 + return $this->stdin; 16 + } 17 + 18 + public function setRepository(PhabricatorRepository $repository) { 19 + $this->repository = $repository; 20 + return $this; 21 + } 22 + 23 + public function getRepository() { 24 + return $this->repository; 25 + } 26 + 27 + public function setViewer(PhabricatorUser $viewer) { 28 + $this->viewer = $viewer; 29 + return $this; 30 + } 31 + 32 + public function getViewer() { 33 + return $this->viewer; 34 + } 35 + 36 + public function execute() { 37 + $type = $this->getRepository()->getVersionControlSystem(); 38 + switch ($type) { 39 + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 40 + $err = $this->executeGitHook(); 41 + break; 42 + default: 43 + throw new Exception(pht('Unsupported repository type "%s"!', $type)); 44 + } 45 + 46 + return $err; 47 + } 48 + 49 + private function executeGitHook() { 50 + $updates = $this->parseGitUpdates($this->getStdin()); 51 + 52 + // TODO: Do useful things. 53 + 54 + return 0; 55 + } 56 + 57 + private function parseGitUpdates($stdin) { 58 + $updates = array(); 59 + 60 + $lines = phutil_split_lines($stdin, $retain_endings = false); 61 + foreach ($lines as $line) { 62 + $parts = explode(' ', $line, 3); 63 + if (count($parts) != 3) { 64 + throw new Exception(pht('Expected "old new ref", got "%s".', $line)); 65 + } 66 + $updates[] = array( 67 + 'old' => $parts[0], 68 + 'new' => $parts[1], 69 + 'ref' => $parts[2], 70 + ); 71 + } 72 + 73 + return $updates; 74 + } 75 + 76 + }
+2 -1
src/applications/diffusion/ssh/DiffusionSSHGitReceivePackWorkflow.php
··· 25 25 $command = csprintf('git-receive-pack %s', $repository->getLocalPath()); 26 26 $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); 27 27 28 - $future = new ExecFuture('%C', $command); 28 + $future = id(new ExecFuture('%C', $command)) 29 + ->setEnv($this->getEnvironment()); 29 30 30 31 $err = $this->newPassthruCommand() 31 32 ->setIOChannel($this->getIOChannel())
+2 -1
src/applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php
··· 22 22 $command = csprintf('git-upload-pack -- %s', $repository->getLocalPath()); 23 23 $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); 24 24 25 - $future = new ExecFuture('%C', $command); 25 + $future = id(new ExecFuture('%C', $command)) 26 + ->setEnv($this->getEnvironment()); 26 27 27 28 $err = $this->newPassthruCommand() 28 29 ->setIOChannel($this->getIOChannel())
+6
src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
··· 17 17 return $this->args; 18 18 } 19 19 20 + public function getEnvironment() { 21 + return array( 22 + 'PHABRICATOR_USER' => $this->getUser()->getUsername(), 23 + ); 24 + } 25 + 20 26 abstract protected function executeRepositoryOperations(); 21 27 22 28 protected function writeError($message) {
+43 -5
src/applications/repository/engine/PhabricatorRepositoryPullEngine.php
··· 83 83 } 84 84 } else { 85 85 if ($repository->isHosted()) { 86 - $this->logPull( 87 - pht( 88 - "Repository '%s' is hosted, so Phabricator does not pull ". 89 - "updates for it.", 90 - $callsign)); 86 + if ($is_git) { 87 + $this->installGitHook(); 88 + } else { 89 + $this->logPull( 90 + pht( 91 + "Repository '%s' is hosted, so Phabricator does not pull ". 92 + "updates for it.", 93 + $callsign)); 94 + } 91 95 } else { 92 96 $this->logPull( 93 97 pht( ··· 146 150 )); 147 151 } 148 152 153 + private function installHook($path) { 154 + $this->log('%s', pht('Installing commit hook to "%s"...', $path)); 155 + 156 + $repository = $this->getRepository(); 157 + $callsign = $repository->getCallsign(); 158 + 159 + $root = dirname(phutil_get_library_root('phabricator')); 160 + $bin = $root.'/bin/commit-hook'; 161 + $cmd = csprintf('exec -- %s %s', $bin, $callsign); 162 + 163 + $hook = "#!/bin/sh\n{$cmd}\n"; 164 + 165 + Filesystem::writeFile($path, $hook); 166 + Filesystem::changePermissions($path, 0755); 167 + } 168 + 149 169 150 170 /* -( Pulling Git Working Copies )----------------------------------------- */ 151 171 ··· 279 299 } 280 300 281 301 302 + /** 303 + * @task git 304 + */ 305 + private function installGitHook() { 306 + $repository = $this->getRepository(); 307 + $path = $repository->getLocalPath(); 308 + 309 + if ($repository->isWorkingCopyBare()) { 310 + $path .= 'hooks/pre-receive'; 311 + } else { 312 + $path .= '.git/hooks/pre-receive'; 313 + } 314 + 315 + $this->installHook($path); 316 + } 317 + 318 + 282 319 /* -( Pulling Mercurial Working Copies )----------------------------------- */ 283 320 284 321 ··· 356 393 $path = rtrim($repository->getLocalPath(), '/'); 357 394 execx('svnadmin create -- %s', $path); 358 395 } 396 + 359 397 360 398 }