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

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

+210
+2
src/__phutil_library_map__.php
··· 371 371 'DifferentialInlineCommentPreviewController' => 'applications/differential/controller/DifferentialInlineCommentPreviewController.php', 372 372 'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php', 373 373 'DifferentialInlineCommentView' => 'applications/differential/view/DifferentialInlineCommentView.php', 374 + 'DifferentialJIRAIssuesFieldSpecification' => 'applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php', 374 375 'DifferentialLinesFieldSpecification' => 'applications/differential/field/specification/DifferentialLinesFieldSpecification.php', 375 376 'DifferentialLintFieldSpecification' => 'applications/differential/field/specification/DifferentialLintFieldSpecification.php', 376 377 'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php', ··· 2411 2412 'DifferentialInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController', 2412 2413 'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery', 2413 2414 'DifferentialInlineCommentView' => 'AphrontView', 2415 + 'DifferentialJIRAIssuesFieldSpecification' => 'DifferentialFieldSpecification', 2414 2416 'DifferentialLinesFieldSpecification' => 'DifferentialFieldSpecification', 2415 2417 'DifferentialLintFieldSpecification' => 'DifferentialFieldSpecification', 2416 2418 'DifferentialLocalCommitsView' => 'AphrontView',
+4
src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php
··· 35 35 new DifferentialAsanaRepresentationFieldSpecification(), 36 36 ); 37 37 38 + if (PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider()) { 39 + $fields[] = new DifferentialJIRAIssuesFieldSpecification(); 40 + } 41 + 38 42 return $fields; 39 43 } 40 44
+198
src/applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php
··· 1 + <?php 2 + 3 + final class DifferentialJIRAIssuesFieldSpecification 4 + extends DifferentialFieldSpecification { 5 + 6 + private $value; 7 + private $error; 8 + 9 + public function getStorageKey() { 10 + return 'phabricator:jira-issues'; 11 + } 12 + 13 + public function getValueForStorage() { 14 + return json_encode($this->value); 15 + } 16 + 17 + public function setValueFromStorage($value) { 18 + if (!strlen($value)) { 19 + $this->value = array(); 20 + } else { 21 + $this->value = json_decode($value, true); 22 + } 23 + return $this; 24 + } 25 + 26 + public function shouldAppearOnEdit() { 27 + return true; 28 + } 29 + 30 + public function setValueFromRequest(AphrontRequest $request) { 31 + $this->value = $request->getStrList($this->getStorageKey()); 32 + return $this; 33 + } 34 + 35 + public function renderEditControl() { 36 + return id(new AphrontFormTextControl()) 37 + ->setLabel(pht('JIRA Issues')) 38 + ->setCaption( 39 + pht('Example: %s', phutil_tag('tt', array(), 'JIS-3, JIS-9'))) 40 + ->setName($this->getStorageKey()) 41 + ->setValue(implode(', ', $this->value)) 42 + ->setError($this->error); 43 + } 44 + 45 + public function shouldAppearOnRevisionView() { 46 + return true; 47 + } 48 + 49 + public function renderLabelForRevisionView() { 50 + return pht('JIRA Issues:'); 51 + } 52 + 53 + public function renderValueForRevisionView() { 54 + $xobjs = $this->loadDoorkeeperExternalObjects(); 55 + if (!$xobjs) { 56 + return null; 57 + } 58 + 59 + $links = array(); 60 + foreach ($xobjs as $xobj) { 61 + $links[] = phutil_tag( 62 + 'a', 63 + array( 64 + 'href' => $xobj->getObjectURI(), 65 + 'target' => '_blank', 66 + ), 67 + $xobj->getObjectID()); 68 + } 69 + 70 + return phutil_implode_html(', ', $links); 71 + } 72 + 73 + public function shouldAppearOnConduitView() { 74 + return true; 75 + } 76 + 77 + public function getValueForConduit() { 78 + return $this->value; 79 + } 80 + 81 + public function shouldAppearOnCommitMessage() { 82 + return true; 83 + } 84 + 85 + public function getCommitMessageKey() { 86 + return 'jira.issues'; 87 + } 88 + 89 + public function setValueFromParsedCommitMessage($value) { 90 + $this->value = $value; 91 + return $this; 92 + } 93 + 94 + public function shouldOverwriteWhenCommitMessageIsEdited() { 95 + return true; 96 + } 97 + 98 + public function renderLabelForCommitMessage() { 99 + return 'JIRA Issues'; 100 + } 101 + 102 + public function renderValueForCommitMessage($is_edit) { 103 + return implode(', ', $this->value); 104 + } 105 + 106 + public function getSupportedCommitMessageLabels() { 107 + return array( 108 + 'JIRA', 109 + 'JIRA Issues', 110 + 'JIRA Issue', 111 + ); 112 + } 113 + 114 + public function parseValueFromCommitMessage($value) { 115 + return preg_split('/[\s,]+/', $value, $limit = -1, PREG_SPLIT_NO_EMPTY); 116 + } 117 + 118 + public function validateField() { 119 + if ($this->value) { 120 + $refs = id(new DoorkeeperImportEngine()) 121 + ->setViewer($this->getUser()) 122 + ->setRefs($this->buildDoorkeeperRefs()) 123 + ->execute(); 124 + 125 + $bad = array(); 126 + foreach ($refs as $ref) { 127 + if (!$ref->getIsVisible()) { 128 + $bad[] = $ref->getObjectID(); 129 + } 130 + } 131 + 132 + if ($bad) { 133 + $bad = implode(', ', $bad); 134 + $this->error = pht('Invalid'); 135 + throw new DifferentialFieldValidationException( 136 + pht( 137 + "Some JIRA issues could not be loaded. They may not exist, or ". 138 + "you may not have permission to view them: %s", 139 + $bad)); 140 + } 141 + } 142 + } 143 + 144 + private function buildDoorkeeperRefs() { 145 + $provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider(); 146 + 147 + $refs = array(); 148 + foreach ($this->value as $jira_key) { 149 + $refs[] = id(new DoorkeeperObjectRef()) 150 + ->setApplicationType(DoorkeeperBridgeJIRA::APPTYPE_JIRA) 151 + ->setApplicationDomain($provider->getProviderDomain()) 152 + ->setObjectType(DoorkeeperBridgeJIRA::OBJTYPE_ISSUE) 153 + ->setObjectID($jira_key); 154 + } 155 + 156 + return $refs; 157 + } 158 + 159 + private function loadDoorkeeperExternalObjects() { 160 + $refs = $this->buildDoorkeeperRefs(); 161 + if (!$refs) { 162 + return array(); 163 + } 164 + 165 + $xobjs = id(new DoorkeeperExternalObjectQuery()) 166 + ->setViewer($this->getUser()) 167 + ->withObjectKeys(mpull($refs, 'getObjectKey')) 168 + ->execute(); 169 + 170 + return $xobjs; 171 + } 172 + 173 + public function didWriteRevision(DifferentialRevisionEditor $editor) { 174 + $revision = $editor->getRevision(); 175 + $revision_phid = $revision->getPHID(); 176 + 177 + $edge_type = PhabricatorEdgeConfig::TYPE_PHOB_HAS_JIRAISSUE; 178 + $edge_dsts = mpull($this->loadDoorkeeperExternalObjects(), 'getPHID'); 179 + 180 + $edges = PhabricatorEdgeQuery::loadDestinationPHIDs( 181 + $revision_phid, 182 + $edge_type); 183 + 184 + $editor = id(new PhabricatorEdgeEditor()) 185 + ->setActor($this->getUser()); 186 + 187 + foreach (array_diff($edges, $edge_dsts) as $rem_edge) { 188 + $editor->removeEdge($revision_phid, $edge_type, $rem_edge); 189 + } 190 + 191 + foreach (array_diff($edge_dsts, $edges) as $add_edge) { 192 + $editor->addEdge($revision_phid, $edge_type, $add_edge); 193 + } 194 + 195 + $editor->save(); 196 + } 197 + 198 + }
+6
src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
··· 68 68 const TYPE_PHOB_HAS_ASANASUBTASK = 80003; 69 69 const TYPE_ASANASUBTASK_HAS_PHOB = 80002; 70 70 71 + const TYPE_PHOB_HAS_JIRAISSUE = 80004; 72 + const TYPE_JIRAISSUE_HAS_PHOB = 80005; 73 + 71 74 public static function getInverse($edge_type) { 72 75 static $map = array( 73 76 self::TYPE_TASK_HAS_COMMIT => self::TYPE_COMMIT_HAS_TASK, ··· 129 132 130 133 self::TYPE_DREV_HAS_REVIEWER => self::TYPE_REVIEWER_FOR_DREV, 131 134 self::TYPE_REVIEWER_FOR_DREV => self::TYPE_DREV_HAS_REVIEWER, 135 + 136 + self::TYPE_PHOB_HAS_JIRAISSUE => self::TYPE_JIRAISSUE_HAS_PHOB, 137 + self:: TYPE_JIRAISSUE_HAS_PHOB => self::TYPE_PHOB_HAS_JIRAISSUE 132 138 ); 133 139 134 140 return idx($map, $edge_type);