diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -614,6 +614,7 @@ 'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php', 'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php', 'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php', + 'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php', 'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php', 'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php', 'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php', @@ -622,6 +623,7 @@ 'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php', 'DoorkeeperFeedWorkerAsana' => 'applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php', 'DoorkeeperFeedWorkerJIRA' => 'applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php', + 'DoorkeeperGitHubHookController' => 'applications/doorkeeper/controller/DoorkeeperGitHubHookController.php', 'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php', 'DoorkeeperMissingLinkException' => 'applications/doorkeeper/exception/DoorkeeperMissingLinkException.php', 'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php', @@ -3195,6 +3197,7 @@ 'DivinerWorkflow' => 'PhabricatorManagementWorkflow', 'DoorkeeperBridge' => 'Phobject', 'DoorkeeperBridgeAsana' => 'DoorkeeperBridge', + 'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge', 'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge', 'DoorkeeperDAO' => 'PhabricatorLiskDAO', 'DoorkeeperExternalObject' => @@ -3206,6 +3209,7 @@ 'DoorkeeperFeedWorker' => 'FeedPushWorker', 'DoorkeeperFeedWorkerAsana' => 'DoorkeeperFeedWorker', 'DoorkeeperFeedWorkerJIRA' => 'DoorkeeperFeedWorker', + 'DoorkeeperGitHubHookController' => 'PhabricatorController', 'DoorkeeperImportEngine' => 'Phobject', 'DoorkeeperMissingLinkException' => 'Exception', 'DoorkeeperObjectRef' => 'Phobject', diff --git a/src/applications/doorkeeper/application/PhabricatorApplicationDoorkeeper.php b/src/applications/doorkeeper/application/PhabricatorApplicationDoorkeeper.php --- a/src/applications/doorkeeper/application/PhabricatorApplicationDoorkeeper.php +++ b/src/applications/doorkeeper/application/PhabricatorApplicationDoorkeeper.php @@ -25,6 +25,7 @@ return array( '/doorkeeper/' => array( 'tags/' => 'DoorkeeperTagsController', + 'github-hook/' => 'DoorkeeperGitHubHookController', ), ); } diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php new file mode 100644 --- /dev/null +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php @@ -0,0 +1,207 @@ +getApplicationType() != self::APPTYPE_GITHUB) { + return false; + } + + $types = array( + self::OBJTYPE_PULL_REQUEST => true, + ); + + return isset($types[$ref->getObjectType()]); + } + + public function pullRefs(array $refs) { + + $id_map = mpull($refs, 'getObjectID', 'getObjectKey'); + + // Object IDs for pull requests are formatted as: + // + // user:repo:pullid + // + // since we need both the user and repo name to retrieve + // data about a pull request. + $data_map = array(); + foreach ($id_map as $key => $id) { + $split = explode(':', $id); + $user = str_replace('/', '', $split[0]); + $repo = str_replace('/', '', $split[1]); + $pull_id = (int)$split[2]; + $data_map[$key] = array( + 'id' => $id, + 'user' => $user, + 'repo' => $repo, + 'pullID' => $pull_id, + 'getURI' => + 'https://api.github.com/repos/'.$user.'/'.$repo.'/pulls/'.$pull_id); + } + + // Query the GitHub public API, retrieving all of the information. + $futures = array(); + $user_agent = + 'Phabricator Instance at '. + PhabricatorEnv::getProductionURI('/'); + foreach ($data_map as $key => $data) { + $futures[$key] = id(new HTTPSFuture($data['getURI'])) + ->setMethod('GET') + ->setTimeout(30) + ->addHeader('User-Agent', $user_agent); + } + + $results = array(); + $failed = array(); + foreach (Futures($futures) as $key => $future) { + try { + $response = $future->resolvex(); + $results[$key] = json_decode($response[0], true); + } catch (Exception $ex) { + if (($ex instanceof HTTPFutureResponseStatus) && + ($ex->getStatusCode() == 404)) { + // This indicates that the object has been deleted (or never existed, + // or isn't visible to the current user) but it's a successful sync of + // an object which isn't visible. + } else { + // This is something else, so consider it a synchronization failure. + phlog($ex); + $failed[$key] = $ex; + } + } + } + + // Set all the associated data. + foreach ($refs as $ref) { + $did_fail = idx($failed, $ref->getObjectKey()); + if ($did_fail) { + // Set at least the key to identify, normally the + // name is the name of the PR. + $ref->setAttribute('name', 'GitHub PR '.$ref->getObjectID()); + $ref->setSyncFailed(true); + continue; + } + + $result = idx($results, $ref->getObjectKey()); + if (!$result) { + continue; + } + + $ref->setIsVisible(true); + $ref->setAttribute('name', $result['title']); + + // TODO: Does the data belong on the ref attributes or the + // object properties?? + + $obj = $ref->getExternalObject(); + $obj->setObjectURI($result['url']); + $obj->setProperty('name', $result['title']); + $obj->setProperty('diffURL', $result['diff_url']); + $obj->setProperty('state', $result['state']); + $obj->setProperty('userLogin', $result['user']['login']); + $obj->setProperty('body', $result['body']); + $obj->setProperty('headGitURL', $result['head']['repo']['git_url']); + $obj->setProperty('headGitSHA', $result['head']['sha']); + $obj->setProperty('headLabel', $result['head']['label']); + $obj->setProperty('headOwnerLogin', $result['head']['owner']['login']); + $obj->setProperty('baseGitURL', $result['base']['repo']['git_url']); + $obj->setProperty('baseGitSHA', $result['base']['sha']); + $obj->setProperty('baseLabel', $result['base']['label']); + $obj->setProperty('baseOwnerLogin', $result['base']['owner']['login']); + $obj->setProperty('merged', $result['merged']); + + $existing_revision_id = $obj->getProperty('revisionID'); + $revision_id = $this->syncRevisionForExternalObject( + $obj, + $existing_revision_id); + $obj->setProperty('revisionID', $revision_id); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $obj->save(); + unset($unguarded); + } + } + + private function syncRevisionForExternalObject( + DoorkeeperExternalObject $pr, + $existing_revision_id) { + + $author_user = $this->lookupPhabricatorUserByConnectedAccount( + $pr->getProperty('userLogin')); + + if ($existing_revision_id === null) { + // There is no existing revision, so we need to create one. + $revision = DifferentialRevision::initializeNewRevision($author_user); + $revision->attachReviewerStatus(array()); + } else { + $revision = id(new DifferentialRevisionQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIDs(array($existing_revision_id)) + ->needReviewerStatus(true) + ->needActiveDiffs(true) + ->executeOne(); + } + + $revision->setAuthorPHID($author_user->getPHID()); + $revision->setTitle($pr->getProperty('name')); + $revision->setSummary($pr->getProperty('body')); + + // Download diff. + $user_agent = + 'Phabricator Instance at '. + PhabricatorEnv::getProductionURI('/'); + $response = id(new HTTPSFuture($pr->getProperty('diffURL'))) + ->setMethod('GET') + ->setTimeout(30) + ->addHeader('User-Agent', $user_agent) + ->resolvex(); + $raw_diff = $response[0]; + + // Copied from Conduit code? Should we just make + // the Conduit call here? + $parser = new ArcanistDiffParser(); + $changes = $parser->parseDiff($raw_diff); + $diff = DifferentialDiff::newFromRawChanges($changes); + + $diff->setLintStatus(DifferentialLintStatus::LINT_SKIP); + $diff->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP); + + $diff->setDescription('Received from GitHub service hook'); + + $diff->setAuthorPHID($author_user->getPHID()); + $diff->setCreationMethod('github'); + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $diff->save(); + unset($unguarded); + + // Attach diff. + $xactions = array(); + $xactions[] = id(new DifferentialTransaction()) + ->setTransactionType(DifferentialTransaction::TYPE_UPDATE) + ->setNewValue($diff->getPHID()); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $editor = id(new DifferentialTransactionEditor()) + ->setActor($author_user) + ->setContentSource( + PhabricatorContentSource::newForSource('github', array())) + ->setContinueOnNoEffect(true) + ->applyTransactions($revision, $xactions); + unset($unguarded); + + return $revision->getID(); + } + + private function lookupPhabricatorUserByConnectedAccount($githubAccount) { + // TODO + return id(new PhabricatorPeopleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withUsernames(array('github')) + ->executeOne(); + } + +} diff --git a/src/applications/doorkeeper/controller/DoorkeeperGitHubHookController.php b/src/applications/doorkeeper/controller/DoorkeeperGitHubHookController.php new file mode 100644 --- /dev/null +++ b/src/applications/doorkeeper/controller/DoorkeeperGitHubHookController.php @@ -0,0 +1,85 @@ +isGitHubRequest(); + } + + public function shouldRequireEnabledUser() { + return !$this->isGitHubRequest(); + } + + public function shouldAllowPublic() { + return $this->isGitHubRequest(); + } + + private function isGitHubRequest() { + return + $_SERVER['REQUEST_METHOD'] == 'POST' && + $_SERVER['HTTP_X_GITHUB_EVENT'] != ''; + } + + public function processRequest() { + if (!$this->isGitHubRequest()) { + // TODO: Maybe display something to the user telling + // them that this is the GitHub endpoint and how to + // configure it? + + // return new Aphront500Response(); + + $payload = json_decode($_GET['payload']); + $type = $_GET['type']; + } else { + $payload = json_decode($_POST['payload']); + $type = $_SERVER['HTTP_X_GITHUB_EVENT']; + } + + try { + switch ($type) { + case "pull_request": + return $this->handlePullRequest($payload); + default: + return id(new AphrontPlainTextResponse()) + ->setContent('okay'); + } + } catch (Exception $ex) { + // This should be a 500. + return id(new AphrontPlainTextResponse()) + ->setContent($ex); + } + } + + private function handlePullRequest($payload) { + if (!$this->isGitHubRequest()) { + $pr_id = $_GET['id']; + } else { + $pr_id = + $payload->pull_request->base->repo->name.':'. + $payload->pull_request->base->repo->owner->login.':'. + $payload->number; + } + + // Push this through the Doorkeeper import engine. + $ref = id(new DoorkeeperObjectRef()) + ->setApplicationType(DoorkeeperBridgeGitHub::APPTYPE_GITHUB) + ->setApplicationDomain(DoorkeeperBridgeGitHub::APPDOMAIN_GITHUB) + ->setObjectType(DoorkeeperBridgeGitHub::OBJTYPE_PULL_REQUEST) + ->setObjectID($pr_id); + + try { + $refs = id(new DoorkeeperImportEngine()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setRefs(array($ref)) + ->setThrowOnMissingLink(true) + ->execute(); + } catch (DoorkeeperMissingLinkException $ex) { + return id(new AphrontPlainTextResponse()) + ->setContent('no link'); + } + + return id(new AphrontPlainTextResponse()) + ->setContent($pr_id.' imported'); + } + +} diff --git a/src/applications/metamta/contentsource/PhabricatorContentSource.php b/src/applications/metamta/contentsource/PhabricatorContentSource.php --- a/src/applications/metamta/contentsource/PhabricatorContentSource.php +++ b/src/applications/metamta/contentsource/PhabricatorContentSource.php @@ -14,6 +14,7 @@ const SOURCE_LEGACY = 'legacy'; const SOURCE_DAEMON = 'daemon'; const SOURCE_LIPSUM = 'lipsum'; + const SOURCE_GITHUB = 'github'; private $source; private $params = array(); @@ -77,6 +78,7 @@ self::SOURCE_DAEMON => pht('Daemons'), self::SOURCE_LIPSUM => pht('Lipsum'), self::SOURCE_UNKNOWN => pht('Old World'), + self::SOURCE_GITHUB => pht('GitHub'), ); }