From 853544b54ae07e496b337f373660a56f43f9e49e Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 3 Sep 2013 17:27:51 -0700 Subject: [PATCH] Add "JIRA Issues" field to Differential Summary: Ref T3687. This adds a field which allows you to link Differential Revisions to JIRA issues. This is just about as basic as it can get, but gets the job done. The field enables itself if you have a JIRA auth provide. You enter JIRA issues in a comma-delimited format and it generates appropriate edges. Nothing is pushed to the issues yet. The only real rough part here is that if you commandeer a revision which is linked to issues you can't see, editing it is difficult via the CLI. This seems pretty much like a non-issue, but at some point we can let the field throw some kind of "RecoverableInvalidFieldException" which just warns the user. The "no reviewers, continue anyway?" prompt could then use that too. Test Plan: - Edited via web UI, tried valid/invalid edits, checked that edges showed up in the database, added/removed issues, clicked issue links. - Edited via CLI, tried valid/invalid edits. Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T3687 Differential Revision: https://secure.phabricator.com/D6879 --- src/__phutil_library_map__.php | 2 + .../DifferentialDefaultFieldSelector.php | 4 + ...fferentialJIRAIssuesFieldSpecification.php | 198 ++++++++++++++++++ .../edges/constants/PhabricatorEdgeConfig.php | 6 + 4 files changed, 210 insertions(+) create mode 100644 src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a1893c9f65..4d0b655a1b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -371,6 +371,7 @@ phutil_register_library_map(array( 'DifferentialInlineCommentPreviewController' => 'applications/differential/controller/DifferentialInlineCommentPreviewController.php', 'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php', 'DifferentialInlineCommentView' => 'applications/differential/view/DifferentialInlineCommentView.php', + 'DifferentialJIRAIssuesFieldSpecification' => 'applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php', 'DifferentialLinesFieldSpecification' => 'applications/differential/field/specification/DifferentialLinesFieldSpecification.php', 'DifferentialLintFieldSpecification' => 'applications/differential/field/specification/DifferentialLintFieldSpecification.php', 'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php', @@ -2411,6 +2412,7 @@ phutil_register_library_map(array( 'DifferentialInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController', 'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery', 'DifferentialInlineCommentView' => 'AphrontView', + 'DifferentialJIRAIssuesFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialLinesFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialLintFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialLocalCommitsView' => 'AphrontView', diff --git a/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php b/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php index 1e46717a56..0d4d7eec45 100644 --- a/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php +++ b/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php @@ -35,6 +35,10 @@ final class DifferentialDefaultFieldSelector new DifferentialAsanaRepresentationFieldSpecification(), ); + if (PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider()) { + $fields[] = new DifferentialJIRAIssuesFieldSpecification(); + } + return $fields; } diff --git a/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php b/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php new file mode 100644 index 0000000000..a80551f46f --- /dev/null +++ b/src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php @@ -0,0 +1,198 @@ +value); + } + + public function setValueFromStorage($value) { + if (!strlen($value)) { + $this->value = array(); + } else { + $this->value = json_decode($value, true); + } + return $this; + } + + public function shouldAppearOnEdit() { + return true; + } + + public function setValueFromRequest(AphrontRequest $request) { + $this->value = $request->getStrList($this->getStorageKey()); + return $this; + } + + public function renderEditControl() { + return id(new AphrontFormTextControl()) + ->setLabel(pht('JIRA Issues')) + ->setCaption( + pht('Example: %s', phutil_tag('tt', array(), 'JIS-3, JIS-9'))) + ->setName($this->getStorageKey()) + ->setValue(implode(', ', $this->value)) + ->setError($this->error); + } + + public function shouldAppearOnRevisionView() { + return true; + } + + public function renderLabelForRevisionView() { + return pht('JIRA Issues:'); + } + + public function renderValueForRevisionView() { + $xobjs = $this->loadDoorkeeperExternalObjects(); + if (!$xobjs) { + return null; + } + + $links = array(); + foreach ($xobjs as $xobj) { + $links[] = phutil_tag( + 'a', + array( + 'href' => $xobj->getObjectURI(), + 'target' => '_blank', + ), + $xobj->getObjectID()); + } + + return phutil_implode_html(', ', $links); + } + + public function shouldAppearOnConduitView() { + return true; + } + + public function getValueForConduit() { + return $this->value; + } + + public function shouldAppearOnCommitMessage() { + return true; + } + + public function getCommitMessageKey() { + return 'jira.issues'; + } + + public function setValueFromParsedCommitMessage($value) { + $this->value = $value; + return $this; + } + + public function shouldOverwriteWhenCommitMessageIsEdited() { + return true; + } + + public function renderLabelForCommitMessage() { + return 'JIRA Issues'; + } + + public function renderValueForCommitMessage($is_edit) { + return implode(', ', $this->value); + } + + public function getSupportedCommitMessageLabels() { + return array( + 'JIRA', + 'JIRA Issues', + 'JIRA Issue', + ); + } + + public function parseValueFromCommitMessage($value) { + return preg_split('/[\s,]+/', $value, $limit = -1, PREG_SPLIT_NO_EMPTY); + } + + public function validateField() { + if ($this->value) { + $refs = id(new DoorkeeperImportEngine()) + ->setViewer($this->getUser()) + ->setRefs($this->buildDoorkeeperRefs()) + ->execute(); + + $bad = array(); + foreach ($refs as $ref) { + if (!$ref->getIsVisible()) { + $bad[] = $ref->getObjectID(); + } + } + + if ($bad) { + $bad = implode(', ', $bad); + $this->error = pht('Invalid'); + throw new DifferentialFieldValidationException( + pht( + "Some JIRA issues could not be loaded. They may not exist, or ". + "you may not have permission to view them: %s", + $bad)); + } + } + } + + private function buildDoorkeeperRefs() { + $provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider(); + + $refs = array(); + foreach ($this->value as $jira_key) { + $refs[] = id(new DoorkeeperObjectRef()) + ->setApplicationType(DoorkeeperBridgeJIRA::APPTYPE_JIRA) + ->setApplicationDomain($provider->getProviderDomain()) + ->setObjectType(DoorkeeperBridgeJIRA::OBJTYPE_ISSUE) + ->setObjectID($jira_key); + } + + return $refs; + } + + private function loadDoorkeeperExternalObjects() { + $refs = $this->buildDoorkeeperRefs(); + if (!$refs) { + return array(); + } + + $xobjs = id(new DoorkeeperExternalObjectQuery()) + ->setViewer($this->getUser()) + ->withObjectKeys(mpull($refs, 'getObjectKey')) + ->execute(); + + return $xobjs; + } + + public function didWriteRevision(DifferentialRevisionEditor $editor) { + $revision = $editor->getRevision(); + $revision_phid = $revision->getPHID(); + + $edge_type = PhabricatorEdgeConfig::TYPE_PHOB_HAS_JIRAISSUE; + $edge_dsts = mpull($this->loadDoorkeeperExternalObjects(), 'getPHID'); + + $edges = PhabricatorEdgeQuery::loadDestinationPHIDs( + $revision_phid, + $edge_type); + + $editor = id(new PhabricatorEdgeEditor()) + ->setActor($this->getUser()); + + foreach (array_diff($edges, $edge_dsts) as $rem_edge) { + $editor->removeEdge($revision_phid, $edge_type, $rem_edge); + } + + foreach (array_diff($edge_dsts, $edges) as $add_edge) { + $editor->addEdge($revision_phid, $edge_type, $add_edge); + } + + $editor->save(); + } + +} diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php index ce6a86db55..d95c0add9f 100644 --- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php +++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php @@ -68,6 +68,9 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { const TYPE_PHOB_HAS_ASANASUBTASK = 80003; const TYPE_ASANASUBTASK_HAS_PHOB = 80002; + const TYPE_PHOB_HAS_JIRAISSUE = 80004; + const TYPE_JIRAISSUE_HAS_PHOB = 80005; + public static function getInverse($edge_type) { static $map = array( self::TYPE_TASK_HAS_COMMIT => self::TYPE_COMMIT_HAS_TASK, @@ -129,6 +132,9 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { self::TYPE_DREV_HAS_REVIEWER => self::TYPE_REVIEWER_FOR_DREV, self::TYPE_REVIEWER_FOR_DREV => self::TYPE_DREV_HAS_REVIEWER, + + self::TYPE_PHOB_HAS_JIRAISSUE => self::TYPE_JIRAISSUE_HAS_PHOB, + self:: TYPE_JIRAISSUE_HAS_PHOB => self::TYPE_PHOB_HAS_JIRAISSUE ); return idx($map, $edge_type);