diff --git a/scripts/repository/commit_hook.php b/scripts/repository/commit_hook.php index fbd93b1797..86e927f41a 100755 --- a/scripts/repository/commit_hook.php +++ b/scripts/repository/commit_hook.php @@ -1,56 +1,79 @@ #!/usr/bin/env php setViewer(PhabricatorUser::getOmnipotentUser()) - ->withUsernames(array($username)) - ->executeOne(); -if (!$user) { - throw new Exception(pht('No such user "%s"!', $username)); -} - if ($argc < 2) { throw new Exception(pht('usage: commit-hook ')); } +$engine = new DiffusionCommitHookEngine(); + $repository = id(new PhabricatorRepositoryQuery()) - ->setViewer($user) + ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withCallsigns(array($argv[1])) - ->requireCapabilities( - array( - // This capability check is redundant, but can't hurt. - PhabricatorPolicyCapability::CAN_VIEW, - DiffusionCapabilityPush::CAPABILITY, - )) ->executeOne(); if (!$repository) { throw new Exception(pht('No such repository "%s"!', $callsign)); } if (!$repository->isHosted()) { - // This should be redundant too, but double check just in case. + // This should be redundant, but double check just in case. throw new Exception(pht('Repository "%s" is not hosted!', $callsign)); } +$engine->setRepository($repository); + + +// Figure out which user is writing the commit. + +if ($repository->isGit()) { + $username = getenv('PHABRICATOR_USER'); + if (!strlen($username)) { + throw new Exception(pht('usage: PHABRICATOR_USER should be defined!')); + } +} else if ($repository->isSVN()) { + // NOTE: In Subversion, the entire environment gets wiped so we can't read + // PHABRICATOR_USER. Instead, we've set "--tunnel-user" to specify the + // correct user; read this user out of the commit log. + + if ($argc < 4) { + throw new Exception(pht('usage: commit-hook ')); + } + + $svn_repo = $argv[2]; + $svn_txn = $argv[3]; + list($username) = execx('svnlook author -t %s %s', $svn_txn, $svn_repo); + $username = rtrim($username, "\n"); + + $engine->setSubversionTransactionInfo($svn_txn, $svn_repo); +} else { + throw new Exceptiont(pht('Unknown repository type.')); +} + +$user = id(new PhabricatorPeopleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withUsernames(array($username)) + ->executeOne(); + +if (!$user) { + throw new Exception(pht('No such user "%s"!', $username)); +} + +$engine->setViewer($user); + + +// Read stdin for the hook engine. + $stdin = @file_get_contents('php://stdin'); if ($stdin === false) { throw new Exception(pht('Failed to read stdin!')); } -$engine = id(new DiffusionCommitHookEngine()) - ->setViewer($user) - ->setRepository($repository) - ->setStdin($stdin); +$engine->setStdin($stdin); $err = $engine->execute(); exit($err); diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php index bb2db36148..2268f59aef 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php @@ -1,554 +1,559 @@ getDiffusionRequest(); $repository = $drequest->getRepository(); $content = array(); $crumbs = $this->buildCrumbs(); $content[] = $crumbs; $content[] = $this->buildPropertiesTable($drequest->getRepository()); $phids = array(); try { $history_results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'offset' => 0, 'limit' => 15)); $history = DiffusionPathChange::newFromConduit( $history_results['pathChanges']); foreach ($history as $item) { $data = $item->getCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } if ($data->getCommitDetail('committerPHID')) { $phids[$data->getCommitDetail('committerPHID')] = true; } } } $history_exception = null; } catch (Exception $ex) { $history_results = null; $history = null; $history_exception = $ex; } try { $browse_results = DiffusionBrowseResultSet::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.browsequery', array( 'path' => $drequest->getPath(), 'commit' => $drequest->getCommit(), ))); $browse_paths = $browse_results->getPaths(); foreach ($browse_paths as $item) { $data = $item->getLastCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } if ($data->getCommitDetail('committerPHID')) { $phids[$data->getCommitDetail('committerPHID')] = true; } } } $browse_exception = null; } catch (Exception $ex) { $browse_results = null; $browse_paths = null; $browse_exception = $ex; } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); if ($browse_results) { $readme = $this->callConduitWithDiffusionRequest( 'diffusion.readmequery', array( 'paths' => $browse_results->getPathDicts() )); } else { $readme = null; } $content[] = $this->buildHistoryTable( $history_results, $history, $history_exception, $handles); $content[] = $this->buildBrowseTable( $browse_results, $browse_paths, $browse_exception, $handles); try { $content[] = $this->buildTagListTable($drequest); } catch (Exception $ex) { if (!$repository->isImporting()) { $content[] = $this->renderStatusMessage( pht('Unable to Load Tags'), $ex->getMessage()); } } try { $content[] = $this->buildBranchListTable($drequest); } catch (Exception $ex) { if (!$repository->isImporting()) { $content[] = $this->renderStatusMessage( pht('Unable to Load Branches'), $ex->getMessage()); } } if ($readme) { $box = new PHUIBoxView(); $box->setShadow(true); $box->appendChild($readme); $box->addPadding(PHUI::PADDING_LARGE); $panel = new AphrontPanelView(); $panel->setHeader(pht('README')); $panel->setNoBackground(); $panel->appendChild($box); $content[] = $panel; } return $this->buildApplicationPage( $content, array( 'title' => $drequest->getRepository()->getName(), 'device' => true, )); } private function buildPropertiesTable(PhabricatorRepository $repository) { $user = $this->getRequest()->getUser(); $header = id(new PHUIHeaderView()) ->setHeader($repository->getName()) ->setUser($user) ->setPolicyObject($repository); if (!$repository->isTracked()) { $header->setStatus('policy-noone', '', pht('Inactive')); } else if ($repository->isImporting()) { $header->setStatus('time', 'red', pht('Importing...')); } else { $header->setStatus('oh-ok', '', pht('Active')); } $actions = $this->buildActionList($repository); $view = id(new PHUIPropertyListView()) ->setUser($user); $view->addProperty(pht('Callsign'), $repository->getCallsign()); if ($repository->isHosted()) { $serve_off = PhabricatorRepository::SERVE_OFF; $callsign = $repository->getCallsign(); $repo_path = '/diffusion/'.$callsign.'/'; $serve_ssh = $repository->getServeOverSSH(); if ($serve_ssh !== $serve_off) { $uri = new PhutilURI(PhabricatorEnv::getProductionURI($repo_path)); - $uri->setProtocol('ssh'); + + if ($repository->isSVN()) { + $uri->setProtocol('svn+ssh'); + } else { + $uri->setProtocol('ssh'); + } $ssh_user = PhabricatorEnv::getEnvConfig('diffusion.ssh-user'); if ($ssh_user) { $uri->setUser($ssh_user); } // This isn't quite right, but for now it's probably better to drop // the port than sometimes show ":8080", etc. Using most VCS commands // against nonstandard ports is a huge pain so few installs are likely // to configure SSH on a nonstandard port. $uri->setPort(null); $clone_uri = $this->renderCloneURI( $uri, $serve_ssh, '/settings/panel/ssh/'); $view->addProperty(pht('Clone URI (SSH)'), $clone_uri); } $serve_http = $repository->getServeOverHTTP(); if ($serve_http !== $serve_off) { $http_uri = PhabricatorEnv::getProductionURI($repo_path); $clone_uri = $this->renderCloneURI( $http_uri, $serve_http, PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth') ? '/settings/panel/vcspassword/' : null); $view->addProperty(pht('Clone URI (HTTP)'), $clone_uri); } } else { switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $view->addProperty( pht('Clone URI'), $this->renderCloneURI( $repository->getPublicRemoteURI())); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $view->addProperty( pht('Repository Root'), $this->renderCloneURI( $repository->getPublicRemoteURI())); break; } } $description = $repository->getDetail('description'); if (strlen($description)) { $description = PhabricatorMarkupEngine::renderOneObject( $repository, 'description', $user); $view->addSectionHeader(pht('Description')); $view->addTextContent($description); } $view->setActionList($actions); return id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($view); } private function buildBranchListTable(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); if ($drequest->getBranch() === null) { return null; } $limit = 15; $branches = DiffusionBranchInformation::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.branchquery', array( 'limit' => $limit + 1, ))); if (!$branches) { return null; } $more_branches = (count($branches) > $limit); $branches = array_slice($branches, 0, $limit); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers(mpull($branches, 'getHeadCommitIdentifier')) ->withRepository($drequest->getRepository()) ->execute(); $table = id(new DiffusionBranchTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setBranches($branches) ->setCommits($commits); $panel = id(new AphrontPanelView()) ->setHeader(pht('Branches')) ->setNoBackground(); if ($more_branches) { $panel->setCaption(pht('Showing %d branches.', $limit)); } $panel->addButton( phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'branches', )), 'class' => 'grey button', ), pht("Show All Branches \xC2\xBB"))); $panel->appendChild($table); return $panel; } private function buildTagListTable(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); $tag_limit = 15; $tags = array(); try { $tags = DiffusionRepositoryTag::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.tagsquery', array( // On the home page, we want to find tags on any branch. 'commit' => null, 'limit' => $tag_limit + 1, ))); } catch (ConduitException $e) { if ($e->getMessage() != 'ERR-UNSUPPORTED-VCS') { throw $e; } } if (!$tags) { return null; } $more_tags = (count($tags) > $tag_limit); $tags = array_slice($tags, 0, $tag_limit); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers(mpull($tags, 'getCommitIdentifier')) ->withRepository($drequest->getRepository()) ->needCommitData(true) ->execute(); $view = id(new DiffusionTagListView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setTags($tags) ->setCommits($commits); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $panel = id(new AphrontPanelView()) ->setHeader(pht('Tags')) ->setNoBackground(true); if ($more_tags) { $panel->setCaption(pht('Showing the %d most recent tags.', $tag_limit)); } $panel->addButton( phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'tags', )), 'class' => 'grey button', ), pht("Show All Tags \xC2\xBB"))); $panel->appendChild($view); return $panel; } private function buildActionList(PhabricatorRepository $repository) { $viewer = $this->getRequest()->getUser(); $view_uri = $this->getApplicationURI($repository->getCallsign().'/'); $edit_uri = $this->getApplicationURI($repository->getCallsign().'/edit/'); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($repository) ->setObjectURI($view_uri); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Repository')) ->setIcon('edit') ->setHref($edit_uri) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); return $view; } private function buildHistoryTable( $history_results, $history, $history_exception, array $handles) { $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); if ($history_exception) { if ($repository->isImporting()) { return $this->renderStatusMessage( pht('Still Importing...'), pht( 'This repository is still importing. History is not yet '. 'available.')); } else { return $this->renderStatusMessage( pht('Unable to Retrieve History'), $history_exception->getMessage()); } } $history_table = id(new DiffusionHistoryTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setHandles($handles) ->setHistory($history); // TODO: Super sketchy. $history_table->loadRevisions(); if ($history_results) { $history_table->setParents($history_results['parents']); } $history_table->setIsHead(true); $callsign = $drequest->getRepository()->getCallsign(); $all = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'history', )), ), pht('View Full Commit History')); $panel = new AphrontPanelView(); $panel->setHeader(pht("Recent Commits · %s", $all)); $panel->appendChild($history_table); $panel->setNoBackground(); return $panel; } private function buildBrowseTable( $browse_results, $browse_paths, $browse_exception, array $handles) { $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); if ($browse_exception) { if ($repository->isImporting()) { // The history table renders a useful message. return null; } else { return $this->renderStatusMessage( pht('Unable to Retrieve Paths'), $browse_exception->getMessage()); } } $browse_table = id(new DiffusionBrowseTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setHandles($handles); if ($browse_paths) { $browse_table->setPaths($browse_paths); } else { $browse_table->setPaths(array()); } $browse_uri = $drequest->generateURI(array('action' => 'browse')); $browse_panel = new AphrontPanelView(); $browse_panel->setHeader( phutil_tag( 'a', array('href' => $browse_uri), pht('Browse Repository'))); $browse_panel->appendChild($browse_table); $browse_panel->setNoBackground(); return $browse_panel; } private function renderCloneURI( $uri, $serve_mode = null, $manage_uri = null) { require_celerity_resource('diffusion-icons-css'); Javelin::initBehavior('select-on-click'); $input = javelin_tag( 'input', array( 'type' => 'text', 'value' => (string)$uri, 'class' => 'diffusion-clone-uri', 'sigil' => 'select-on-click', )); $extras = array(); if ($serve_mode) { if ($serve_mode === PhabricatorRepository::SERVE_READONLY) { $extras[] = pht('(Read Only)'); } } if ($manage_uri) { if ($this->getRequest()->getUser()->isLoggedIn()) { $extras[] = phutil_tag( 'a', array( 'href' => $manage_uri, ), pht('Manage Credentials')); } } if ($extras) { $extras = phutil_implode_html(' ', $extras); $extras = phutil_tag( 'div', array( 'class' => 'diffusion-clone-extras', ), $extras); } return array($input, $extras); } } diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php index ca24d0271b..282ae16697 100644 --- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -1,76 +1,95 @@ subversionTransaction = $transaction; + $this->subversionRepository = $repository; + return $this; + } public function setStdin($stdin) { $this->stdin = $stdin; return $this; } public function getStdin() { return $this->stdin; } public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->repository; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function execute() { $type = $this->getRepository()->getVersionControlSystem(); switch ($type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $err = $this->executeGitHook(); break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $err = $this->executeSubversionHook(); + break; default: throw new Exception(pht('Unsupported repository type "%s"!', $type)); } return $err; } private function executeGitHook() { $updates = $this->parseGitUpdates($this->getStdin()); // TODO: Do useful things. return 0; } + private function executeSubversionHook() { + + // TODO: Do useful things here, too. + + return 0; + } + private function parseGitUpdates($stdin) { $updates = array(); $lines = phutil_split_lines($stdin, $retain_endings = false); foreach ($lines as $line) { $parts = explode(' ', $line, 3); if (count($parts) != 3) { throw new Exception(pht('Expected "old new ref", got "%s".', $line)); } $updates[] = array( 'old' => $parts[0], 'new' => $parts[1], 'ref' => $parts[2], ); } return $updates; } } diff --git a/src/applications/diffusion/ssh/DiffusionSSHSubversionServeWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHSubversionServeWorkflow.php index 23e5c6f28e..e4efa50d17 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHSubversionServeWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHSubversionServeWorkflow.php @@ -1,262 +1,264 @@ setName('svnserve'); $this->setArguments( array( array( 'name' => 'tunnel', 'short' => 't', ), )); } protected function executeRepositoryOperations() { $args = $this->getArgs(); if (!$args->getArg('tunnel')) { throw new Exception("Expected `svnserve -t`!"); } - $command = csprintf('svnserve -t'); + $command = csprintf( + 'svnserve -t --tunnel-user=%s', + $this->getUser()->getUsername()); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $future = new ExecFuture('%C', $command); $this->inProtocol = new DiffusionSubversionWireProtocol(); $this->outProtocol = new DiffusionSubversionWireProtocol(); $err = id($this->newPassthruCommand()) ->setIOChannel($this->getIOChannel()) ->setCommandChannelFromExecFuture($future) ->setWillWriteCallback(array($this, 'willWriteMessageCallback')) ->setWillReadCallback(array($this, 'willReadMessageCallback')) ->execute(); if (!$err && $this->didSeeWrite) { $this->getRepository()->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, PhabricatorRepositoryStatusMessage::CODE_OKAY); } return $err; } public function willWriteMessageCallback( PhabricatorSSHPassthruCommand $command, $message) { $proto = $this->inProtocol; $messages = $proto->writeData($message); $result = array(); foreach ($messages as $message) { $message_raw = $message['raw']; $struct = $message['structure']; if (!$this->inSeenGreeting) { $this->inSeenGreeting = true; // The first message the client sends looks like: // // ( version ( cap1 ... ) url ... ) // // We want to grab the URL, load the repository, make sure it exists and // is accessible, and then replace it with the location of the // repository on disk. $uri = $struct[2]['value']; $struct[2]['value'] = $this->makeInternalURI($uri); $message_raw = $proto->serializeStruct($struct); } else if (isset($struct[0]) && $struct[0]['type'] == 'word') { if (!$proto->isReadOnlyCommand($struct)) { $this->didSeeWrite = true; $this->requireWriteAccess($struct[0]['value']); } // Several other commands also pass in URLs. We need to translate // all of these into the internal representation; this also makes sure // they're valid and accessible. switch ($struct[0]['value']) { case 'reparent': // ( reparent ( url ) ) $struct[1]['value'][0]['value'] = $this->makeInternalURI( $struct[1]['value'][0]['value']); $message_raw = $proto->serializeStruct($struct); break; case 'switch': // ( switch ( ( rev ) target recurse url ... ) ) $struct[1]['value'][3]['value'] = $this->makeInternalURI( $struct[1]['value'][3]['value']); $message_raw = $proto->serializeStruct($struct); break; case 'diff': // ( diff ( ( rev ) target recurse ignore-ancestry url ... ) ) $struct[1]['value'][4]['value'] = $this->makeInternalURI( $struct[1]['value'][4]['value']); $message_raw = $proto->serializeStruct($struct); break; } } $result[] = $message_raw; } if (!$result) { return null; } return implode('', $result); } public function willReadMessageCallback( PhabricatorSSHPassthruCommand $command, $message) { $proto = $this->outProtocol; $messages = $proto->writeData($message); $result = array(); foreach ($messages as $message) { $message_raw = $message['raw']; $struct = $message['structure']; if (isset($struct[0]) && ($struct[0]['type'] == 'word')) { if ($struct[0]['value'] == 'success') { switch ($this->outPhaseCount) { case 0: // This is the "greeting", which announces capabilities. break; case 1: // This responds to the client greeting, and announces auth. break; case 2: // This responds to auth, which should be trivial over SSH. break; case 3: // This contains the URI of the repository. We need to edit it; // if it does not match what the client requested it will reject // the response. $struct[1]['value'][1]['value'] = $this->makeExternalURI( $struct[1]['value'][1]['value']); $message_raw = $proto->serializeStruct($struct); break; default: // We don't care about other protocol frames. break; } $this->outPhaseCount++; } else if ($struct[0]['value'] == 'failure') { // Find any error messages which include the internal URI, and // replace the text with the external URI. foreach ($struct[1]['value'] as $key => $error) { $code = $error['value'][0]['value']; $message = $error['value'][1]['value']; $message = str_replace( $this->internalBaseURI, $this->externalBaseURI, $message); // Derp derp derp derp derp. The structure looks like this: // ( failure ( ( code message ... ) ... ) ) $struct[1]['value'][$key]['value'][1]['value'] = $message; } $message_raw = $proto->serializeStruct($struct); } } $result[] = $message_raw; } if (!$result) { return null; } return implode('', $result); } private function makeInternalURI($uri_string) { $uri = new PhutilURI($uri_string); $proto = $uri->getProtocol(); if ($proto !== 'svn+ssh') { throw new Exception( pht( 'Protocol for URI "%s" MUST be "svn+ssh".', $uri_string)); } $path = $uri->getPath(); // Subversion presumably deals with this, but make sure there's nothing // skethcy going on with the URI. if (preg_match('(/\\.\\./)', $path)) { throw new Exception( pht( 'String "/../" is invalid in path specification "%s".', $uri_string)); } $repository = $this->loadRepository($path); $path = preg_replace( '(^/diffusion/[A-Z]+)', rtrim($repository->getLocalPath(), '/'), $path); if (preg_match('(^/diffusion/[A-Z]+/$)', $path)) { $path = rtrim($path, '/'); } $uri->setPath($path); // If this is happening during the handshake, these are the base URIs for // the request. if ($this->externalBaseURI === null) { $pre = (string)id(clone $uri)->setPath(''); $this->externalBaseURI = $pre.'/diffusion/'.$repository->getCallsign(); $this->internalBaseURI = $pre.rtrim($repository->getLocalPath(), '/'); } return (string)$uri; } private function makeExternalURI($uri) { $internal = $this->internalBaseURI; $external = $this->externalBaseURI; if (strncmp($uri, $internal, strlen($internal)) === 0) { $uri = $external.substr($uri, strlen($internal)); } return $uri; } } diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index 1ab6818a46..ab3c759fae 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -1,398 +1,410 @@ getRepository(); $is_hg = false; $is_git = false; $is_svn = false; $vcs = $repository->getVersionControlSystem(); $callsign = $repository->getCallsign(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // We never pull a local copy of non-hosted Subversion repositories. if (!$repository->isHosted()) { $this->skipPull( pht( "Repository '%s' is a non-hosted Subversion repository, which ". "does not require a local working copy to be pulled.", $callsign)); return; } $is_svn = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $is_git = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $is_hg = true; break; default: $this->abortPull(pht('Unknown VCS "%s"!', $vcs)); } $callsign = $repository->getCallsign(); $local_path = $repository->getLocalPath(); if ($local_path === null) { $this->abortPull( pht( "No local path is configured for repository '%s'.", $callsign)); } try { $dirname = dirname($local_path); if (!Filesystem::pathExists($dirname)) { Filesystem::createDirectory($dirname, 0755, $recursive = true); } if (!Filesystem::pathExists($local_path)) { $this->logPull( pht( "Creating a new working copy for repository '%s'.", $callsign)); if ($is_git) { $this->executeGitCreate(); } else if ($is_hg) { $this->executeMercurialCreate(); } else { $this->executeSubversionCreate(); } } else { if ($repository->isHosted()) { if ($is_git) { $this->installGitHook(); + } else if ($is_svn) { + $this->installSubversionHook(); } else { $this->logPull( pht( "Repository '%s' is hosted, so Phabricator does not pull ". "updates for it.", $callsign)); } } else { $this->logPull( pht( "Updating the working copy for repository '%s'.", $callsign)); if ($is_git) { $this->executeGitUpdate(); } else { $this->executeMercurialUpdate(); } } } } catch (Exception $ex) { $this->abortPull( pht('Pull of "%s" failed: %s', $callsign, $ex->getMessage()), $ex); } $this->donePull(); return $this; } private function skipPull($message) { $this->log('%s', $message); $this->donePull(); } private function abortPull($message, Exception $ex = null) { $code_error = PhabricatorRepositoryStatusMessage::CODE_ERROR; $this->updateRepositoryInitStatus($code_error, $message); if ($ex) { throw $ex; } else { throw new Exception($message); } } private function logPull($message) { $code_working = PhabricatorRepositoryStatusMessage::CODE_WORKING; $this->updateRepositoryInitStatus($code_working, $message); $this->log('%s', $message); } private function donePull() { $code_okay = PhabricatorRepositoryStatusMessage::CODE_OKAY; $this->updateRepositoryInitStatus($code_okay); } private function updateRepositoryInitStatus($code, $message = null) { $this->getRepository()->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_INIT, $code, array( 'message' => $message )); } private function installHook($path) { $this->log('%s', pht('Installing commit hook to "%s"...', $path)); $repository = $this->getRepository(); $callsign = $repository->getCallsign(); $root = dirname(phutil_get_library_root('phabricator')); $bin = $root.'/bin/commit-hook'; - $cmd = csprintf('exec -- %s %s', $bin, $callsign); + $cmd = csprintf('exec -- %s %s "$@"', $bin, $callsign); $hook = "#!/bin/sh\n{$cmd}\n"; Filesystem::writeFile($path, $hook); Filesystem::changePermissions($path, 0755); } /* -( Pulling Git Working Copies )----------------------------------------- */ /** * @task git */ private function executeGitCreate() { $repository = $this->getRepository(); $path = rtrim($repository->getLocalPath(), '/'); if ($repository->isHosted()) { $repository->execxRemoteCommand( 'init --bare -- %s', $path); } else { $repository->execxRemoteCommand( 'clone --bare -- %s %s', $repository->getRemoteURI(), $path); } } /** * @task git */ private function executeGitUpdate() { $repository = $this->getRepository(); list($err, $stdout) = $repository->execLocalCommand( 'rev-parse --show-toplevel'); $message = null; $path = $repository->getLocalPath(); if ($err) { // Try to raise a more tailored error message in the more common case // of the user creating an empty directory. (We could try to remove it, // but might not be able to, and it's much simpler to raise a good // message than try to navigate those waters.) if (is_dir($path)) { $files = Filesystem::listDirectory($path, $include_hidden = true); if (!$files) { $message = "Expected to find a git repository at '{$path}', but there ". "is an empty directory there. Remove the directory: the daemon ". "will run 'git clone' for you."; } else { $message = "Expected to find a git repository at '{$path}', but there is ". "a non-repository directory (with other stuff in it) there. Move ". "or remove this directory (or reconfigure the repository to use a ". "different directory), and then either clone a repository ". "yourself or let the daemon do it."; } } else if (is_file($path)) { $message = "Expected to find a git repository at '{$path}', but there is a ". "file there instead. Remove it and let the daemon clone a ". "repository for you."; } else { $message = "Expected to find a git repository at '{$path}', but did not."; } } else { $repo_path = rtrim($stdout, "\n"); if (empty($repo_path)) { // This can mean one of two things: we're in a bare repository, or // we're inside a git repository inside another git repository. Since // the first is dramatically more likely now that we perform bare // clones and I don't have a great way to test for the latter, assume // we're OK. } else if (!Filesystem::pathsAreEquivalent($repo_path, $path)) { $err = true; $message = "Expected to find repo at '{$path}', but the actual ". "git repository root for this directory is '{$repo_path}'. ". "Something is misconfigured. The repository's 'Local Path' should ". "be set to some place where the daemon can check out a working ". "copy, and should not be inside another git repository."; } } if ($err && $repository->canDestroyWorkingCopy()) { phlog("Repository working copy at '{$path}' failed sanity check; ". "destroying and re-cloning. {$message}"); Filesystem::remove($path); $this->executeGitCreate(); } else if ($err) { throw new Exception($message); } $retry = false; do { // This is a local command, but needs credentials. if ($repository->isWorkingCopyBare()) { // For bare working copies, we need this magic incantation. $future = $repository->getRemoteCommandFuture( 'fetch origin %s --prune', '+refs/heads/*:refs/heads/*'); } else { $future = $repository->getRemoteCommandFuture( 'fetch --all --prune'); } $future->setCWD($path); list($err, $stdout, $stderr) = $future->resolve(); if ($err && !$retry && $repository->canDestroyWorkingCopy()) { $retry = true; // Fix remote origin url if it doesn't match our configuration $origin_url = $repository->execLocalCommand( 'config --get remote.origin.url'); $remote_uri = $repository->getDetail('remote-uri'); if ($origin_url != $remote_uri) { $repository->execLocalCommand( 'remote set-url origin %s', $remote_uri); } } else if ($err) { throw new Exception( "git fetch failed with error #{$err}:\n". "stdout:{$stdout}\n\n". "stderr:{$stderr}\n"); } else { $retry = false; } } while ($retry); } /** * @task git */ private function installGitHook() { $repository = $this->getRepository(); $path = $repository->getLocalPath(); if ($repository->isWorkingCopyBare()) { $path .= 'hooks/pre-receive'; } else { $path .= '.git/hooks/pre-receive'; } $this->installHook($path); } /* -( Pulling Mercurial Working Copies )----------------------------------- */ /** * @task hg */ private function executeMercurialCreate() { $repository = $this->getRepository(); $path = rtrim($repository->getLocalPath(), '/'); if ($repository->isHosted()) { $repository->execxRemoteCommand( 'init -- %s', $path); } else { $repository->execxRemoteCommand( 'clone -- %s %s', $repository->getRemoteURI(), $path); } } /** * @task hg */ private function executeMercurialUpdate() { $repository = $this->getRepository(); $path = $repository->getLocalPath(); // This is a local command, but needs credentials. $future = $repository->getRemoteCommandFuture('pull -u'); $future->setCWD($path); try { $future->resolvex(); } catch (CommandException $ex) { $err = $ex->getError(); $stdout = $ex->getStdOut(); // NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the behavior // of "hg pull" to return 1 in case of a successful pull with no changes. // This behavior has been reverted, but users who updated between Feb 1, // 2012 and Mar 1, 2012 will have the erroring version. Do a dumb test // against stdout to check for this possibility. // See: https://github.com/facebook/phabricator/issues/101/ // NOTE: Mercurial has translated versions, which translate this error // string. In a translated version, the string will be something else, // like "aucun changement trouve". There didn't seem to be an easy way // to handle this (there are hard ways but this is not a common problem // and only creates log spam, not application failures). Assume English. // TODO: Remove this once we're far enough in the future that deployment // of 2.1 is exceedingly rare? if ($err == 1 && preg_match('/no changes found/', $stdout)) { return; } else { throw $ex; } } } /* -( Pulling Subversion Working Copies )---------------------------------- */ /** * @task svn */ private function executeSubversionCreate() { $repository = $this->getRepository(); $path = rtrim($repository->getLocalPath(), '/'); execx('svnadmin create -- %s', $path); } + /** + * @task svn + */ + private function installSubversionHook() { + $repository = $this->getRepository(); + $path = $repository->getLocalPath().'hooks/pre-commit'; + + $this->installHook($path); + } + }