@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.

Begin bridging GitHub objects through Doorkeeper

Summary:
Ref T10538. This sets up a Doorkeeper bridge for GitHub issues, and pulls issues from GitHub to create ExternalObject references.

Broadly, does nothing useful.

Test Plan: Put a `var_dump()` in there somewhere and saw it probably do something when running `bin/nuance update --item 44`.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10538

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

+312 -5
+6
src/__phutil_library_map__.php
··· 840 840 'DoorkeeperAsanaRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php', 841 841 'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php', 842 842 'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php', 843 + 'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php', 844 + 'DoorkeeperBridgeGitHubIssue' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php', 843 845 'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php', 844 846 'DoorkeeperBridgeJIRATestCase' => 'applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php', 845 847 'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php', ··· 1423 1425 'NuanceGitHubEventItemType' => 'applications/nuance/item/NuanceGitHubEventItemType.php', 1424 1426 'NuanceGitHubImportCursor' => 'applications/nuance/cursor/NuanceGitHubImportCursor.php', 1425 1427 'NuanceGitHubIssuesImportCursor' => 'applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php', 1428 + 'NuanceGitHubRawEvent' => 'applications/nuance/github/NuanceGitHubRawEvent.php', 1426 1429 'NuanceGitHubRepositoryImportCursor' => 'applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php', 1427 1430 'NuanceGitHubRepositorySourceDefinition' => 'applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php', 1428 1431 'NuanceImportCursor' => 'applications/nuance/cursor/NuanceImportCursor.php', ··· 4970 4973 'DoorkeeperAsanaRemarkupRule' => 'DoorkeeperRemarkupRule', 4971 4974 'DoorkeeperBridge' => 'Phobject', 4972 4975 'DoorkeeperBridgeAsana' => 'DoorkeeperBridge', 4976 + 'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge', 4977 + 'DoorkeeperBridgeGitHubIssue' => 'DoorkeeperBridgeGitHub', 4973 4978 'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge', 4974 4979 'DoorkeeperBridgeJIRATestCase' => 'PhabricatorTestCase', 4975 4980 'DoorkeeperDAO' => 'PhabricatorLiskDAO', ··· 5681 5686 'NuanceGitHubEventItemType' => 'NuanceItemType', 5682 5687 'NuanceGitHubImportCursor' => 'NuanceImportCursor', 5683 5688 'NuanceGitHubIssuesImportCursor' => 'NuanceGitHubImportCursor', 5689 + 'NuanceGitHubRawEvent' => 'Phobject', 5684 5690 'NuanceGitHubRepositoryImportCursor' => 'NuanceGitHubImportCursor', 5685 5691 'NuanceGitHubRepositorySourceDefinition' => 'NuanceSourceDefinition', 5686 5692 'NuanceImportCursor' => 'Phobject',
+10
src/applications/doorkeeper/bridge/DoorkeeperBridge.php
··· 3 3 abstract class DoorkeeperBridge extends Phobject { 4 4 5 5 private $viewer; 6 + private $context = array(); 6 7 private $throwOnMissingLink; 7 8 8 9 public function setThrowOnMissingLink($throw_on_missing_link) { ··· 17 18 18 19 final public function getViewer() { 19 20 return $this->viewer; 21 + } 22 + 23 + final public function setContext($context) { 24 + $this->context = $context; 25 + return $this; 26 + } 27 + 28 + final public function getContextProperty($key, $default = null) { 29 + return idx($this->context, $key, $default); 20 30 } 21 31 22 32 public function isEnabled() {
+50
src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php
··· 1 + <?php 2 + 3 + abstract class DoorkeeperBridgeGitHub extends DoorkeeperBridge { 4 + 5 + const APPTYPE_GITHUB = 'github'; 6 + const APPDOMAIN_GITHUB = 'github.com'; 7 + 8 + public function canPullRef(DoorkeeperObjectRef $ref) { 9 + if ($ref->getApplicationType() != self::APPTYPE_GITHUB) { 10 + return false; 11 + } 12 + 13 + if ($ref->getApplicationDomain() != self::APPDOMAIN_GITHUB) { 14 + return false; 15 + } 16 + 17 + return true; 18 + } 19 + 20 + protected function getGitHubAccessToken() { 21 + $context_token = $this->getContextProperty('github.token'); 22 + if ($context_token) { 23 + return $context_token->openEnvelope(); 24 + } 25 + 26 + // TODO: Do a bunch of work to fetch the viewer's linked account if 27 + // they have one. 28 + 29 + return $this->didFailOnMissingLink(); 30 + } 31 + 32 + protected function parseGitHubIssueID($id) { 33 + $matches = null; 34 + if (!preg_match('(^([^/]+)/([^/]+)#([1-9]\d*)\z)', $id, $matches)) { 35 + throw new Exception( 36 + pht( 37 + 'GitHub Issue ID "%s" is not properly formatted. Expected an ID '. 38 + 'in the form "owner/repository#123".', 39 + $id)); 40 + } 41 + 42 + return array( 43 + $matches[1], 44 + $matches[2], 45 + (int)$matches[3], 46 + ); 47 + } 48 + 49 + 50 + }
+97
src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php
··· 1 + <?php 2 + 3 + final class DoorkeeperBridgeGitHubIssue 4 + extends DoorkeeperBridgeGitHub { 5 + 6 + const OBJTYPE_GITHUB_ISSUE = 'github.issue'; 7 + 8 + public function canPullRef(DoorkeeperObjectRef $ref) { 9 + if (!parent::canPullRef($ref)) { 10 + return false; 11 + } 12 + 13 + if ($ref->getObjectType() !== self::OBJTYPE_GITHUB_ISSUE) { 14 + return false; 15 + } 16 + 17 + return true; 18 + } 19 + 20 + public function pullRefs(array $refs) { 21 + $token = $this->getGitHubAccessToken(); 22 + if (!strlen($token)) { 23 + return null; 24 + } 25 + 26 + $template = id(new PhutilGitHubFuture()) 27 + ->setAccessToken($token); 28 + 29 + $futures = array(); 30 + $id_map = mpull($refs, 'getObjectID', 'getObjectKey'); 31 + foreach ($id_map as $key => $id) { 32 + list($user, $repository, $number) = $this->parseGitHubIssueID($id); 33 + $uri = "/repos/{$user}/{$repository}/issues/{$number}"; 34 + $data = array(); 35 + $futures[$key] = id(clone $template) 36 + ->setRawGitHubQuery($uri, $data); 37 + } 38 + 39 + $results = array(); 40 + $failed = array(); 41 + foreach (new FutureIterator($futures) as $key => $future) { 42 + try { 43 + $results[$key] = $future->resolve(); 44 + } catch (Exception $ex) { 45 + if (($ex instanceof HTTPFutureResponseStatus) && 46 + ($ex->getStatusCode() == 404)) { 47 + // TODO: Do we end up here for deleted objects and invisible 48 + // objects? 49 + } else { 50 + phlog($ex); 51 + $failed[$key] = $ex; 52 + } 53 + } 54 + } 55 + 56 + $viewer = $this->getViewer(); 57 + 58 + foreach ($refs as $ref) { 59 + $ref->setAttribute('name', pht('GitHub Issue %s', $ref->getObjectID())); 60 + 61 + $did_fail = idx($failed, $ref->getObjectKey()); 62 + if ($did_fail) { 63 + $ref->setSyncFailed(true); 64 + continue; 65 + } 66 + 67 + $result = idx($results, $ref->getObjectKey()); 68 + if (!$result) { 69 + continue; 70 + } 71 + 72 + $body = $result->getBody(); 73 + 74 + $ref->setIsVisible(true); 75 + $ref->setAttribute('api.raw', $body); 76 + $ref->setAttribute('name', $body['title']); 77 + 78 + $obj = $ref->getExternalObject(); 79 + if ($obj->getID()) { 80 + continue; 81 + } 82 + 83 + $this->fillObjectFromData($obj, $result); 84 + 85 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 86 + $obj->save(); 87 + unset($unguarded); 88 + } 89 + } 90 + 91 + public function fillObjectFromData(DoorkeeperExternalObject $obj, $result) { 92 + $body = $result->getBody(); 93 + $uri = $body['html_url']; 94 + $obj->setObjectURI($uri); 95 + } 96 + 97 + }
+6
src/applications/doorkeeper/engine/DoorkeeperImportEngine.php
··· 7 7 private $phids = array(); 8 8 private $localOnly; 9 9 private $throwOnMissingLink; 10 + private $context = array(); 10 11 11 12 public function setViewer(PhabricatorUser $viewer) { 12 13 $this->viewer = $viewer; ··· 37 38 return $this; 38 39 } 39 40 41 + public function setContextProperty($key, $value) { 42 + $this->context[$key] = $value; 43 + return $this; 44 + } 40 45 41 46 /** 42 47 * Configure behavior if remote refs can not be retrieved because an ··· 96 101 foreach ($bridges as $key => $bridge) { 97 102 $bridge->setViewer($viewer); 98 103 $bridge->setThrowOnMissingLink($this->throwOnMissingLink); 104 + $bridge->setContext($this->context); 99 105 } 100 106 101 107 $working_set = $refs;
+72
src/applications/nuance/github/NuanceGitHubRawEvent.php
··· 1 + <?php 2 + 3 + final class NuanceGitHubRawEvent extends Phobject { 4 + 5 + private $raw; 6 + private $type; 7 + 8 + const TYPE_ISSUE = 'issue'; 9 + const TYPE_REPOSITORY = 'repository'; 10 + 11 + public static function newEvent($type, array $raw) { 12 + $event = new self(); 13 + $event->type = $type; 14 + $event->raw = $raw; 15 + return $event; 16 + } 17 + 18 + public function getRepositoryFullName() { 19 + return $this->getRepositoryFullRawName(); 20 + } 21 + 22 + public function isIssueEvent() { 23 + if ($this->isPullRequestEvent()) { 24 + return false; 25 + } 26 + 27 + if ($this->type == self::TYPE_ISSUE) { 28 + return true; 29 + } 30 + 31 + switch ($this->getIssueRawKind()) { 32 + case 'IssuesEvent': 33 + case 'IssuesCommentEvent': 34 + return true; 35 + } 36 + 37 + return false; 38 + } 39 + 40 + public function isPullRequestEvent() { 41 + return false; 42 + } 43 + 44 + public function getIssueNumber() { 45 + if (!$this->isIssueEvent()) { 46 + return null; 47 + } 48 + 49 + $raw = $this->raw; 50 + 51 + if ($this->type == self::TYPE_ISSUE) { 52 + return idxv($raw, array('issue', 'number')); 53 + } 54 + 55 + if ($this->type == self::TYPE_REPOSITORY) { 56 + return idxv($raw, array('payload', 'issue', 'number')); 57 + } 58 + 59 + return null; 60 + } 61 + 62 + private function getRepositoryFullRawName() { 63 + $raw = $this->raw; 64 + return idxv($raw, array('repo', 'name')); 65 + } 66 + 67 + private function getIssueRawKind() { 68 + $raw = $this->raw; 69 + return idxv($raw, array('type')); 70 + } 71 + 72 + }
+64 -3
src/applications/nuance/item/NuanceGitHubEventItemType.php
··· 74 74 } 75 75 76 76 protected function updateItemFromSource(NuanceItem $item) { 77 + $viewer = $this->getViewer(); 78 + $is_dirty = false; 79 + 77 80 // TODO: Link up the requestor, etc. 78 81 82 + $source = $item->getSource(); 83 + $token = $source->getSourceProperty('github.token'); 84 + $token = new PhutilOpaqueEnvelope($token); 85 + 86 + $ref = $this->getDoorkeeperRef($item); 87 + if ($ref) { 88 + $ref = id(new DoorkeeperImportEngine()) 89 + ->setViewer($viewer) 90 + ->setRefs(array($ref)) 91 + ->setThrowOnMissingLink(true) 92 + ->setContextProperty('github.token', $token) 93 + ->executeOne(); 94 + 95 + if ($ref->getSyncFailed()) { 96 + $xobj = null; 97 + } else { 98 + $xobj = $ref->getExternalObject(); 99 + } 100 + 101 + if ($xobj) { 102 + $item->setItemProperty('doorkeeper.xobj.phid', $xobj->getPHID()); 103 + $is_dirty = true; 104 + } 105 + } 106 + 79 107 if ($item->getStatus() == NuanceItem::STATUS_IMPORTING) { 80 - $item 81 - ->setStatus(NuanceItem::STATUS_ROUTING) 82 - ->save(); 108 + $item->setStatus(NuanceItem::STATUS_ROUTING); 109 + $is_dirty = true; 110 + } 111 + 112 + if ($is_dirty) { 113 + $item->save(); 114 + } 115 + } 116 + 117 + private function getDoorkeeperRef(NuanceItem $item) { 118 + $raw = $this->newRawEvent($item); 119 + 120 + $full_repository = $raw->getRepositoryFullName(); 121 + if (!strlen($full_repository)) { 122 + return null; 123 + } 124 + 125 + if ($raw->isIssueEvent()) { 126 + $ref_type = DoorkeeperBridgeGitHubIssue::OBJTYPE_GITHUB_ISSUE; 127 + $issue_number = $raw->getIssueNumber(); 128 + $full_ref = "{$full_repository}#{$issue_number}"; 129 + } else { 130 + return null; 83 131 } 132 + 133 + return id(new DoorkeeperObjectRef()) 134 + ->setApplicationType(DoorkeeperBridgeGitHub::APPTYPE_GITHUB) 135 + ->setApplicationDomain(DoorkeeperBridgeGitHub::APPDOMAIN_GITHUB) 136 + ->setObjectType($ref_type) 137 + ->setObjectID($full_ref); 138 + } 139 + 140 + private function newRawEvent(NuanceItem $item) { 141 + $type = $item->getItemProperty('api.type'); 142 + $raw = $item->getItemProperty('api.raw', array()); 143 + 144 + return NuanceGitHubRawEvent::newEvent($type, $raw); 84 145 } 85 146 86 147 }
+7 -2
src/applications/nuance/worker/NuanceItemUpdateWorker.php
··· 25 25 26 26 private function updateItem(NuanceItem $item) { 27 27 $impl = $item->getImplementation(); 28 - if ($impl->canUpdateItems()) { 29 - $impl->updateItem($item); 28 + if (!$impl->canUpdateItems()) { 29 + return null; 30 30 } 31 + 32 + $viewer = $this->getViewer(); 33 + 34 + $impl->setViewer($viewer); 35 + $impl->updateItem($item); 31 36 } 32 37 33 38 private function routeItem(NuanceItem $item) {