mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 16:52:41 +01:00
Basic task dependencies for Maniphest
Summary: This allows you to edit dependencies. It is a better patch than it used to be. It depends on D725. - If you create a cycle, it just throws an exception and aborts the workflow. It should not do this. - Tasks which depend on the current task aren't shown in the UI. Need to add a new table for this. - Transaction text says "attached Task" but should probably say "added a dependency on task". Test Plan: Created valid and invalid dependencies between tasks. Created valid and invalid dependencies between revisions. Reviewed By: tuomaspelkonen Reviewers: davidreuss, jungejason, tuomaspelkonen, aran Commenters: codeblock CC: aran, codeblock, tuomaspelkonen, epriestley Differential Revision: 595
This commit is contained in:
parent
24390d2b40
commit
f49e35deaf
11 changed files with 191 additions and 22 deletions
|
@ -63,7 +63,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'aphront-headsup-action-list-view-css' =>
|
||||
array(
|
||||
'uri' => '/res/af3dff49/rsrc/css/aphront/headsup-action-list-view.css',
|
||||
'uri' => '/res/5f89dc44/rsrc/css/aphront/headsup-action-list-view.css',
|
||||
'type' => 'css',
|
||||
'requires' =>
|
||||
array(
|
||||
|
@ -463,7 +463,7 @@ celerity_register_resource_map(array(
|
|||
),
|
||||
'javelin-behavior-differential-keyboard-navigation' =>
|
||||
array(
|
||||
'uri' => '/res/3bdfaec7/rsrc/js/application/differential/behavior-keyboard-nav.js',
|
||||
'uri' => '/res/e36415a2/rsrc/js/application/differential/behavior-keyboard-nav.js',
|
||||
'type' => 'js',
|
||||
'requires' =>
|
||||
array(
|
||||
|
|
|
@ -436,6 +436,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorOAuthProviderGithub' => 'applications/auth/oauth/provider/github',
|
||||
'PhabricatorOAuthRegistrationController' => 'applications/auth/controller/oauthregistration/base',
|
||||
'PhabricatorOAuthUnlinkController' => 'applications/auth/controller/unlink',
|
||||
'PhabricatorObjectGraph' => 'applications/phid/graph',
|
||||
'PhabricatorObjectHandle' => 'applications/phid/handle',
|
||||
'PhabricatorObjectHandleData' => 'applications/phid/handle/data',
|
||||
'PhabricatorObjectSelectorDialog' => 'view/control/objectselector',
|
||||
|
@ -979,6 +980,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorOAuthProviderGithub' => 'PhabricatorOAuthProvider',
|
||||
'PhabricatorOAuthRegistrationController' => 'PhabricatorAuthController',
|
||||
'PhabricatorOAuthUnlinkController' => 'PhabricatorAuthController',
|
||||
'PhabricatorObjectGraph' => 'AbstractDirectedGraph',
|
||||
'PhabricatorOwnersController' => 'PhabricatorController',
|
||||
'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO',
|
||||
'PhabricatorOwnersDeleteController' => 'PhabricatorOwnersController',
|
||||
|
|
|
@ -440,6 +440,16 @@ class DifferentialRevisionViewController extends DifferentialController {
|
|||
|
||||
$properties['Unit'] = $ustar.' '.$umsg.$utail;
|
||||
|
||||
$drevs = $revision->getAttachedPHIDs(
|
||||
PhabricatorPHIDConstants::PHID_TYPE_DREV);
|
||||
if ($drevs) {
|
||||
$links = array();
|
||||
foreach ($drevs as $drev_phid) {
|
||||
$links[] = $handles[$drev_phid]->renderLink();
|
||||
}
|
||||
$properties['Depends On'] = implode('<br />', $links);
|
||||
}
|
||||
|
||||
if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
|
||||
$tasks = $revision->getAttachedPHIDs(
|
||||
PhabricatorPHIDConstants::PHID_TYPE_TASK);
|
||||
|
@ -513,6 +523,13 @@ class DifferentialRevisionViewController extends DifferentialController {
|
|||
require_celerity_resource('phabricator-object-selector-css');
|
||||
require_celerity_resource('javelin-behavior-phabricator-object-selector');
|
||||
|
||||
$links[] = array(
|
||||
'class' => 'action-dependencies',
|
||||
'name' => 'Edit Dependencies',
|
||||
'href' => "/search/attach/{$revision_phid}/DREV/dependencies/",
|
||||
'sigil' => 'workflow',
|
||||
);
|
||||
|
||||
if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
|
||||
$links[] = array(
|
||||
'class' => 'attach-maniphest',
|
||||
|
|
|
@ -129,8 +129,18 @@ class ManiphestTaskDetailController extends ManiphestController {
|
|||
}
|
||||
}
|
||||
|
||||
if (idx($attached, PhabricatorPHIDConstants::PHID_TYPE_DREV)) {
|
||||
$revs = idx($attached, PhabricatorPHIDConstants::PHID_TYPE_DREV);
|
||||
$dtasks = idx($attached, PhabricatorPHIDConstants::PHID_TYPE_TASK);
|
||||
if ($dtasks) {
|
||||
$dtask_links = array();
|
||||
foreach ($dtasks as $dtask => $info) {
|
||||
$dtask_links[] = $handles[$dtask]->renderLink();
|
||||
}
|
||||
$dtask_links = implode('<br />', $dtask_links);
|
||||
$dict['Depends On'] = $dtask_links;
|
||||
}
|
||||
|
||||
$revs = idx($attached, PhabricatorPHIDConstants::PHID_TYPE_DREV);
|
||||
if ($revs) {
|
||||
$rev_links = array();
|
||||
foreach ($revs as $rev => $info) {
|
||||
$rev_links[] = $handles[$rev]->renderLink();
|
||||
|
@ -139,23 +149,21 @@ class ManiphestTaskDetailController extends ManiphestController {
|
|||
$dict['Revisions'] = $rev_links;
|
||||
}
|
||||
|
||||
if (idx($attached, PhabricatorPHIDConstants::PHID_TYPE_FILE)) {
|
||||
$file_infos = idx($attached, PhabricatorPHIDConstants::PHID_TYPE_FILE);
|
||||
$file_infos = idx($attached, PhabricatorPHIDConstants::PHID_TYPE_FILE);
|
||||
if ($file_infos) {
|
||||
$file_phids = array_keys($file_infos);
|
||||
|
||||
if ($file_phids) {
|
||||
$files = id(new PhabricatorFile())->loadAllWhere(
|
||||
'phid IN (%Ls)',
|
||||
$file_phids);
|
||||
$files = id(new PhabricatorFile())->loadAllWhere(
|
||||
'phid IN (%Ls)',
|
||||
$file_phids);
|
||||
|
||||
$views = array();
|
||||
foreach ($files as $file) {
|
||||
$view = new AphrontFilePreviewView();
|
||||
$view->setFile($file);
|
||||
$views[] = $view->render();
|
||||
}
|
||||
$dict['Files'] = implode('', $views);
|
||||
$views = array();
|
||||
foreach ($files as $file) {
|
||||
$view = new AphrontFilePreviewView();
|
||||
$view->setFile($file);
|
||||
$views[] = $view->render();
|
||||
}
|
||||
$dict['Files'] = implode('', $views);
|
||||
}
|
||||
|
||||
$dict['Description'] =
|
||||
|
@ -212,6 +220,13 @@ class ManiphestTaskDetailController extends ManiphestController {
|
|||
$action->setClass('action-merge');
|
||||
$actions[] = $action;
|
||||
|
||||
$action = new AphrontHeadsupActionView();
|
||||
$action->setName('Edit Dependencies');
|
||||
$action->setURI('/search/attach/'.$task->getPHID().'/TASK/dependencies/');
|
||||
$action->setWorkflow(true);
|
||||
$action->setClass('action-dependencies');
|
||||
$actions[] = $action;
|
||||
|
||||
$action = new AphrontHeadsupActionView();
|
||||
$action->setName('Edit Differential Revisions');
|
||||
$action->setURI('/search/attach/'.$task->getPHID().'/DREV/');
|
||||
|
|
|
@ -456,7 +456,9 @@ class ManiphestTransactionDetailView extends ManiphestView {
|
|||
$old_raw = nonempty($old, array());
|
||||
$new_raw = nonempty($new, array());
|
||||
|
||||
foreach (array(PhabricatorPHIDConstants::PHID_TYPE_DREV,
|
||||
foreach (array(
|
||||
PhabricatorPHIDConstants::PHID_TYPE_DREV,
|
||||
PhabricatorPHIDConstants::PHID_TYPE_TASK,
|
||||
PhabricatorPHIDConstants::PHID_TYPE_FILE) as $type) {
|
||||
$old = array_keys(idx($old_raw, $type, array()));
|
||||
$new = array_keys(idx($new_raw, $type, array()));
|
||||
|
@ -480,6 +482,11 @@ class ManiphestTransactionDetailView extends ManiphestView {
|
|||
$singular = 'file';
|
||||
$plural = 'files';
|
||||
break;
|
||||
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
|
||||
$singular = 'Maniphest Task';
|
||||
$plural = 'Maniphest Tasks';
|
||||
$dependency = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($added && !$removed) {
|
||||
|
|
49
src/applications/phid/graph/PhabricatorObjectGraph.php
Normal file
49
src/applications/phid/graph/PhabricatorObjectGraph.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2011 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
final class PhabricatorObjectGraph extends AbstractDirectedGraph {
|
||||
|
||||
private $edgeType;
|
||||
|
||||
public function setEdgeType($edge_type) {
|
||||
$this->edgeType = $edge_type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function loadEdges(array $nodes) {
|
||||
if (!$this->edgeType) {
|
||||
throw new Exception("Set edge type before loading graph!");
|
||||
}
|
||||
|
||||
$handle_data = new PhabricatorObjectHandleData($nodes);
|
||||
$objects = $handle_data->loadObjects();
|
||||
|
||||
$result = array();
|
||||
foreach ($nodes as $phid) {
|
||||
$object = idx($objects, $phid);
|
||||
if ($object) {
|
||||
$result[$phid] = $object->getAttachedPHIDs($this->edgeType);
|
||||
} else {
|
||||
$result[$phid] = array();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
15
src/applications/phid/graph/__init__.php
Normal file
15
src/applications/phid/graph/__init__.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'applications/phid/handle/data');
|
||||
|
||||
phutil_require_module('phutil', 'utils');
|
||||
phutil_require_module('phutil', 'utils/abstractgraph');
|
||||
|
||||
|
||||
phutil_require_source('PhabricatorObjectGraph.php');
|
|
@ -22,8 +22,9 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
|||
private $type;
|
||||
private $action;
|
||||
|
||||
const ACTION_ATTACH = 'attach';
|
||||
const ACTION_MERGE = 'merge';
|
||||
const ACTION_ATTACH = 'attach';
|
||||
const ACTION_MERGE = 'merge';
|
||||
const ACTION_DEPENDENCIES = 'dependencies';
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$this->phid = $data['phid'];
|
||||
|
@ -58,12 +59,23 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
|||
switch ($this->action) {
|
||||
case self::ACTION_MERGE:
|
||||
return $this->performMerge($object, $handle, $phids);
|
||||
|
||||
case self::ACTION_DEPENDENCIES:
|
||||
case self::ACTION_ATTACH:
|
||||
$two_way = true;
|
||||
if ($this->action == self::ACTION_DEPENDENCIES) {
|
||||
$two_way = false;
|
||||
$this->detectGraphCycles(
|
||||
$object,
|
||||
$attach_type,
|
||||
$phids);
|
||||
}
|
||||
$this->performAttach(
|
||||
$object_type,
|
||||
$object,
|
||||
$attach_type,
|
||||
$phids);
|
||||
$phids,
|
||||
$two_way);
|
||||
return id(new AphrontReloadResponse())->setURI($handle->getURI());
|
||||
default:
|
||||
throw new Exception("Unsupported attach action.");
|
||||
|
@ -71,6 +83,7 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
|||
} else {
|
||||
switch ($this->action) {
|
||||
case self::ACTION_ATTACH:
|
||||
case self::ACTION_DEPENDENCIES:
|
||||
$phids = $object->getAttachedPHIDs($attach_type);
|
||||
break;
|
||||
default:
|
||||
|
@ -132,7 +145,8 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
|||
$object_type,
|
||||
$object,
|
||||
$attach_type,
|
||||
array $phids) {
|
||||
array $phids,
|
||||
$two_way) {
|
||||
|
||||
$object_phid = $object->getPHID();
|
||||
|
||||
|
@ -161,6 +175,10 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
|||
// Update the primary object.
|
||||
$this->writeOutboundEdges($object_type, $object, $attach_type, $phids);
|
||||
|
||||
if (!$two_way) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through all of the attached/detached objects and update them.
|
||||
foreach ($attach_objs as $phid => $attach_obj) {
|
||||
$attached_phids = $attach_obj->getAttachedPHIDs($object_type);
|
||||
|
@ -290,6 +308,12 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
|||
"These tasks will be merged into the current task and then closed. ".
|
||||
"The current task will grow stronger.";
|
||||
break;
|
||||
case self::ACTION_DEPENDENCIES:
|
||||
$dialog_title = "Edit Dependencies";
|
||||
$header_text = "Current Dependencies";
|
||||
$button_text = "Save Dependencies";
|
||||
$instructions = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return array(
|
||||
|
@ -302,4 +326,39 @@ class PhabricatorSearchAttachController extends PhabricatorSearchController {
|
|||
);
|
||||
}
|
||||
|
||||
private function detectGraphCycles(
|
||||
$object,
|
||||
$attach_type,
|
||||
array $phids) {
|
||||
|
||||
// Detect graph cycles.
|
||||
$graph = new PhabricatorObjectGraph();
|
||||
$graph->setEdgeType($attach_type);
|
||||
$graph->addNodes(array(
|
||||
$object->getPHID() => $phids,
|
||||
));
|
||||
$graph->loadGraph();
|
||||
|
||||
foreach ($phids as $phid) {
|
||||
$cycle = $graph->detectCycles($phid);
|
||||
if (!$cycle) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Improve this behavior so it's not all-or-nothing?
|
||||
|
||||
$handles = id(new PhabricatorObjectHandleData($cycle))
|
||||
->loadHandles();
|
||||
$names = array();
|
||||
foreach ($cycle as $cycle_phid) {
|
||||
$names[] = $handles[$cycle_phid]->getFullName();
|
||||
}
|
||||
$names = implode(" \xE2\x86\x92 ", $names);
|
||||
$which = $handles[$phid]->getFullName();
|
||||
throw new Exception(
|
||||
"You can not create a dependency on '{$which}' because it ".
|
||||
"would create a circular dependency: {$names}.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ phutil_require_module('phabricator', 'applications/maniphest/editor/transaction'
|
|||
phutil_require_module('phabricator', 'applications/maniphest/storage/task');
|
||||
phutil_require_module('phabricator', 'applications/maniphest/storage/transaction');
|
||||
phutil_require_module('phabricator', 'applications/phid/constants');
|
||||
phutil_require_module('phabricator', 'applications/phid/graph');
|
||||
phutil_require_module('phabricator', 'applications/phid/handle/data');
|
||||
phutil_require_module('phabricator', 'applications/search/controller/search');
|
||||
phutil_require_module('phabricator', 'view/control/objectselector');
|
||||
|
|
|
@ -65,3 +65,7 @@
|
|||
.aphront-headsup-action-list .action-merge {
|
||||
background-image: url(/rsrc/image/icon/fatcow/arrow_merge.png);
|
||||
}
|
||||
|
||||
.aphront-headsup-action-list .action-dependencies {
|
||||
background-image: url(/rsrc/image/icon/fatcow/link.png);
|
||||
}
|
||||
|
|
BIN
webroot/rsrc/image/icon/fatcow/link.png
Executable file
BIN
webroot/rsrc/image/icon/fatcow/link.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 649 B |
Loading…
Reference in a new issue