1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 08:52:39 +01:00

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
This commit is contained in:
epriestley 2013-09-03 17:27:51 -07:00
parent 825fb9c85a
commit 853544b54a
4 changed files with 210 additions and 0 deletions

View file

@ -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',

View file

@ -35,6 +35,10 @@ final class DifferentialDefaultFieldSelector
new DifferentialAsanaRepresentationFieldSpecification(),
);
if (PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider()) {
$fields[] = new DifferentialJIRAIssuesFieldSpecification();
}
return $fields;
}

View file

@ -0,0 +1,198 @@
<?php
final class DifferentialJIRAIssuesFieldSpecification
extends DifferentialFieldSpecification {
private $value;
private $error;
public function getStorageKey() {
return 'phabricator:jira-issues';
}
public function getValueForStorage() {
return json_encode($this->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();
}
}

View file

@ -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);