diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php @@ -76,9 +76,6 @@ $ref->setAttribute('name', $body['title']); $obj = $ref->getExternalObject(); - if ($obj->getID()) { - continue; - } $this->fillObjectFromData($obj, $result); @@ -92,6 +89,19 @@ $body = $result->getBody(); $uri = $body['html_url']; $obj->setObjectURI($uri); + + $title = idx($body, 'title'); + $description = idx($body, 'body'); + + $created = idx($body, 'created_at'); + $created = strtotime($created); + + $state = idx($body, 'state'); + + $obj->setProperty('task.title', $title); + $obj->setProperty('task.description', $description); + $obj->setProperty('task.created', $created); + $obj->setProperty('task.state', $state); } } diff --git a/src/applications/nuance/github/NuanceGitHubRawEvent.php b/src/applications/nuance/github/NuanceGitHubRawEvent.php --- a/src/applications/nuance/github/NuanceGitHubRawEvent.php +++ b/src/applications/nuance/github/NuanceGitHubRawEvent.php @@ -90,6 +90,10 @@ return null; } + public function getComment() { + return 'TODO: Actually extract comment text.'; + } + public function getURI() { $raw = $this->raw; diff --git a/src/applications/nuance/item/NuanceGitHubEventItemType.php b/src/applications/nuance/item/NuanceGitHubEventItemType.php --- a/src/applications/nuance/item/NuanceGitHubEventItemType.php +++ b/src/applications/nuance/item/NuanceGitHubEventItemType.php @@ -5,6 +5,8 @@ const ITEMTYPE = 'github.event'; + private $externalObject; + public function getItemTypeDisplayName() { return pht('GitHub Event'); } @@ -79,29 +81,13 @@ // TODO: Link up the requestor, etc. - $source = $item->getSource(); - $token = $source->getSourceProperty('github.token'); - $token = new PhutilOpaqueEnvelope($token); + $is_dirty = false; - $ref = $this->getDoorkeeperRef($item); - if ($ref) { - $ref = id(new DoorkeeperImportEngine()) - ->setViewer($viewer) - ->setRefs(array($ref)) - ->setThrowOnMissingLink(true) - ->setContextProperty('github.token', $token) - ->executeOne(); - - if ($ref->getSyncFailed()) { - $xobj = null; - } else { - $xobj = $ref->getExternalObject(); - } + $xobj = $this->reloadExternalObject($item); - if ($xobj) { - $item->setItemProperty('doorkeeper.xobj.phid', $xobj->getPHID()); - $is_dirty = true; - } + if ($xobj) { + $item->setItemProperty('doorkeeper.xobj.phid', $xobj->getPHID()); + $is_dirty = true; } if ($item->getStatus() == NuanceItem::STATUS_IMPORTING) { @@ -137,6 +123,56 @@ ->setObjectID($full_ref); } + private function reloadExternalObject(NuanceItem $item, $local = false) { + $ref = $this->getDoorkeeperRef($item); + if (!$ref) { + return null; + } + + $source = $item->getSource(); + $token = $source->getSourceProperty('github.token'); + $token = new PhutilOpaqueEnvelope($token); + + $viewer = $this->getViewer(); + + $ref = id(new DoorkeeperImportEngine()) + ->setViewer($viewer) + ->setRefs(array($ref)) + ->setThrowOnMissingLink(true) + ->setContextProperty('github.token', $token) + ->needLocalOnly($local) + ->executeOne(); + + if ($ref->getSyncFailed()) { + $xobj = null; + } else { + $xobj = $ref->getExternalObject(); + } + + if ($xobj) { + $this->externalObject = $xobj; + } + + return $xobj; + } + + private function getExternalObject(NuanceItem $item) { + if ($this->externalObject === null) { + $xobj = $this->reloadExternalObject($item, $local = true); + if ($xobj) { + $this->externalObject = $xobj; + } else { + $this->externalObject = false; + } + } + + if ($this->externalObject) { + return $this->externalObject; + } + + return null; + } + private function newRawEvent(NuanceItem $item) { $type = $item->getItemProperty('api.type'); $raw = $item->getItemProperty('api.raw', array()); @@ -147,6 +183,15 @@ public function getItemActions(NuanceItem $item) { $actions = array(); + $xobj = $this->getExternalObject($item); + if ($xobj) { + $actions[] = $this->newItemAction($item, 'reload') + ->setName(pht('Reload from GitHub')) + ->setIcon('fa-refresh') + ->setWorkflow(true) + ->setRenderAsForm(true); + } + $actions[] = $this->newItemAction($item, 'sync') ->setName(pht('Import to Maniphest')) ->setIcon('fa-anchor') @@ -189,6 +234,7 @@ ->appendForm($form) ->addCancelButton($item->getURI(), pht('Done')); case 'sync': + case 'reload': $item->issueCommand($viewer->getPHID(), $action); return id(new AphrontRedirectResponse())->setURI($item->getURI()); } @@ -238,9 +284,117 @@ ->appendChild($property_list); } - protected function handleCommand(NuanceItem $item, $action) { + protected function handleCommand( + NuanceItem $item, + NuanceItemCommand $command) { + + $action = $command->getCommand(); + switch ($action) { + case 'sync': + return $this->syncItem($item, $command); + case 'reload': + $this->reloadExternalObject($item); + return true; + } + return null; } + private function syncItem( + NuanceItem $item, + NuanceItemCommand $command) { + + $xobj_phid = $item->getItemProperty('doorkeeper.xobj.phid'); + if (!$xobj_phid) { + throw new Exception( + pht( + 'Unable to sync: no external object PHID.')); + } + + // TODO: Write some kind of marker to prevent double-synchronization. + + $viewer = $this->getViewer(); + + $xobj = id(new DoorkeeperExternalObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($xobj_phid)) + ->executeOne(); + if (!$xobj) { + throw new Exception( + pht( + 'Unable to sync: failed to load object "%s".', + $xobj_phid)); + } + + $nuance_phid = id(new PhabricatorNuanceApplication())->getPHID(); + + $xactions = array(); + + $task = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withBridgedObjectPHIDs(array($xobj_phid)) + ->executeOne(); + if (!$task) { + $task = ManiphestTask::initializeNewTask($viewer) + ->setAuthorPHID($nuance_phid) + ->setBridgedObjectPHID($xobj_phid); + + $title = $xobj->getProperty('task.title'); + if (!strlen($title)) { + $title = pht('Nuance Item %d Task', $item->getID()); + } + + $description = $xobj->getProperty('task.description'); + $created = $xobj->getProperty('task.created'); + $state = $xobj->getProperty('task.state'); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_TITLE) + ->setNewValue($title) + ->setDateCreated($created); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION) + ->setNewValue($description) + ->setDateCreated($created); + + $task->setDateCreated($created); + + // TODO: Synchronize state. + } + + $event = $this->newRawEvent($item); + $comment = $event->getComment(); + if (strlen($comment)) { + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) + ->attachComment( + id(new ManiphestTransactionComment()) + ->setContent($comment)); + } + + // TODO: Preserve the item's original source. + $source = PhabricatorContentSource::newForSource( + PhabricatorContentSource::SOURCE_DAEMON, + array()); + + // TOOD: This should really be the external source. + $acting_phid = $nuance_phid; + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setActingAsPHID($acting_phid) + ->setContentSource($source) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $xactions = $editor->applyTransactions($task, $xactions); + + return array( + 'objectPHID' => $task->getPHID(), + 'xactionPHIDs' => mpull($xactions, 'getPHID'), + ); + } + } diff --git a/src/applications/nuance/item/NuanceItemType.php b/src/applications/nuance/item/NuanceItemType.php --- a/src/applications/nuance/item/NuanceItemType.php +++ b/src/applications/nuance/item/NuanceItemType.php @@ -131,7 +131,9 @@ ->applyTransactions($item, array($xaction)); } - protected function handleCommand(NuanceItem $item, $action) { + protected function handleCommand( + NuanceItem $item, + NuanceItemCommand $command) { return null; }