Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
index 512a834a5d..cd4450f0c7 100644
--- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
@@ -1,587 +1,601 @@
<?php
/**
* @group maniphest
*/
final class ManiphestTaskDetailController extends ManiphestController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$e_title = null;
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
$task = id(new ManiphestTask())->load($this->id);
if (!$task) {
return new Aphront404Response();
}
$workflow = $request->getStr('workflow');
$parent_task = null;
if ($workflow && is_numeric($workflow)) {
$parent_task = id(new ManiphestTask())->load($workflow);
}
$transactions = id(new ManiphestTransaction())->loadAllWhere(
'taskID = %d ORDER BY id ASC',
$task->getID());
$extensions = ManiphestTaskExtensions::newExtensions();
$aux_fields = $extensions->loadFields($task, $user);
$e_commit = PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT;
$e_dep_on = PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK;
$e_dep_by = PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK;
$e_rev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV;
+ $e_mock = PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK;
$phid = $task->getPHID();
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($phid))
->withEdgeTypes(
array(
$e_commit,
$e_dep_on,
$e_dep_by,
$e_rev,
+ $e_mock,
));
$edges = idx($query->execute(), $phid);
$phids = array_fill_keys($query->getDestinationPHIDs(), true);
foreach ($transactions as $transaction) {
foreach ($transaction->extractPHIDs() as $phid) {
$phids[$phid] = true;
}
}
foreach ($task->getCCPHIDs() as $phid) {
$phids[$phid] = true;
}
foreach ($task->getProjectPHIDs() as $phid) {
$phids[$phid] = true;
}
if ($task->getOwnerPHID()) {
$phids[$task->getOwnerPHID()] = true;
}
$phids[$task->getAuthorPHID()] = true;
$attached = $task->getAttached();
foreach ($attached as $type => $list) {
foreach ($list as $phid => $info) {
$phids[$phid] = true;
}
}
if ($parent_task) {
$phids[$parent_task->getPHID()] = true;
}
$phids = array_keys($phids);
$phids = array_merge(
$phids,
array_mergev(mpull($aux_fields, 'getRequiredHandlePHIDs')));
$this->loadHandles($phids);
$handles = $this->getLoadedHandles();
foreach ($aux_fields as $aux_field) {
$aux_field->setHandles($handles);
}
$context_bar = null;
if ($parent_task) {
$context_bar = new AphrontContextBarView();
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/?parent='.$parent_task->getID(),
'class' => 'green button',
),
pht('Create Another Subtask')));
$context_bar->appendChild(hsprintf(
'Created a subtask of <strong>%s</strong>',
$this->getHandle($parent_task->getPHID())->renderLink()));
} else if ($workflow == 'create') {
$context_bar = new AphrontContextBarView();
$context_bar->addButton(phutil_tag('label', array(), 'Create Another'));
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/?template='.$task->getID(),
'class' => 'green button',
),
pht('Similar Task')));
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/',
'class' => 'green button',
),
pht('Empty Task')));
$context_bar->appendChild(pht('New task created.'));
}
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
$engine->addObject($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION);
foreach ($transactions as $xaction) {
if ($xaction->hasComments()) {
$engine->addObject($xaction, ManiphestTransaction::MARKUP_FIELD_BODY);
}
}
foreach ($aux_fields as $aux_field) {
foreach ($aux_field->getMarkupFields() as $markup_field) {
$engine->addObject($aux_field, $markup_field);
$aux_field->setMarkupEngine($engine);
}
}
$engine->process();
$transaction_types = ManiphestTransactionType::getTransactionTypeMap();
$resolution_types = ManiphestTaskStatus::getTaskStatusMap();
if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) {
$resolution_types = array_select_keys(
$resolution_types,
array(
ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
ManiphestTaskStatus::STATUS_CLOSED_INVALID,
ManiphestTaskStatus::STATUS_CLOSED_SPITE,
));
} else {
$resolution_types = array(
ManiphestTaskStatus::STATUS_OPEN => 'Reopened',
);
$transaction_types[ManiphestTransactionType::TYPE_STATUS] =
'Reopen Task';
unset($transaction_types[ManiphestTransactionType::TYPE_PRIORITY]);
unset($transaction_types[ManiphestTransactionType::TYPE_OWNER]);
}
$default_claim = array(
$user->getPHID() => $user->getUsername().' ('.$user->getRealName().')',
);
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$task->getPHID());
if ($draft) {
$draft_text = $draft->getDraft();
} else {
$draft_text = null;
}
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
// Prevent tasks from being closed "out of spite" in serious business
// installs.
unset($resolution_types[ManiphestTaskStatus::STATUS_CLOSED_SPITE]);
}
$comment_form = new AphrontFormView();
$comment_form
->setUser($user)
->setAction('/maniphest/transaction/save/')
->setEncType('multipart/form-data')
->addHiddenInput('taskID', $task->getID())
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Action'))
->setName('action')
->setOptions($transaction_types)
->setID('transaction-action'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Resolution'))
->setName('resolution')
->setControlID('resolution')
->setControlStyle('display: none')
->setOptions($resolution_types))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Assign To'))
->setName('assign_to')
->setControlID('assign_to')
->setControlStyle('display: none')
->setID('assign-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('CCs'))
->setName('ccs')
->setControlID('ccs')
->setControlStyle('display: none')
->setID('cc-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Priority'))
->setName('priority')
->setOptions($priority_map)
->setControlID('priority')
->setControlStyle('display: none')
->setValue($task->getPriority()))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setControlID('projects')
->setControlStyle('display: none')
->setID('projects-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormFileControl())
->setLabel(pht('File'))
->setName('file')
->setControlID('file')
->setControlStyle('display: none'))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel(pht('Comments'))
->setName('comments')
->setValue($draft_text)
->setID('transaction-comments')
->setUser($user))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($is_serious ? pht('Submit') : pht('Avast!')));
$control_map = array(
ManiphestTransactionType::TYPE_STATUS => 'resolution',
ManiphestTransactionType::TYPE_OWNER => 'assign_to',
ManiphestTransactionType::TYPE_CCS => 'ccs',
ManiphestTransactionType::TYPE_PRIORITY => 'priority',
ManiphestTransactionType::TYPE_PROJECTS => 'projects',
ManiphestTransactionType::TYPE_ATTACH => 'file',
);
$tokenizer_map = array(
ManiphestTransactionType::TYPE_PROJECTS => array(
'id' => 'projects-tokenizer',
'src' => '/typeahead/common/projects/',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => pht('Type a project name...'),
),
ManiphestTransactionType::TYPE_OWNER => array(
'id' => 'assign-tokenizer',
'src' => '/typeahead/common/users/',
'value' => $default_claim,
'limit' => 1,
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => pht('Type a user name...'),
),
ManiphestTransactionType::TYPE_CCS => array(
'id' => 'cc-tokenizer',
'src' => '/typeahead/common/mailable/',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => pht('Type a user or mailing list...'),
),
);
Javelin::initBehavior('maniphest-transaction-controls', array(
'select' => 'transaction-action',
'controlMap' => $control_map,
'tokenizers' => $tokenizer_map,
));
Javelin::initBehavior('maniphest-transaction-preview', array(
'uri' => '/maniphest/transaction/preview/'.$task->getID().'/',
'preview' => 'transaction-preview',
'comments' => 'transaction-comments',
'action' => 'transaction-action',
'map' => $control_map,
'tokenizers' => $tokenizer_map,
));
$comment_header = id(new PhabricatorHeaderView())
->setHeader($is_serious ? pht('Add Comment') : pht('Weigh In'));
$preview_panel = hsprintf(
'<div class="aphront-panel-preview">
<div id="transaction-preview">
<div class="aphront-panel-preview-loading-text">%s</div>
</div>
</div>',
pht('Loading preview...'));
$transaction_view = new ManiphestTransactionListView();
$transaction_view->setTransactions($transactions);
$transaction_view->setHandles($this->getLoadedHandles());
$transaction_view->setUser($user);
$transaction_view->setAuxiliaryFields($aux_fields);
$transaction_view->setMarkupEngine($engine);
$object_name = 'T'.$task->getID();
$actions = $this->buildActionView($task);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($object_name)
->setHref('/'.$object_name))
->setActionList($actions)
->addAction(
id(new PHUIListItemView())
->setHref($this->getApplicationURI('/task/create/'))
->setName(pht('Create Task'))
->setIcon('create'));
$header = $this->buildHeaderView($task);
$properties = $this->buildPropertyView($task, $aux_fields, $edges, $engine);
return $this->buildApplicationPage(
array(
$crumbs,
$context_bar,
$header,
$actions,
$properties,
$transaction_view,
$comment_header,
$comment_form,
$preview_panel,
),
array(
'title' => 'T'.$task->getID().' '.$task->getTitle(),
'pageObjects' => array($task->getPHID()),
'device' => true,
));
}
private function buildHeaderView(ManiphestTask $task) {
$view = id(new PhabricatorHeaderView())
->setHeader($task->getTitle());
$view->addTag(ManiphestView::renderTagForTask($task));
return $view;
}
private function buildActionView(ManiphestTask $task) {
$viewer = $this->getRequest()->getUser();
$viewer_phid = $viewer->getPHID();
$viewer_is_cc = in_array($viewer_phid, $task->getCCPHIDs());
$id = $task->getID();
$phid = $task->getPHID();
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($task)
->setObjectURI($this->getRequest()->getRequestURI())
->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Task'))
->setIcon('edit')
->setHref($this->getApplicationURI("/task/edit/{$id}/")));
if ($task->getOwnerPHID() === $viewer_phid) {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Automatically Subscribed'))
->setDisabled(true)
->setIcon('enable'));
} else {
$action = $viewer_is_cc ? 'rem' : 'add';
$name = $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe');
$icon = $viewer_is_cc ? 'disable' : 'check';
$view->addAction(
id(new PhabricatorActionView())
->setName($name)
->setHref("/maniphest/subscribe/{$action}/{$id}/")
->setRenderAsForm(true)
->setUser($viewer)
->setIcon($icon));
}
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Merge Duplicates In'))
->setHref("/search/attach/{$phid}/TASK/merge/")
->setWorkflow(true)
->setIcon('merge'));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Create Subtask'))
->setHref($this->getApplicationURI("/task/create/?parent={$id}"))
->setIcon('fork'));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Dependencies'))
->setHref("/search/attach/{$phid}/TASK/dependencies/")
->setWorkflow(true)
->setIcon('link'));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Differential Revisions'))
->setHref("/search/attach/{$phid}/DREV/")
->setWorkflow(true)
->setIcon('attach'));
+ $pholio_app =
+ PhabricatorApplication::getByClass('PhabricatorApplicationPholio');
+ if ($pholio_app->isInstalled()) {
+ $view->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Edit Pholio Mocks'))
+ ->setHref("/search/attach/{$phid}/MOCK/edge/")
+ ->setWorkflow(true)
+ ->setIcon('attach'));
+ }
+
return $view;
}
private function buildPropertyView(
ManiphestTask $task,
array $aux_fields,
array $edges,
PhabricatorMarkupEngine $engine) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorPropertyListView())
->setUser($viewer)
->setObject($task);
$view->addProperty(
pht('Assigned To'),
$task->getOwnerPHID()
- ? $this->getHandle($task->getOwnerPHID())->renderLink()
- : phutil_tag('em', array(), pht('None')));
+ ? $this->getHandle($task->getOwnerPHID())->renderLink()
+ : phutil_tag('em', array(), pht('None')));
$view->addProperty(
pht('Priority'),
ManiphestTaskPriority::getTaskPriorityName($task->getPriority()));
$view->addProperty(
pht('Subscribers'),
$task->getCCPHIDs()
- ? $this->renderHandlesForPHIDs($task->getCCPHIDs(), ',')
- : phutil_tag('em', array(), pht('None')));
+ ? $this->renderHandlesForPHIDs($task->getCCPHIDs(), ',')
+ : phutil_tag('em', array(), pht('None')));
$view->addProperty(
pht('Author'),
$this->getHandle($task->getAuthorPHID())->renderLink());
$source = $task->getOriginalEmailSource();
if ($source) {
$subject = '[T'.$task->getID().'] '.$task->getTitle();
$view->addProperty(
pht('From Email'),
phutil_tag(
'a',
array(
'href' => 'mailto:'.$source.'?subject='.$subject
- ),
+ ),
$source));
}
$view->addProperty(
pht('Projects'),
$task->getProjectPHIDs()
- ? $this->renderHandlesForPHIDs($task->getProjectPHIDs(), ',')
- : phutil_tag('em', array(), pht('None')));
+ ? $this->renderHandlesForPHIDs($task->getProjectPHIDs(), ',')
+ : phutil_tag('em', array(), pht('None')));
foreach ($aux_fields as $aux_field) {
$value = $aux_field->renderForDetailView();
if (strlen($value)) {
$view->addProperty($aux_field->getLabel(), $value);
}
}
$edge_types = array(
PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK
- => pht('Dependent Tasks'),
+ => pht('Dependent Tasks'),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK
- => pht('Depends On'),
+ => pht('Depends On'),
PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV
- => pht('Differential Revisions'),
+ => pht('Differential Revisions'),
+ PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK
+ => pht('Pholio Mocks'),
);
$revisions_commits = array();
$handles = $this->getLoadedHandles();
$commit_phids = array_keys(
$edges[PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT]);
if ($commit_phids) {
$commit_drev = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV;
$drev_edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($commit_phids)
->withEdgeTypes(array($commit_drev))
->execute();
foreach ($commit_phids as $phid) {
$revisions_commits[$phid] = $handles[$phid]->renderLink();
$revision_phid = key($drev_edges[$phid][$commit_drev]);
$revision_handle = idx($handles, $revision_phid);
if ($revision_handle) {
$task_drev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV;
unset($edges[$task_drev][$revision_phid]);
$revisions_commits[$phid] = hsprintf(
'%s / %s',
$revision_handle->renderLink($revision_handle->getName()),
$revisions_commits[$phid]);
}
}
}
foreach ($edge_types as $edge_type => $edge_name) {
if ($edges[$edge_type]) {
$view->addProperty(
$edge_name,
$this->renderHandlesForPHIDs(array_keys($edges[$edge_type])));
}
}
if ($revisions_commits) {
$view->addProperty(
pht('Commits'),
phutil_implode_html(phutil_tag('br'), $revisions_commits));
}
$attached = $task->getAttached();
$file_infos = idx($attached, PhabricatorPHIDConstants::PHID_TYPE_FILE);
if ($file_infos) {
$file_phids = array_keys($file_infos);
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
$file_view = new PhabricatorFileLinkListView();
$file_view->setFiles($files);
$view->addProperty(
pht('Files'),
$file_view->render());
}
$view->invokeWillRenderEvent();
if (strlen($task->getDescription())) {
$view->addSectionHeader(pht('Description'));
$view->addTextContent(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->getOutput($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION)));
}
return $view;
}
-
}
diff --git a/src/applications/maniphest/view/ManiphestTransactionDetailView.php b/src/applications/maniphest/view/ManiphestTransactionDetailView.php
index 35e40cb8d5..68cf06a888 100644
--- a/src/applications/maniphest/view/ManiphestTransactionDetailView.php
+++ b/src/applications/maniphest/view/ManiphestTransactionDetailView.php
@@ -1,837 +1,857 @@
<?php
/**
* @group maniphest
*/
final class ManiphestTransactionDetailView extends ManiphestView {
private $transactions;
private $handles;
private $markupEngine;
private $forEmail;
private $preview;
private $commentNumber;
private $rangeSpecification;
private $renderSummaryOnly;
private $renderFullSummary;
private $auxiliaryFields;
public function setAuxiliaryFields(array $fields) {
assert_instances_of($fields, 'ManiphestAuxiliaryFieldSpecification');
$this->auxiliaryFields = mpull($fields, null, 'getAuxiliaryKey');
return $this;
}
public function getAuxiliaryField($key) {
return idx($this->auxiliaryFields, $key);
}
public function setTransactionGroup(array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$this->transactions = $transactions;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function setRenderSummaryOnly($render_summary_only) {
$this->renderSummaryOnly = $render_summary_only;
return $this;
}
public function getRenderSummaryOnly() {
return $this->renderSummaryOnly;
}
public function setRenderFullSummary($render_full_summary) {
$this->renderFullSummary = $render_full_summary;
return $this;
}
public function getRenderFullSummary() {
return $this->renderFullSummary;
}
public function setCommentNumber($comment_number) {
$this->commentNumber = $comment_number;
return $this;
}
public function setRangeSpecification($range) {
$this->rangeSpecification = $range;
return $this;
}
public function getRangeSpecification() {
return $this->rangeSpecification;
}
public function renderForEmail($with_date) {
$this->forEmail = true;
$transaction = reset($this->transactions);
$author = $this->renderHandles(array($transaction->getAuthorPHID()));
$action = null;
$descs = array();
$comments = null;
foreach ($this->transactions as $transaction) {
list($verb, $desc, $classes) = $this->describeAction($transaction);
if ($desc === null) {
continue;
}
if ($action === null) {
$action = $verb;
}
$desc = $author.' '.$desc.'.';
if ($with_date) {
// NOTE: This is going into a (potentially multi-recipient) email so
// we can't use a single user's timezone preferences. Use the server's
// instead, but make the timezone explicit.
$datetime = date('M jS \a\t g:i A T', $transaction->getDateCreated());
$desc = "On {$datetime}, {$desc}";
}
$descs[] = $desc;
if ($transaction->hasComments()) {
$comments = $transaction->getComments();
}
}
$descs = implode("\n", $descs);
if ($comments) {
$descs .= "\n".$comments;
}
foreach ($this->transactions as $transaction) {
$supplemental = $this->renderSupplementalInfoForEmail($transaction);
if ($supplemental) {
$descs .= "\n\n".$supplemental;
}
}
$this->forEmail = false;
return array($action, $descs);
}
public function render() {
if (!$this->user) {
throw new Exception("Call setUser() before render()!");
}
$handles = $this->handles;
$transactions = $this->transactions;
require_celerity_resource('maniphest-transaction-detail-css');
$comment_transaction = null;
foreach ($this->transactions as $transaction) {
if ($transaction->hasComments()) {
$comment_transaction = $transaction;
break;
}
}
$any_transaction = reset($transactions);
$author = $this->handles[$any_transaction->getAuthorPHID()];
$more_classes = array();
$descs = array();
foreach ($transactions as $transaction) {
list($verb, $desc, $classes) = $this->describeAction($transaction);
if ($desc === null) {
continue;
}
$more_classes = array_merge($more_classes, $classes);
$full_summary = null;
if ($this->getRenderFullSummary()) {
$full_summary = $this->renderFullSummary($transaction);
}
$descs[] = javelin_tag(
'div',
array(
'sigil' => 'maniphest-transaction-description',
),
array(
$author->renderLink(),
' ',
$desc,
'.',
$full_summary,
));
}
if ($this->getRenderSummaryOnly()) {
return phutil_implode_html("\n", $descs);
}
if ($comment_transaction && $comment_transaction->hasComments()) {
$comment_block = $this->markupEngine->getOutput(
$comment_transaction,
ManiphestTransaction::MARKUP_FIELD_BODY);
$comment_block = phutil_tag(
'div',
array('class' => 'maniphest-transaction-comments phabricator-remarkup'),
$comment_block);
} else {
$comment_block = null;
}
$source_transaction = nonempty($comment_transaction, $any_transaction);
$xaction_view = id(new PhabricatorTransactionView())
->setUser($this->user)
->setImageURI($author->getImageURI())
->setContentSource($source_transaction->getContentSource())
->setActions($descs);
foreach ($more_classes as $class) {
$xaction_view->addClass($class);
}
if ($this->preview) {
$xaction_view->setIsPreview($this->preview);
} else {
$xaction_view->setEpoch($any_transaction->getDateCreated());
if ($this->commentNumber) {
$anchor_name = 'comment-'.$this->commentNumber;
$anchor_text =
'T'.$any_transaction->getTaskID().
'#'.$this->commentNumber;
$xaction_view->setAnchor($anchor_name, $anchor_text);
}
}
$xaction_view->appendChild($comment_block);
return $xaction_view->render();
}
private function renderSupplementalInfoForEmail($transaction) {
$handles = $this->handles;
$type = $transaction->getTransactionType();
$new = $transaction->getNewValue();
$old = $transaction->getOldValue();
switch ($type) {
case ManiphestTransactionType::TYPE_DESCRIPTION:
return "NEW DESCRIPTION\n ".trim($new)."\n\n".
"PREVIOUS DESCRIPTION\n ".trim($old);
case ManiphestTransactionType::TYPE_ATTACH:
$old_raw = nonempty($old, array());
$new_raw = nonempty($new, array());
$attach_types = array(
PhabricatorPHIDConstants::PHID_TYPE_DREV,
PhabricatorPHIDConstants::PHID_TYPE_FILE,
);
foreach ($attach_types as $attach_type) {
$old = array_keys(idx($old_raw, $attach_type, array()));
$new = array_keys(idx($new_raw, $attach_type, array()));
if ($old != $new) {
break;
}
}
$added = array_diff($new, $old);
if (!$added) {
break;
}
$links = array();
foreach (array_select_keys($handles, $added) as $handle) {
$links[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI());
}
$links = implode("\n", $links);
switch ($attach_type) {
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$title = 'ATTACHED REVISIONS';
break;
case PhabricatorPHIDConstants::PHID_TYPE_FILE:
$title = 'ATTACHED FILES';
break;
}
return $title."\n".$links;
case ManiphestTransactionType::TYPE_EDGE:
$add = array_diff_key($new, $old);
if (!$add) {
break;
}
$links = array();
foreach ($add as $phid => $ignored) {
$handle = $handles[$phid];
$links[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI());
}
$links = implode("\n", $links);
$edge_type = $transaction->getMetadataValue('edge:type');
$title = $this->getEdgeEmailTitle($edge_type, $add);
return $title."\n".$links;
default:
break;
}
return null;
}
private function describeAction($transaction) {
$verb = null;
$desc = null;
$classes = array();
$handles = $this->handles;
$type = $transaction->getTransactionType();
$author_phid = $transaction->getAuthorPHID();
$new = $transaction->getNewValue();
$old = $transaction->getOldValue();
switch ($type) {
case ManiphestTransactionType::TYPE_TITLE:
$verb = 'Retitled';
$desc = 'changed the title from '.$this->renderString($old).
' to '.$this->renderString($new);
break;
case ManiphestTransactionType::TYPE_DESCRIPTION:
$verb = 'Edited';
if ($this->forEmail || $this->getRenderFullSummary()) {
$desc = 'updated the task description';
} else {
$desc = 'updated the task description; '.
$this->renderExpandLink($transaction);
}
break;
case ManiphestTransactionType::TYPE_NONE:
$verb = 'Commented On';
$desc = 'added a comment';
break;
case ManiphestTransactionType::TYPE_OWNER:
if ($transaction->getAuthorPHID() == $new) {
$verb = 'Claimed';
$desc = 'claimed this task';
$classes[] = 'claimed';
} else if (!$new) {
$verb = 'Up For Grabs';
$desc = 'placed this task up for grabs';
$classes[] = 'upforgrab';
} else if (!$old) {
$verb = 'Assigned';
$desc = 'assigned this task to '.$this->renderHandles(array($new));
$classes[] = 'assigned';
} else {
$verb = 'Reassigned';
$desc = 'reassigned this task from '.
$this->renderHandles(array($old)).
' to '.
$this->renderHandles(array($new));
$classes[] = 'reassigned';
}
break;
case ManiphestTransactionType::TYPE_CCS:
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
// can only add in preview so just show placeholder if nothing to add
if ($this->preview && empty($added)) {
$verb = 'Changed CC';
$desc = 'changed CCs..';
break;
}
if ($added && !$removed) {
$verb = 'Added CC';
if (count($added) == 1) {
$desc = 'added '.$this->renderHandles($added).' to CC';
} else {
$desc = 'added CCs: '.$this->renderHandles($added);
}
} else if ($removed && !$added) {
$verb = 'Removed CC';
if (count($removed) == 1) {
$desc = 'removed '.$this->renderHandles($removed).' from CC';
} else {
$desc = 'removed CCs: '.$this->renderHandles($removed);
}
} else {
$verb = 'Changed CC';
$desc = 'changed CCs, added: '.$this->renderHandles($added).'; '.
'removed: '.$this->renderHandles($removed);
}
break;
case ManiphestTransactionType::TYPE_EDGE:
$edge_type = $transaction->getMetadataValue('edge:type');
$add = array_diff_key($new, $old);
$rem = array_diff_key($old, $new);
if ($add && !$rem) {
$verb = $this->getEdgeAddVerb($edge_type);
$desc = $this->getEdgeAddList($edge_type, $add);
} else if ($rem && !$add) {
$verb = $this->getEdgeRemVerb($edge_type);
$desc = $this->getEdgeRemList($edge_type, $rem);
} else {
$verb = $this->getEdgeEditVerb($edge_type);
$desc = $this->getEdgeEditList($edge_type, $add, $rem);
}
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
// can only add in preview so just show placeholder if nothing to add
if ($this->preview && empty($added)) {
$verb = 'Changed Projects';
$desc = 'changed projects..';
break;
}
if ($added && !$removed) {
$verb = 'Added Project';
if (count($added) == 1) {
$desc = 'added project '.$this->renderHandles($added);
} else {
$desc = 'added projects: '.$this->renderHandles($added);
}
} else if ($removed && !$added) {
$verb = 'Removed Project';
if (count($removed) == 1) {
$desc = 'removed project '.$this->renderHandles($removed);
} else {
$desc = 'removed projects: '.$this->renderHandles($removed);
}
} else {
$verb = 'Changed Projects';
$desc = 'changed projects, added: '.$this->renderHandles($added).'; '.
'removed: '.$this->renderHandles($removed);
}
break;
case ManiphestTransactionType::TYPE_STATUS:
if ($new == ManiphestTaskStatus::STATUS_OPEN) {
if ($old) {
$verb = 'Reopened';
$desc = 'reopened this task';
$classes[] = 'reopened';
} else {
$verb = 'Created';
$desc = 'created this task';
$classes[] = 'created';
}
} else if ($new == ManiphestTaskStatus::STATUS_CLOSED_SPITE) {
$verb = 'Spited';
$desc = 'closed this task out of spite';
$classes[] = 'spited';
} else if ($new == ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE) {
$verb = 'Merged';
$desc = 'closed this task as a duplicate';
$classes[] = 'duplicate';
} else {
$verb = 'Closed';
$full = idx(ManiphestTaskStatus::getTaskStatusMap(), $new, '???');
$desc = 'closed this task as "'.$full.'"';
$classes[] = 'closed';
}
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$old_name = ManiphestTaskPriority::getTaskPriorityName($old);
$new_name = ManiphestTaskPriority::getTaskPriorityName($new);
if ($old == ManiphestTaskPriority::PRIORITY_TRIAGE) {
$verb = 'Triaged';
$desc = 'triaged this task as "'.$new_name.'" priority';
} else if ($old > $new) {
$verb = 'Lowered Priority';
$desc = 'lowered the priority of this task from "'.$old_name.'" to '.
'"'.$new_name.'"';
} else {
$verb = 'Raised Priority';
$desc = 'raised the priority of this task from "'.$old_name.'" to '.
'"'.$new_name.'"';
}
if ($new == ManiphestTaskPriority::PRIORITY_UNBREAK_NOW) {
$classes[] = 'unbreaknow';
}
break;
case ManiphestTransactionType::TYPE_ATTACH:
if ($this->preview) {
$verb = 'Changed Attached';
$desc = 'changed attachments..';
break;
}
$old_raw = nonempty($old, array());
$new_raw = nonempty($new, array());
foreach (array(
PhabricatorPHIDConstants::PHID_TYPE_DREV,
PhabricatorPHIDConstants::PHID_TYPE_TASK,
PhabricatorPHIDConstants::PHID_TYPE_FILE) as $attach_type) {
$old = array_keys(idx($old_raw, $attach_type, array()));
$new = array_keys(idx($new_raw, $attach_type, array()));
if ($old != $new) {
break;
}
}
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
$add_desc = $this->renderHandles($added);
$rem_desc = $this->renderHandles($removed);
if ($added && !$removed) {
$verb = 'Attached';
$desc =
'attached '.
$this->getAttachName($attach_type, count($added)).': '.
$add_desc;
} else if ($removed && !$added) {
$verb = 'Detached';
$desc =
'detached '.
$this->getAttachName($attach_type, count($removed)).': '.
$rem_desc;
} else {
$verb = 'Changed Attached';
$desc =
'changed attached '.
$this->getAttachName($attach_type, count($added) + count($removed)).
', added: '.$add_desc.'; '.
'removed: '.$rem_desc;
}
break;
case ManiphestTransactionType::TYPE_AUXILIARY:
$aux_key = $transaction->getMetadataValue('aux:key');
$aux_field = $this->getAuxiliaryField($aux_key);
$verb = null;
if ($aux_field) {
$verb = $aux_field->renderTransactionEmailVerb($transaction);
}
if ($verb === null) {
if ($old === null) {
$verb = "Set Field";
} else if ($new === null) {
$verb = "Removed Field";
} else {
$verb = "Updated Field";
}
}
$desc = null;
if ($aux_field) {
$use_field = $aux_field;
} else {
$use_field = id(new ManiphestAuxiliaryFieldDefaultSpecification())
->setFieldType(
ManiphestAuxiliaryFieldDefaultSpecification::TYPE_STRING);
}
$desc = $use_field->renderTransactionDescription(
$transaction,
$this->forEmail
? ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_TEXT
: ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_HTML);
break;
default:
return array($type, ' brazenly '.$type."'d", $classes);
}
// TODO: [HTML] This code will all be rewritten when we switch to using
// ApplicationTransactions. It does not handle HTML or translations
// correctly right now.
$desc = phutil_safe_html($desc);
return array($verb, $desc, $classes);
}
private function renderFullSummary($transaction) {
switch ($transaction->getTransactionType()) {
case ManiphestTransactionType::TYPE_DESCRIPTION:
$id = $transaction->getID();
$old_text = phutil_utf8_hard_wrap($transaction->getOldValue(), 80);
$old_text = implode("\n", $old_text);
$new_text = phutil_utf8_hard_wrap($transaction->getNewValue(), 80);
$new_text = implode("\n", $new_text);
$engine = new PhabricatorDifferenceEngine();
$changeset = $engine->generateChangesetFromFileContent($old_text,
$new_text);
$whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
$parser = new DifferentialChangesetParser();
$parser->setChangeset($changeset);
$parser->setRenderingReference($id);
$parser->setMarkupEngine($this->markupEngine);
$parser->setWhitespaceMode($whitespace_mode);
$spec = $this->getRangeSpecification();
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$output = $parser->render($range_s, $range_e, $mask);
return $output;
}
return null;
}
private function renderExpandLink($transaction) {
$id = $transaction->getID();
Javelin::initBehavior('maniphest-transaction-expand');
return javelin_tag(
'a',
array(
'href' => '/maniphest/task/descriptionchange/'.$id.'/',
'sigil' => 'maniphest-expand-transaction',
'mustcapture' => true,
),
'show details');
}
private function renderHandles($phids, $full = false) {
$links = array();
foreach ($phids as $phid) {
if ($this->forEmail) {
if ($full) {
$links[] = $this->handles[$phid]->getFullName();
} else {
$links[] = $this->handles[$phid]->getName();
}
} else {
$links[] = $this->handles[$phid]->renderLink();
}
}
if ($this->forEmail) {
return implode(', ', $links);
} else {
return phutil_implode_html(', ', $links);
}
}
private function renderString($string) {
if ($this->forEmail) {
return '"'.$string.'"';
} else {
return '"'.phutil_escape_html($string).'"';
}
}
/* -( Strings )------------------------------------------------------------ */
/**
* @task strings
*/
private function getAttachName($attach_type, $count) {
switch ($attach_type) {
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
return pht('Differential Revision(s)', $count);
case PhabricatorPHIDConstants::PHID_TYPE_FILE:
return pht('file(s)', $count);
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
return pht('Maniphest Task(s)', $count);
}
}
/**
* @task strings
*/
private function getEdgeEmailTitle($type, array $list) {
$count = count($list);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('DIFFERENTIAL %d REVISION(S)', $count);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('DEPENDS ON %d TASK(S)', $count);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('DEPENDENT %d TASK(s)', $count);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('ATTACHED %d COMMIT(S)', $count);
+ case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
+ return pht('ATTACHED %d MOCK(S)', $count);
default:
return pht('ATTACHED %d OBJECT(S)', $count);
}
}
/**
* @task strings
*/
private function getEdgeAddVerb($type) {
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('Added Revision');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('Added Dependency');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('Added Dependent Task');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('Added Commit');
+ case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
+ return pht('Added Mock');
default:
return pht('Added Object');
}
}
/**
* @task strings
*/
private function getEdgeRemVerb($type) {
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('Removed Revision');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('Removed Dependency');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('Removed Dependent Task');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('Removed Commit');
+ case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
+ return pht('Removed Mock');
default:
return pht('Removed Object');
}
}
/**
* @task strings
*/
private function getEdgeEditVerb($type) {
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('Changed Revisions');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('Changed Dependencies');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('Changed Dependent Tasks');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('Changed Commits');
+ case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
+ return pht('Changed Mocks');
default:
return pht('Changed Objects');
}
}
/**
* @task strings
*/
private function getEdgeAddList($type, array $add) {
$list = $this->renderHandles(array_keys($add), $full = true);
$count = count($add);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('added %d revision(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('added %d dependencie(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('added %d dependent task(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('added %d commit(s): %s', $count, $list);
+ case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
+ return pht('added %d mock(s): %s', $count, $list);
default:
return pht('added %d object(s): %s', $count, $list);
}
}
/**
* @task strings
*/
private function getEdgeRemList($type, array $rem) {
$list = $this->renderHandles(array_keys($rem), $full = true);
$count = count($rem);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('removed %d revision(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('removed %d dependencie(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('removed %d dependent task(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('removed %d commit(s): %s', $count, $list);
+ case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
+ return pht('removed %d mock(s): %s', $count, $list);
default:
return pht('removed %d object(s): %s', $count, $list);
}
}
/**
* @task strings
*/
private function getEdgeEditList($type, array $add, array $rem) {
$add_list = $this->renderHandles(array_keys($add), $full = true);
$rem_list = $this->renderHandles(array_keys($rem), $full = true);
$add_count = count($add_list);
$rem_count = count($rem_list);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht(
'changed %d revision(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht(
'changed %d dependencie(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht(
'changed %d dependent task(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht(
'changed %d commit(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
+ case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK:
+ return pht(
+ 'changed %d mock(s), added %d: %s; removed %d: %s',
+ $add_count + $rem_count,
+ $add_count,
+ $add_list,
+ $rem_count,
+ $rem_list);
default:
return pht(
'changed %d object(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
}
}
}
diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php
index 3a214e5b74..8c4a14bccd 100644
--- a/src/applications/pholio/controller/PholioMockViewController.php
+++ b/src/applications/pholio/controller/PholioMockViewController.php
@@ -1,197 +1,224 @@
<?php
/**
* @group pholio
*/
final class PholioMockViewController extends PholioController {
private $id;
private $imageID;
+ private $maniphestTaskPHIDs = array();
+
+ private function setManiphestTaskPHIDs($maniphest_task_phids) {
+ $this->maniphestTaskPHIDs = $maniphest_task_phids;
+ return $this;
+ }
+ private function getManiphestTaskPHIDs() {
+ return $this->maniphestTaskPHIDs;
+ }
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->imageID = idx($data, 'imageID');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$mock = id(new PholioMockQuery())
->setViewer($user)
->withIDs(array($this->id))
->needImages(true)
->needCoverFiles(true)
->executeOne();
if (!$mock) {
return new Aphront404Response();
}
$xactions = id(new PholioTransactionQuery())
->setViewer($user)
->withObjectPHIDs(array($mock->getPHID()))
->execute();
- $phids = array($mock->getAuthorPHID());
+ $phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
+ $mock->getPHID(),
+ PhabricatorEdgeConfig::TYPE_MOCK_HAS_TASK);
+ $this->setManiphestTaskPHIDs($phids);
+ $phids[] = $mock->getAuthorPHID();
$this->loadHandles($phids);
$engine = id(new PhabricatorMarkupEngine())
->setViewer($user);
$engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION);
foreach ($xactions as $xaction) {
if ($xaction->getComment()) {
$engine->addObject(
$xaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
$title = $mock->getName();
$header = id(new PhabricatorHeaderView())
->setHeader($title);
$actions = $this->buildActionView($mock);
$properties = $this->buildPropertyView($mock, $engine);
require_celerity_resource('pholio-css');
require_celerity_resource('pholio-inline-comments-css');
$comment_form_id = celerity_generate_unique_node_id();
$output = id(new PholioMockImagesView())
->setRequestURI($request->getRequestURI())
->setCommentFormID($comment_form_id)
->setUser($user)
->setMock($mock)
->setImageID($this->imageID);
$xaction_view = id(new PholioTransactionView())
->setUser($this->getRequest()->getUser())
->setTransactions($xactions)
->setMarkupEngine($engine);
$add_comment = $this->buildAddCommentView($mock, $comment_form_id);
$crumbs = $this->buildApplicationCrumbs($this->buildSideNav());
$crumbs->setActionList($actions);
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName('M'.$mock->getID())
->setHref('/M'.$mock->getID()));
$content = array(
$crumbs,
$header,
$actions,
$properties,
$output->render(),
$xaction_view,
$add_comment,
);
return $this->buildApplicationPage(
$content,
array(
'title' => 'M'.$mock->getID().' '.$title,
'device' => true,
'pageObjects' => array($mock->getPHID()),
));
}
private function buildActionView(PholioMock $mock) {
$user = $this->getRequest()->getUser();
$actions = id(new PhabricatorActionListView())
->setUser($user)
->setObjectURI($this->getRequest()->getRequestURI())
->setObject($mock);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$mock,
PhabricatorPolicyCapability::CAN_EDIT);
$actions->addAction(
id(new PhabricatorActionView())
- ->setIcon('edit')
- ->setName(pht('Edit Mock'))
- ->setHref($this->getApplicationURI('/edit/'.$mock->getID().'/'))
- ->setDisabled(!$can_edit)
- ->setWorkflow(!$can_edit));
+ ->setIcon('edit')
+ ->setName(pht('Edit Mock'))
+ ->setHref($this->getApplicationURI('/edit/'.$mock->getID().'/'))
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(!$can_edit));
+
+ $actions->addAction(
+ id(new PhabricatorActionView())
+ ->setIcon('attach')
+ ->setName(pht('Edit Maniphest Tasks'))
+ ->setHref("/search/attach/{$mock->getPHID()}/TASK/edge/")
+ ->setDisabled(!$user->isLoggedIn())
+ ->setWorkflow(true));
return $actions;
}
private function buildPropertyView(
PholioMock $mock,
PhabricatorMarkupEngine $engine) {
$user = $this->getRequest()->getUser();
$properties = id(new PhabricatorPropertyListView())
->setUser($user)
->setObject($mock);
$properties->addProperty(
pht('Author'),
$this->getHandle($mock->getAuthorPHID())->renderLink());
$properties->addProperty(
pht('Created'),
phabricator_datetime($mock->getDateCreated(), $user));
$descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
$user,
$mock);
$properties->addProperty(
pht('Visible To'),
$descriptions[PhabricatorPolicyCapability::CAN_VIEW]);
+ if ($this->getManiphestTaskPHIDs()) {
+ $properties->addProperty(
+ pht('Maniphest Tasks'),
+ $this->renderHandlesForPHIDs($this->getManiphestTaskPHIDs()));
+ }
+
$properties->invokeWillRenderEvent();
$properties->addImageContent(
$engine->getOutput($mock, PholioMock::MARKUP_FIELD_DESCRIPTION));
return $properties;
}
private function buildAddCommentView(PholioMock $mock, $comment_form_id) {
$user = $this->getRequest()->getUser();
$draft = PhabricatorDraft::newFromUserAndKey($user, $mock->getPHID());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$title = $is_serious
? pht('Add Comment')
: pht('History Beckons');
$header = id(new PhabricatorHeaderView())
->setHeader($title);
$button_name = $is_serious
? pht('Add Comment')
: pht('Answer The Call');
$form = id(new PhabricatorApplicationTransactionCommentView())
->setUser($user)
->setFormID($comment_form_id)
->setDraft($draft)
->setSubmitButtonName($button_name)
->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/'))
->setRequestURI($this->getRequest()->getRequestURI());
return array(
$header,
$form,
);
}
}
diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php
index d52a514251..d5ef09248d 100644
--- a/src/applications/pholio/editor/PholioMockEditor.php
+++ b/src/applications/pholio/editor/PholioMockEditor.php
@@ -1,367 +1,368 @@
<?php
/**
* @group pholio
*/
final class PholioMockEditor extends PhabricatorApplicationTransactionEditor {
private $newImages = array();
private function setNewImages(array $new_images) {
assert_instances_of($new_images, 'PholioImage');
$this->newImages = $new_images;
return $this;
}
private function getNewImages() {
return $this->newImages;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
+ $types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PholioTransactionType::TYPE_NAME;
$types[] = PholioTransactionType::TYPE_DESCRIPTION;
$types[] = PholioTransactionType::TYPE_INLINE;
$types[] = PholioTransactionType::TYPE_IMAGE_FILE;
$types[] = PholioTransactionType::TYPE_IMAGE_NAME;
$types[] = PholioTransactionType::TYPE_IMAGE_DESCRIPTION;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_NAME:
return $object->getName();
case PholioTransactionType::TYPE_DESCRIPTION:
return $object->getDescription();
case PholioTransactionType::TYPE_IMAGE_FILE:
$images = $object->getImages();
return mpull($images, 'getPHID');
case PholioTransactionType::TYPE_IMAGE_NAME:
$name = null;
$phid = null;
$image = $this->getImageForXaction($object, $xaction);
if ($image) {
$name = $image->getName();
$phid = $image->getPHID();
}
return array ($phid => $name);
case PholioTransactionType::TYPE_IMAGE_DESCRIPTION:
$description = null;
$phid = null;
$image = $this->getImageForXaction($object, $xaction);
if ($image) {
$description = $image->getDescription();
$phid = $image->getPHID();
}
return array($phid => $description);
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_NAME:
case PholioTransactionType::TYPE_DESCRIPTION:
case PholioTransactionType::TYPE_IMAGE_NAME:
case PholioTransactionType::TYPE_IMAGE_DESCRIPTION:
return $xaction->getNewValue();
case PholioTransactionType::TYPE_IMAGE_FILE:
$raw_new_value = $xaction->getNewValue();
$new_value = array();
foreach ($raw_new_value as $key => $images) {
$new_value[$key] = mpull($images, 'getPHID');
}
$xaction->setNewValue($new_value);
return $this->getPHIDTransactionNewValue($xaction);
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_INLINE:
return true;
}
return parent::transactionHasEffect($object, $xaction);
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_IMAGE_FILE:
return true;
break;
}
}
return false;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$new_images = array();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_IMAGE_FILE:
$new_value = $xaction->getNewValue();
foreach ($new_value as $key => $txn_images) {
if ($key != '+') {
continue;
}
foreach ($txn_images as $image) {
$image->save();
$new_images[] = $image;
}
}
break;
}
}
$this->setNewImages($new_images);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_NAME:
$object->setName($xaction->getNewValue());
if ($object->getOriginalName() === null) {
$object->setOriginalName($xaction->getNewValue());
}
break;
case PholioTransactionType::TYPE_DESCRIPTION:
$object->setDescription($xaction->getNewValue());
break;
}
}
private function getImageForXaction(
PholioMock $mock,
PhabricatorApplicationTransaction $xaction) {
$raw_new_value = $xaction->getNewValue();
$image_phid = key($raw_new_value);
$images = $mock->getImages();
foreach ($images as $image) {
if ($image->getPHID() == $image_phid) {
return $image;
}
}
return null;
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_IMAGE_FILE:
$old_map = array_fuse($xaction->getOldValue());
$new_map = array_fuse($xaction->getNewValue());
$obsolete_map = array_diff_key($old_map, $new_map);
$images = $object->getImages();
foreach ($images as $seq => $image) {
if (isset($obsolete_map[$image->getPHID()])) {
$image->setIsObsolete(1);
$image->save();
unset($images[$seq]);
}
}
$object->attachImages($images);
break;
case PholioTransactionType::TYPE_IMAGE_NAME:
$image = $this->getImageForXaction($object, $xaction);
$value = (string) head($xaction->getNewValue());
$image->setName($value);
$image->save();
break;
case PholioTransactionType::TYPE_IMAGE_DESCRIPTION:
$image = $this->getImageForXaction($object, $xaction);
$value = (string) head($xaction->getNewValue());
$image->setDescription($value);
$image->save();
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$images = $this->getNewImages();
foreach ($images as $image) {
$image->setMockID($object->getID());
$image->save();
}
}
protected function mergeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$type = $u->getTransactionType();
switch ($type) {
case PholioTransactionType::TYPE_NAME:
case PholioTransactionType::TYPE_DESCRIPTION:
return $v;
case PholioTransactionType::TYPE_IMAGE_FILE:
return $this->mergePHIDOrEdgeTransactions($u, $v);
case PholioTransactionType::TYPE_IMAGE_NAME:
case PholioTransactionType::TYPE_IMAGE_DESCRIPTION:
$raw_new_value_u = $u->getNewValue();
$raw_new_value_v = $v->getNewValue();
$phid_u = key($raw_new_value_u);
$phid_v = key($raw_new_value_v);
if ($phid_u == $phid_v) {
return $v;
}
break;
}
return parent::mergeTransactions($u, $v);
}
protected function supportsMail() {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PholioReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
$original_name = $object->getOriginalName();
return id(new PhabricatorMetaMTAMail())
->setSubject("M{$id}: {$name}")
->addHeader('Thread-Topic', "M{$id}: {$original_name}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = new PhabricatorMetaMTAMailBody();
$headers = array();
$comments = array();
$inline_comments = array();
foreach ($xactions as $xaction) {
if ($xaction->shouldHide()) {
continue;
}
$comment = $xaction->getComment();
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_INLINE:
if ($comment && strlen($comment->getContent())) {
$inline_comments[] = $comment;
}
break;
case PhabricatorTransactions::TYPE_COMMENT:
if ($comment && strlen($comment->getContent())) {
$comments[] = $comment->getContent();
}
// fallthrough
default:
$headers[] = id(clone $xaction)
->setRenderingTarget('text')
->getTitle();
break;
}
}
$body->addRawSection(implode("\n", $headers));
foreach ($comments as $comment) {
$body->addRawSection($comment);
}
if ($inline_comments) {
$body->addRawSection(pht('INLINE COMMENTS'));
foreach ($inline_comments as $comment) {
$text = pht(
'Image %d: %s',
$comment->getImageID(),
$comment->getContent());
$body->addRawSection($text);
}
}
$body->addTextSection(
pht('MOCK DETAIL'),
PhabricatorEnv::getProductionURI('/M'.$object->getID()));
return $body;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.pholio.subject-prefix');
}
protected function supportsFeed() {
return true;
}
protected function supportsSearch() {
return true;
}
protected function sortTransactions(array $xactions) {
$head = array();
$tail = array();
// Move inline comments to the end, so the comments precede them.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PholioTransactionType::TYPE_INLINE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioTransactionType::TYPE_INLINE:
return true;
}
return parent::shouldImplyCC($object, $xaction);
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php
index a5070368d4..b101f53f01 100644
--- a/src/applications/search/controller/PhabricatorSearchAttachController.php
+++ b/src/applications/search/controller/PhabricatorSearchAttachController.php
@@ -1,278 +1,317 @@
<?php
/**
* @group search
*/
final class PhabricatorSearchAttachController
extends PhabricatorSearchBaseController {
private $phid;
private $type;
private $action;
const ACTION_ATTACH = 'attach';
const ACTION_MERGE = 'merge';
const ACTION_DEPENDENCIES = 'dependencies';
const ACTION_EDGE = 'edge';
public function willProcessRequest(array $data) {
$this->phid = $data['phid'];
$this->type = $data['type'];
$this->action = idx($data, 'action', self::ACTION_ATTACH);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$handle_data = new PhabricatorObjectHandleData(array($this->phid));
$handle_data->setViewer($user);
$handles = $handle_data->loadHandles();
$handle = $handles[$this->phid];
$object_type = $handle->getType();
$attach_type = $this->type;
$objects = $handle_data->loadObjects();
$object = idx($objects, $this->phid);
if (!$object) {
return new Aphront404Response();
}
$edge_type = null;
switch ($this->action) {
case self::ACTION_EDGE:
case self::ACTION_DEPENDENCIES:
case self::ACTION_ATTACH:
$edge_type = $this->getEdgeType($object_type, $attach_type);
break;
}
if ($request->isFormPost()) {
$phids = explode(';', $request->getStr('phids'));
$phids = array_filter($phids);
$phids = array_values($phids);
if ($edge_type) {
+ $do_txn = $object instanceof PhabricatorApplicationTransactionInterface;
$old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->phid,
$edge_type);
$add_phids = $phids;
$rem_phids = array_diff($old_phids, $add_phids);
- $editor = id(new PhabricatorEdgeEditor());
- $editor->setActor($user);
- foreach ($add_phids as $phid) {
- $editor->addEdge($this->phid, $edge_type, $phid);
- }
- foreach ($rem_phids as $phid) {
- $editor->removeEdge($this->phid, $edge_type, $phid);
- }
-
- try {
- $editor->save();
- } catch (PhabricatorEdgeCycleException $ex) {
- $this->raiseGraphCycleException($ex);
+ if ($do_txn) {
+
+ $txn_editor = $object->getApplicationTransactionEditor()
+ ->setActor($user)
+ ->setContentSourceFromRequest($request);
+ $txn_template = $object->getApplicationTransactionObject()
+ ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
+ ->setMetadataValue('edge:type', $edge_type)
+ ->setNewValue(array(
+ '+' => array_fuse($add_phids),
+ '-' => array_fuse($rem_phids)));
+ $txn_editor->applyTransactions($object, array($txn_template));
+
+ } else {
+
+ $editor = id(new PhabricatorEdgeEditor());
+ $editor->setActor($user);
+ foreach ($add_phids as $phid) {
+ $editor->addEdge($this->phid, $edge_type, $phid);
+ }
+ foreach ($rem_phids as $phid) {
+ $editor->removeEdge($this->phid, $edge_type, $phid);
+ }
+
+ try {
+ $editor->save();
+ } catch (PhabricatorEdgeCycleException $ex) {
+ $this->raiseGraphCycleException($ex);
+ }
}
return id(new AphrontReloadResponse())->setURI($handle->getURI());
} else {
return $this->performMerge($object, $handle, $phids);
}
} else {
if ($edge_type) {
$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->phid,
$edge_type);
} else {
// This is a merge.
$phids = array();
}
}
$strings = $this->getStrings();
$handles = $this->loadViewerHandles($phids);
$obj_dialog = new PhabricatorObjectSelectorDialog();
$obj_dialog
->setUser($user)
->setHandles($handles)
- ->setFilters(array(
- 'assigned' => 'Assigned to Me',
- 'created' => 'Created By Me',
- 'open' => 'All Open '.$strings['target_plural_noun'],
- 'all' => 'All '.$strings['target_plural_noun'],
- ))
+ ->setFilters($this->getFilters($strings))
->setSelectedFilter($strings['selected'])
->setExcluded($this->phid)
->setCancelURI($handle->getURI())
->setSearchURI('/search/select/'.$attach_type.'/')
->setTitle($strings['title'])
->setHeader($strings['header'])
->setButtonText($strings['button'])
->setInstructions($strings['instructions']);
$dialog = $obj_dialog->buildDialog();
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function performMerge(
ManiphestTask $task,
PhabricatorObjectHandle $handle,
array $phids) {
$user = $this->getRequest()->getUser();
$response = id(new AphrontReloadResponse())->setURI($handle->getURI());
$phids = array_fill_keys($phids, true);
unset($phids[$task->getPHID()]); // Prevent merging a task into itself.
if (!$phids) {
return $response;
}
$targets = id(new ManiphestTask())->loadAllWhere(
'phid in (%Ls) ORDER BY id ASC',
array_keys($phids));
if (empty($targets)) {
return $response;
}
$editor = new ManiphestTransactionEditor();
$editor->setActor($user);
$task_names = array();
$merge_into_name = 'T'.$task->getID();
$cc_vector = array();
$cc_vector[] = $task->getCCPHIDs();
foreach ($targets as $target) {
$cc_vector[] = $target->getCCPHIDs();
$cc_vector[] = array(
$target->getAuthorPHID(),
$target->getOwnerPHID());
$close_task = id(new ManiphestTransaction())
->setAuthorPHID($user->getPHID())
->setTransactionType(ManiphestTransactionType::TYPE_STATUS)
->setNewValue(ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE)
->setComments("\xE2\x9C\x98 Merged into {$merge_into_name}.");
$editor->applyTransactions($target, array($close_task));
$task_names[] = 'T'.$target->getID();
}
$all_ccs = array_mergev($cc_vector);
$all_ccs = array_filter($all_ccs);
$all_ccs = array_unique($all_ccs);
$task_names = implode(', ', $task_names);
$add_ccs = id(new ManiphestTransaction())
->setAuthorPHID($user->getPHID())
->setTransactionType(ManiphestTransactionType::TYPE_CCS)
->setNewValue($all_ccs)
->setComments("\xE2\x97\x80 Merged tasks: {$task_names}.");
$editor->applyTransactions($task, array($add_ccs));
return $response;
}
private function getStrings() {
switch ($this->type) {
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$noun = 'Revisions';
$selected = 'created';
break;
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
$noun = 'Tasks';
$selected = 'assigned';
break;
case PhabricatorPHIDConstants::PHID_TYPE_CMIT:
$noun = 'Commits';
$selected = 'created';
break;
+ case PhabricatorPHIDConstants::PHID_TYPE_MOCK:
+ $noun = 'Mocks';
+ $selected = 'created';
+ break;
}
switch ($this->action) {
case self::ACTION_EDGE:
case self::ACTION_ATTACH:
$dialog_title = "Manage Attached {$noun}";
$header_text = "Currently Attached {$noun}";
$button_text = "Save {$noun}";
$instructions = null;
break;
case self::ACTION_MERGE:
$dialog_title = "Merge Duplicate Tasks";
$header_text = "Tasks To Merge";
$button_text = "Merge {$noun}";
$instructions =
"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(
'target_plural_noun' => $noun,
'selected' => $selected,
'title' => $dialog_title,
'header' => $header_text,
'button' => $button_text,
'instructions' => $instructions,
);
}
+ private function getFilters(array $strings) {
+ if ($this->type == PhabricatorPHIDConstants::PHID_TYPE_MOCK) {
+ $filters = array(
+ 'created' => 'Created By Me',
+ 'all' => 'All '.$strings['target_plural_noun'],
+ );
+ } else {
+ $filters = array(
+ 'assigned' => 'Assigned to Me',
+ 'created' => 'Created By Me',
+ 'open' => 'All Open '.$strings['target_plural_noun'],
+ 'all' => 'All '.$strings['target_plural_noun'],
+ );
+ }
+
+ return $filters;
+ }
+
private function getEdgeType($src_type, $dst_type) {
$t_cmit = PhabricatorPHIDConstants::PHID_TYPE_CMIT;
$t_task = PhabricatorPHIDConstants::PHID_TYPE_TASK;
$t_drev = PhabricatorPHIDConstants::PHID_TYPE_DREV;
+ $t_mock = PhabricatorPHIDConstants::PHID_TYPE_MOCK;
$map = array(
$t_cmit => array(
$t_task => PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK,
),
$t_task => array(
$t_cmit => PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT,
$t_task => PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK,
$t_drev => PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV,
+ $t_mock => PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK,
),
$t_drev => array(
$t_drev => PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV,
$t_task => PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK,
),
+ $t_mock => array(
+ $t_task => PhabricatorEdgeConfig::TYPE_MOCK_HAS_TASK,
+ ),
);
if (empty($map[$src_type][$dst_type])) {
return null;
}
return $map[$src_type][$dst_type];
}
private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) {
$cycle = $ex->getCycle();
$handles = $this->loadViewerHandles($cycle);
$names = array();
foreach ($cycle as $cycle_phid) {
$names[] = $handles[$cycle_phid]->getFullName();
}
$names = implode(" \xE2\x86\x92 ", $names);
throw new Exception(
"You can not create that dependency, because it would create a ".
"circular dependency: {$names}.");
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchSelectController.php b/src/applications/search/controller/PhabricatorSearchSelectController.php
index c696dc045e..03f620c9ec 100644
--- a/src/applications/search/controller/PhabricatorSearchSelectController.php
+++ b/src/applications/search/controller/PhabricatorSearchSelectController.php
@@ -1,105 +1,117 @@
<?php
/**
* @group search
*/
final class PhabricatorSearchSelectController
extends PhabricatorSearchBaseController {
private $type;
public function willProcessRequest(array $data) {
$this->type = $data['type'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$query = new PhabricatorSearchQuery();
$query_str = $request->getStr('query');
$matches = array();
$query->setQuery($query_str);
$query->setParameter('type', $this->type);
switch ($request->getStr('filter')) {
case 'assigned':
$query->setParameter('owner', array($user->getPHID()));
$query->setParameter('open', 1);
break;
case 'created';
$query->setParameter('author', array($user->getPHID()));
- $query->setParameter('open', 1);
+ // TODO - if / when we allow pholio mocks to be archived, etc
+ // update this
+ if ($this->type != PhabricatorPHIDConstants::PHID_TYPE_MOCK) {
+ $query->setParameter('open', 1);
+ }
break;
case 'open':
$query->setParameter('open', 1);
break;
}
$query->setParameter('exclude', $request->getStr('exclude'));
$query->setParameter('limit', 100);
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
$results = $engine->executeSearch($query);
$phids = array_fill_keys($results, true);
$phids += $this->queryObjectNames($query_str);
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$data = array();
foreach ($handles as $handle) {
$view = new PhabricatorHandleObjectSelectorDataView($handle);
$data[] = $view->renderData();
}
return id(new AphrontAjaxResponse())->setContent($data);
}
private function queryObjectNames($query) {
$pattern = null;
switch ($this->type) {
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
$pattern = '/\bT(\d+)\b/i';
break;
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$pattern = '/\bD(\d+)\b/i';
break;
+ case PhabricatorPHIDConstants::PHID_TYPE_MOCK:
+ $pattern = '/\bM(\d+)\b/i';
+ break;
}
if (!$pattern) {
return array();
}
$matches = array();
preg_match_all($pattern, $query, $matches);
if (!$matches) {
return array();
}
$object_ids = $matches[1];
if (!$object_ids) {
return array();
}
switch ($this->type) {
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$objects = id(new DifferentialRevision())->loadAllWhere(
'id IN (%Ld)',
$object_ids);
break;
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
$objects = id(new ManiphestTask())->loadAllWhere(
'id IN (%Ld)',
$object_ids);
break;
+ case PhabricatorPHIDConstants::PHID_TYPE_MOCK:
+ $objects = id(new PholioMock())->loadAllWhere(
+ 'id IN (%Ld)',
+ $object_ids);
+ break;
}
return array_fill_keys(mpull($objects, 'getPHID'), true);
}
}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index fd7dded312..88a88282e1 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1,1347 +1,1346 @@
<?php
/**
* @task mail Sending Mail
* @task feed Publishing Feed Stories
* @task search Search Index
*/
abstract class PhabricatorApplicationTransactionEditor
extends PhabricatorEditor {
private $contentSource;
private $object;
private $xactions;
private $isNewObject;
private $mentionedPHIDs;
private $continueOnNoEffect;
private $parentMessageID;
private $subscribers;
private $isPreview;
/**
* When the editor tries to apply transactions that have no effect, should
* it raise an exception (default) or drop them and continue?
*
* Generally, you will set this flag for edits coming from "Edit" interfaces,
* and leave it cleared for edits coming from "Comment" interfaces, so the
* user will get a useful error if they try to submit a comment that does
* nothing (e.g., empty comment with a status change that has already been
* performed by another user).
*
* @param bool True to drop transactions without effect and continue.
* @return this
*/
public function setContinueOnNoEffect($continue) {
$this->continueOnNoEffect = $continue;
return $this;
}
public function getContinueOnNoEffect() {
return $this->continueOnNoEffect;
}
/**
* Not strictly necessary, but reply handlers ideally set this value to
* make email threading work better.
*/
public function setParentMessageID($parent_message_id) {
$this->parentMessageID = $parent_message_id;
return $this;
}
public function getParentMessageID() {
return $this->parentMessageID;
}
protected function getIsNewObject() {
return $this->isNewObject;
}
protected function getMentionedPHIDs() {
return $this->mentionedPHIDs;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function getTransactionTypes() {
$types = array();
if ($this->object instanceof PhabricatorSubscribableInterface) {
$types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
}
if ($this->object instanceof PhabricatorCustomFieldInterface) {
$types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
}
return $types;
}
private function adjustTransactionValues(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $this->getTransactionOldValue($object, $xaction);
$xaction->setOldValue($old);
$new = $this->getTransactionNewValue($object, $xaction);
$xaction->setNewValue($new);
}
private function getTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return array_values($this->subscribers);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return $object->getViewPolicy();
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return $object->getEditPolicy();
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $xaction->getMetadataValue('edge:type');
if (!$edge_type) {
throw new Exception("Edge transaction has no 'edge:type'!");
}
$old_edges = array();
if ($object->getPHID()) {
$edge_src = $object->getPHID();
$old_edges = id(new PhabricatorEdgeQuery())
- ->setViewer($this->getActor())
->withSourcePHIDs(array($edge_src))
->withEdgeTypes(array($edge_type))
->needEdgeData(true)
->execute();
$old_edges = $old_edges[$edge_src][$edge_type];
}
return $old_edges;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getOldValueForApplicationTransactions();
default:
return $this->getCustomTransactionOldValue($object, $xaction);
}
}
private function getTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->getPHIDTransactionNewValue($xaction);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return $xaction->getNewValue();
case PhabricatorTransactions::TYPE_EDGE:
return $this->getEdgeTransactionNewValue($xaction);
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getNewValueFromApplicationTransactions($xaction);
default:
return $this->getCustomTransactionNewValue($object, $xaction);
}
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception("Capability not supported!");
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception("Capability not supported!");
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return $xaction->hasComment();
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getApplicationTransactionHasEffect($xaction);
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
throw new Exception('Not implemented.');
}
private function applyInternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$object->setViewPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$object->setEditPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionInternalEffects($xaction);
}
return $this->applyCustomInternalTransaction($object, $xaction);
}
private function applyExternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($object)
->setActor($this->requireActor());
$old_map = array_fuse($xaction->getOldValue());
$new_map = array_fuse($xaction->getNewValue());
$subeditor->unsubscribe(
array_keys(
array_diff_key($old_map, $new_map)));
$subeditor->subscribeExplicit(
array_keys(
array_diff_key($new_map, $old_map)));
$subeditor->save();
// for the rest of these edits, subscribers should include those just
// added as well as those just removed.
$subscribers = array_unique(array_merge(
$this->subscribers,
$xaction->getOldValue(),
$xaction->getNewValue()));
$this->subscribers = $subscribers;
break;
case PhabricatorTransactions::TYPE_EDGE:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$src = $object->getPHID();
$type = $xaction->getMetadataValue('edge:type');
foreach ($new as $dst_phid => $edge) {
$new[$dst_phid]['src'] = $src;
}
$editor = id(new PhabricatorEdgeEditor())
->setActor($this->getActor());
foreach ($old as $dst_phid => $edge) {
if (!empty($new[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$editor->removeEdge($src, $type, $dst_phid);
}
foreach ($new as $dst_phid => $edge) {
if (!empty($old[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$data = array(
'data' => $edge['data'],
);
$editor->addEdge($src, $type, $dst_phid, $data);
}
$editor->save();
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionExternalEffects($xaction);
}
return $this->applyCustomExternalTransaction($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception("Capability not supported!");
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception("Capability not supported!");
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function setContentSourceFromRequest(AphrontRequest $request) {
return $this->setContentSource(
PhabricatorContentSource::newFromRequest($request));
}
public function getContentSource() {
return $this->contentSource;
}
final public function applyTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->object = $object;
$this->xactions = $xactions;
$this->isNewObject = ($object->getPHID() === null);
$this->validateEditParameters($object, $xactions);
$actor = $this->requireActor();
$this->loadSubscribers($object);
$xactions = $this->applyImplicitCC($object, $xactions);
$mention_xaction = $this->buildMentionTransaction($object, $xactions);
if ($mention_xaction) {
$xactions[] = $mention_xaction;
}
$xactions = $this->combineTransactions($xactions);
foreach ($xactions as $xaction) {
// TODO: This needs to be more sophisticated once we have meta-policies.
$xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
$xaction->setEditPolicy($actor->getPHID());
$xaction->setAuthorPHID($actor->getPHID());
$xaction->setContentSource($this->getContentSource());
}
$is_preview = $this->getIsPreview();
$read_locking = false;
$transaction_open = false;
if (!$is_preview) {
if ($object->getID()) {
foreach ($xactions as $xaction) {
// If any of the transactions require a read lock, hold one and
// reload the object. We need to do this fairly early so that the
// call to `adjustTransactionValues()` (which populates old values)
// is based on the synchronized state of the object, which may differ
// from the state when it was originally loaded.
if ($this->shouldReadLock($object, $xaction)) {
$object->openTransaction();
$object->beginReadLocking();
$transaction_open = true;
$read_locking = true;
$object->reload();
break;
}
}
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
}
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
$this->applyInitialEffects($object, $xactions);
}
foreach ($xactions as $xaction) {
$this->adjustTransactionValues($object, $xaction);
}
$xactions = $this->filterTransactions($object, $xactions);
if (!$xactions) {
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
if ($transaction_open) {
$object->killTransaction();
$transaction_open = false;
}
return array();
}
$xactions = $this->sortTransactions($xactions);
if ($is_preview) {
$this->loadHandles($xactions);
return $xactions;
}
$comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
->setActor($actor)
->setContentSource($this->getContentSource());
if (!$transaction_open) {
$object->openTransaction();
}
foreach ($xactions as $xaction) {
$this->applyInternalEffects($object, $xaction);
}
$object->save();
foreach ($xactions as $xaction) {
$xaction->setObjectPHID($object->getPHID());
if ($xaction->getComment()) {
$xaction->setPHID($xaction->generatePHID());
$comment_editor->applyEdit($xaction, $xaction->getComment());
} else {
$xaction->save();
}
}
foreach ($xactions as $xaction) {
$this->applyExternalEffects($object, $xaction);
}
$this->applyFinalEffects($object, $xactions);
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
$object->saveTransaction();
$this->loadHandles($xactions);
$mail = null;
if ($this->supportsMail()) {
$mail = $this->sendMail($object, $xactions);
}
if ($this->supportsSearch()) {
id(new PhabricatorSearchIndexer())
->indexDocumentByPHID($object->getPHID());
}
if ($this->supportsFeed()) {
$mailed = array();
if ($mail) {
$mailed = $mail->buildRecipientList();
}
$this->publishFeedStory(
$object,
$xactions,
$mailed);
}
$this->didApplyTransactions($xactions);
return $xactions;
}
protected function didApplyTransactions(array $xactions) {
// Hook for subclasses.
return;
}
/**
* Determine if the editor should hold a read lock on the object while
* applying a transaction.
*
* If the editor does not hold a lock, two editors may read an object at the
* same time, then apply their changes without any synchronization. For most
* transactions, this does not matter much. However, it is important for some
* transactions. For example, if an object has a transaction count on it, both
* editors may read the object with `count = 23`, then independently update it
* and save the object with `count = 24` twice. This will produce the wrong
* state: the object really has 25 transactions, but the count is only 24.
*
* Generally, transactions fall into one of four buckets:
*
* - Append operations: Actions like adding a comment to an object purely
* add information to its state, and do not depend on the current object
* state in any way. These transactions never need to hold locks.
* - Overwrite operations: Actions like changing the title or description
* of an object replace the current value with a new value, so the end
* state is consistent without a lock. We currently do not lock these
* transactions, although we may in the future.
* - Edge operations: Edge and subscription operations have internal
* synchronization which limits the damage race conditions can cause.
* We do not currently lock these transactions, although we may in the
* future.
* - Update operations: Actions like incrementing a count on an object.
* These operations generally should use locks, unless it is not
* important that the state remain consistent in the presence of races.
*
* @param PhabricatorLiskDAO Object being updated.
* @param PhabricatorApplicationTransaction Transaction being applied.
* @return bool True to synchronize the edit with a lock.
*/
protected function shouldReadLock(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return false;
}
private function loadHandles(array $xactions) {
$phids = array();
foreach ($xactions as $key => $xaction) {
$phids[$key] = $xaction->getRequiredHandlePHIDs();
}
$handles = array();
$merged = array_mergev($phids);
if ($merged) {
$handles = id(new PhabricatorObjectHandleData($merged))
->setViewer($this->requireActor())
->loadHandles();
}
foreach ($xactions as $key => $xaction) {
$xaction->setHandles(array_select_keys($handles, $phids[$key]));
}
}
private function loadSubscribers(PhabricatorLiskDAO $object) {
if ($object->getPHID() &&
($object instanceof PhabricatorSubscribableInterface)) {
$subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
$this->subscribers = array_fuse($subs);
} else {
$this->subscribers = array();
}
}
private function validateEditParameters(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$this->getContentSource()) {
throw new Exception(
"Call setContentSource() before applyTransactions()!");
}
// Do a bunch of sanity checks that the incoming transactions are fresh.
// They should be unsaved and have only "transactionType" and "newValue"
// set.
$types = array_fill_keys($this->getTransactionTypes(), true);
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
foreach ($xactions as $xaction) {
if ($xaction->getPHID() || $xaction->getID()) {
throw new Exception(
"You can not apply transactions which already have IDs/PHIDs!");
}
if ($xaction->getObjectPHID()) {
throw new Exception(
"You can not apply transactions which already have objectPHIDs!");
}
if ($xaction->getAuthorPHID()) {
throw new Exception(
"You can not apply transactions which already have authorPHIDs!");
}
if ($xaction->getCommentPHID()) {
throw new Exception(
"You can not apply transactions which already have commentPHIDs!");
}
if ($xaction->getCommentVersion() !== 0) {
throw new Exception(
"You can not apply transactions which already have commentVersions!");
}
if ($xaction->getOldValue() !== null) {
throw new Exception(
"You can not apply transactions which already have oldValue!");
}
$type = $xaction->getTransactionType();
if (empty($types[$type])) {
throw new Exception("Transaction has unknown type '{$type}'.");
}
}
// The actor must have permission to view and edit the object.
$actor = $this->requireActor();
PhabricatorPolicyFilter::requireCapability(
$actor,
$xaction,
PhabricatorPolicyCapability::CAN_VIEW);
PhabricatorPolicyFilter::requireCapability(
$actor,
$xaction,
PhabricatorPolicyCapability::CAN_EDIT);
}
private function buildMentionTransaction(
PhabricatorLiskDAO $object,
array $xactions) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
return null;
}
$texts = array();
foreach ($xactions as $xaction) {
$texts[] = $this->getMentionableTextsFromTransaction($xaction);
}
$texts = array_mergev($texts);
$phids = PhabricatorMarkupEngine::extractPHIDsFromMentions($texts);
$this->mentionedPHIDs = $phids;
if ($object->getPHID()) {
// Don't try to subscribe already-subscribed mentions: we want to generate
// a dialog about an action having no effect if the user explicitly adds
// existing CCs, but not if they merely mention existing subscribers.
$phids = array_diff($phids, $this->subscribers);
}
foreach ($phids as $key => $phid) {
if ($object->isAutomaticallySubscribed($phid)) {
unset($phids[$key]);
}
}
$phids = array_values($phids);
if (!$phids) {
return null;
}
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => $phids));
return $xaction;
}
protected function getMentionableTextsFromTransaction(
PhabricatorApplicationTransaction $transaction) {
$texts = array();
if ($transaction->getComment()) {
$texts[] = $transaction->getComment()->getContent();
}
return $texts;
}
protected function mergeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$type = $u->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->mergePHIDOrEdgeTransactions($u, $v);
case PhabricatorTransactions::TYPE_EDGE:
$u_type = $u->getMetadataValue('edge:type');
$v_type = $v->getMetadataValue('edge:type');
if ($u_type == $v_type) {
return $this->mergePHIDOrEdgeTransactions($u, $v);
}
return null;
}
// By default, do not merge the transactions.
return null;
}
/**
* Attempt to combine similar transactions into a smaller number of total
* transactions. For example, two transactions which edit the title of an
* object can be merged into a single edit.
*/
private function combineTransactions(array $xactions) {
$stray_comments = array();
$result = array();
$types = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction->getTransactionType();
if (isset($types[$type])) {
foreach ($types[$type] as $other_key) {
$merged = $this->mergeTransactions($result[$other_key], $xaction);
if ($merged) {
$result[$other_key] = $merged;
if ($xaction->getComment() &&
($xaction->getComment() !== $merged->getComment())) {
$stray_comments[] = $xaction->getComment();
}
if ($result[$other_key]->getComment() &&
($result[$other_key]->getComment() !== $merged->getComment())) {
$stray_comments[] = $result[$other_key]->getComment();
}
// Move on to the next transaction.
continue 2;
}
}
}
$result[$key] = $xaction;
$types[$type][] = $key;
}
// If we merged any comments away, restore them.
foreach ($stray_comments as $comment) {
$xaction = newv(get_class(head($result)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$xaction->setComment($comment);
$result[] = $xaction;
}
return array_values($result);
}
protected function mergePHIDOrEdgeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$result = $u->getNewValue();
foreach ($v->getNewValue() as $key => $value) {
$result[$key] = array_merge($value, idx($result, $key, array()));
}
$u->setNewValue($result);
return $u;
}
protected function getPHIDTransactionNewValue(
PhabricatorApplicationTransaction $xaction) {
$old = array_fuse($xaction->getOldValue());
$new = $xaction->getNewValue();
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
if ($new_set !== null) {
$new_set = array_fuse($new_set);
}
unset($new['=']);
if ($new) {
throw new Exception(
"Invalid 'new' value for PHID transaction. Value should contain only ".
"keys '+' (add PHIDs), '-' (remove PHIDs) and '=' (set PHIDS).");
}
$result = array();
foreach ($old as $phid) {
if ($new_set !== null && empty($new_set[$phid])) {
continue;
}
$result[$phid] = $phid;
}
if ($new_set !== null) {
foreach ($new_set as $phid) {
$result[$phid] = $phid;
}
}
foreach ($new_add as $phid) {
$result[$phid] = $phid;
}
foreach ($new_rem as $phid) {
unset($result[$phid]);
}
return array_values($result);
}
protected function getEdgeTransactionNewValue(
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
unset($new['=']);
if ($new) {
throw new Exception(
"Invalid 'new' value for Edge transaction. Value should contain only ".
"keys '+' (add edges), '-' (remove edges) and '=' (set edges).");
}
$old = $xaction->getOldValue();
$lists = array($new_set, $new_add, $new_rem);
foreach ($lists as $list) {
$this->checkEdgeList($list);
}
$result = array();
foreach ($old as $dst_phid => $edge) {
if ($new_set !== null && empty($new_set[$dst_phid])) {
continue;
}
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge);
}
if ($new_set !== null) {
foreach ($new_set as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge);
}
}
foreach ($new_add as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge);
}
foreach ($new_rem as $dst_phid => $edge) {
unset($result[$dst_phid]);
}
return $result;
}
private function checkEdgeList($list) {
if (!$list) {
return;
}
foreach ($list as $key => $item) {
if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
throw new Exception(
"Edge transactions must have destination PHIDs as in edge ".
"lists (found key '{$key}').");
}
if (!is_array($item) && $item !== $key) {
throw new Exception(
"Edge transactions must have PHIDs or edge specs as values ".
"(found value '{$item}').");
}
}
}
protected function normalizeEdgeTransactionValue(
PhabricatorApplicationTransaction $xaction,
$edge) {
if (!is_array($edge)) {
$edge = array(
'dst' => $edge,
);
}
$edge_type = $xaction->getMetadataValue('edge:type');
if (empty($edge['type'])) {
$edge['type'] = $edge_type;
} else {
if ($edge['type'] != $edge_type) {
$this_type = $edge['type'];
throw new Exception(
"Edge transaction includes edge of type '{$this_type}', but ".
"transaction is of type '{$edge_type}'. Each edge transaction must ".
"alter edges of only one type.");
}
}
if (!isset($edge['data'])) {
$edge['data'] = null;
}
return $edge;
}
protected function sortTransactions(array $xactions) {
$head = array();
$tail = array();
// Move bare comments to the end, so the actions precede them.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PhabricatorTransactions::TYPE_COMMENT) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function filterTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
$no_effect = array();
$has_comment = false;
$any_effect = false;
foreach ($xactions as $key => $xaction) {
if ($this->transactionHasEffect($object, $xaction)) {
if ($xaction->getTransactionType() != $type_comment) {
$any_effect = true;
}
} else {
$no_effect[$key] = $xaction;
}
if ($xaction->hasComment()) {
$has_comment = true;
}
}
if (!$no_effect) {
return $xactions;
}
if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
throw new PhabricatorApplicationTransactionNoEffectException(
$no_effect,
$any_effect,
$has_comment);
}
if (!$any_effect && !$has_comment) {
// If we only have empty comment transactions, just drop them all.
return array();
}
foreach ($no_effect as $key => $xaction) {
if ($xaction->getComment()) {
$xaction->setTransactionType($type_comment);
$xaction->setOldValue(null);
$xaction->setNewValue(null);
} else {
unset($xactions[$key]);
}
}
return $xactions;
}
/* -( Implicit CCs )------------------------------------------------------- */
/**
* When a user interacts with an object, we might want to add them to CC.
*/
final public function applyImplicitCC(
PhabricatorLiskDAO $object,
array $xactions) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
// If the object isn't subscribable, we can't CC them.
return $xactions;
}
$actor_phid = $this->requireActor()->getPHID();
if ($object->isAutomaticallySubscribed($actor_phid)) {
// If they're auto-subscribed, don't CC them.
return $xactions;
}
$should_cc = false;
foreach ($xactions as $xaction) {
if ($this->shouldImplyCC($object, $xaction)) {
$should_cc = true;
break;
}
}
if (!$should_cc) {
// Only some types of actions imply a CC (like adding a comment).
return $xactions;
}
if ($object->getPHID()) {
if (isset($this->subscribers[$actor_phid])) {
// If the user is already subscribed, don't implicitly CC them.
return $xactions;
}
$unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER);
$unsub = array_fuse($unsub);
if (isset($unsub[$actor_phid])) {
// If the user has previously unsubscribed from this object explicitly,
// don't implicitly CC them.
return $xactions;
}
}
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => array($actor_phid)));
array_unshift($xactions, $xaction);
return $xactions;
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return true;
default:
return false;
}
}
/* -( Sending Mail )------------------------------------------------------- */
/**
* @task mail
*/
protected function supportsMail() {
return false;
}
/**
* @task mail
*/
protected function sendMail(
PhabricatorLiskDAO $object,
array $xactions) {
$email_to = array_unique($this->getMailTo($object));
$email_cc = array_unique($this->getMailCC($object));
$phids = array_merge($email_to, $email_cc);
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($this->requireActor())
->loadHandles();
$template = $this->buildMailTemplate($object);
$body = $this->buildMailBody($object, $xactions);
$mail_tags = $this->getMailTags($object, $xactions);
$action = $this->getStrongestAction($object, $xactions)->getActionName();
$template
->setFrom($this->requireActor()->getPHID())
->setSubjectPrefix($this->getMailSubjectPrefix())
->setVarySubjectPrefix('['.$action.']')
->setThreadID($object->getPHID(), $this->getIsNewObject())
->setRelatedPHID($object->getPHID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setMailTags($mail_tags)
->setIsBulk(true)
->setBody($body->render());
if ($this->getParentMessageID()) {
$template->setParentMessageID($this->getParentMessageID());
}
$mails = $this
->buildReplyHandler($object)
->multiplexMail(
$template,
array_select_keys($handles, $email_to),
array_select_keys($handles, $email_cc));
foreach ($mails as $mail) {
$mail->saveAndSend();
}
$template->addTos($email_to);
$template->addCCs($email_cc);
return $template;
}
/**
* @task mail
*/
protected function getStrongestAction(
PhabricatorLiskDAO $object,
array $xactions) {
return last(msort($xactions, 'getActionStrength'));
}
/**
* @task mail
*/
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
throw new Exception("Capability not supported.");
}
/**
* @task mail
*/
protected function getMailSubjectPrefix() {
throw new Exception("Capability not supported.");
}
/**
* @task mail
*/
protected function getMailTags(
PhabricatorLiskDAO $object,
array $xactions) {
$tags = array();
foreach ($xactions as $xaction) {
$tags[] = $xaction->getMailTags();
}
return array_mergev($tags);
}
/**
* @task mail
*/
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
throw new Exception("Capability not supported.");
}
/**
* @task mail
*/
protected function getMailTo(PhabricatorLiskDAO $object) {
throw new Exception("Capability not supported.");
}
/**
* @task mail
*/
protected function getMailCC(PhabricatorLiskDAO $object) {
if ($object instanceof PhabricatorSubscribableInterface) {
return $this->subscribers;
}
throw new Exception("Capability not supported.");
}
/**
* @task mail
*/
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$headers = array();
$comments = array();
foreach ($xactions as $xaction) {
$headers[] = id(clone $xaction)->setRenderingTarget('text')->getTitle();
$comment = $xaction->getComment();
if ($comment && strlen($comment->getContent())) {
$comments[] = $comment->getContent();
}
}
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection(implode("\n", $headers));
foreach ($comments as $comment) {
$body->addRawSection($comment);
}
return $body;
}
/* -( Publishing Feed Stories )-------------------------------------------- */
/**
* @task feed
*/
protected function supportsFeed() {
return false;
}
/**
* @task feed
*/
protected function getFeedStoryType() {
return 'PhabricatorApplicationTransactionFeedStory';
}
/**
* @task feed
*/
protected function getFeedRelatedPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
return array(
$object->getPHID(),
$this->requireActor()->getPHID(),
);
}
/**
* @task feed
*/
protected function getFeedNotifyPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
return array_unique(array_merge(
$this->getMailTo($object),
$this->getMailCC($object)));
}
/**
* @task feed
*/
protected function getFeedStoryData(
PhabricatorLiskDAO $object,
array $xactions) {
$xactions = msort($xactions, 'getActionStrength');
$xactions = array_reverse($xactions);
return array(
'objectPHID' => $object->getPHID(),
'transactionPHIDs' => mpull($xactions, 'getPHID'),
);
}
/**
* @task feed
*/
protected function publishFeedStory(
PhabricatorLiskDAO $object,
array $xactions,
array $mailed_phids) {
$related_phids = $this->getFeedRelatedPHIDs($object, $xactions);
$subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions);
$story_type = $this->getFeedStoryType();
$story_data = $this->getFeedStoryData($object, $xactions);
id(new PhabricatorFeedStoryPublisher())
->setStoryType($story_type)
->setStoryData($story_data)
->setStoryTime(time())
->setStoryAuthorPHID($this->requireActor()->getPHID())
->setRelatedPHIDs($related_phids)
->setPrimaryObjectPHID($object->getPHID())
->setSubscribedPHIDs($subscribed_phids)
->setMailRecipientPHIDs($mailed_phids)
->publish();
}
/* -( Search Index )------------------------------------------------------- */
/**
* @task search
*/
protected function supportsSearch() {
return false;
}
/* -( Custom Fields )------------------------------------------------------- */
/**
* @task customfield
*/
private function getCustomFieldForTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$field_key = $xaction->getMetadataValue('customfield:key');
if (!$field_key) {
throw new Exception(
"Custom field transaction has no 'customfield:key'!");
}
$field = PhabricatorCustomField::getObjectField(
$object,
PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
$field_key);
if (!$field) {
throw new Exception(
"Custom field transaction has invalid 'customfield:key'; field ".
"'{$field_key}' is disabled or does not exist.");
}
if (!$field->shouldAppearInApplicationTransactions()) {
throw new Exception(
"Custom field transaction '{$field_key}' does not implement ".
"integration for ApplicationTransactions.");
}
return $field;
}
}
diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
index 0874b357d0..96b4138dfb 100644
--- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
@@ -1,420 +1,450 @@
<?php
abstract class PhabricatorApplicationTransaction
extends PhabricatorLiskDAO
implements PhabricatorPolicyInterface {
const TARGET_TEXT = 'text';
const TARGET_HTML = 'html';
protected $phid;
protected $objectPHID;
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $commentPHID;
protected $commentVersion = 0;
protected $transactionType;
protected $oldValue;
protected $newValue;
protected $metadata = array();
protected $contentSource;
private $comment;
private $commentNotLoaded;
private $handles;
private $renderingTarget = self::TARGET_HTML;
private $transactionGroup = array();
abstract public function getApplicationTransactionType();
abstract public function getApplicationTransactionCommentObject();
abstract public function getApplicationObjectTypeName();
public function getApplicationTransactionViewObject() {
return new PhabricatorApplicationTransactionView();
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function generatePHID() {
$type = PhabricatorPHIDConstants::PHID_TYPE_XACT;
$subtype = $this->getApplicationTransactionType();
return PhabricatorPHID::generateNewPHID($type, $subtype);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'metadata' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function hasComment() {
return $this->getComment() && strlen($this->getComment()->getContent());
}
public function getComment() {
if ($this->commentNotLoaded) {
throw new Exception("Comment for this transaction was not loaded.");
}
return $this->comment;
}
public function attachComment(
PhabricatorApplicationTransactionComment $comment) {
$this->comment = $comment;
$this->commentNotLoaded = false;
return $this;
}
public function setCommentNotLoaded($not_loaded) {
$this->commentNotLoaded = $not_loaded;
return $this;
}
/* -( Rendering )---------------------------------------------------------- */
public function setRenderingTarget($rendering_target) {
$this->renderingTarget = $rendering_target;
return $this;
}
public function getRenderingTarget() {
return $this->renderingTarget;
}
public function getRequiredHandlePHIDs() {
$phids = array();
$old = $this->getOldValue();
$new = $this->getNewValue();
$phids[] = array($this->getAuthorPHID());
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$phids[] = $old;
$phids[] = $new;
break;
case PhabricatorTransactions::TYPE_EDGE:
$phids[] = ipull($old, 'dst');
$phids[] = ipull($new, 'dst');
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) {
$phids[] = array($old);
}
if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) {
$phids[] = array($new);
}
break;
}
return array_mergev($phids);
}
public function setHandles(array $handles) {
$this->handles = $handles;
return $this;
}
public function getHandle($phid) {
if (empty($this->handles[$phid])) {
throw new Exception(
"Transaction requires a handle ('{$phid}') it did not load.");
}
return $this->handles[$phid];
}
public function getHandleIfExists($phid) {
return idx($this->handles, $phid);
}
public function getHandles() {
if ($this->handles === null) {
throw new Exception(
'Transaction requires handles and it did not load them.'
);
}
return $this->handles;
}
public function renderHandleLink($phid) {
if ($this->renderingTarget == self::TARGET_HTML) {
return $this->getHandle($phid)->renderLink();
} else {
return hsprintf('%s', $this->getHandle($phid)->getName());
}
}
public function renderHandleList(array $phids) {
$links = array();
foreach ($phids as $phid) {
$links[] = $this->renderHandleLink($phid);
}
return phutil_implode_html(', ', $links);
}
public function getIcon() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return 'comment';
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return 'message';
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return 'lock';
case PhabricatorTransactions::TYPE_EDGE:
return 'link';
}
return null;
}
public function getColor() {
return null;
}
public function shouldHide() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
break;
}
return false;
}
public function getNoEffectDescription() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht('You can not post an empty comment.');
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'This %s already has that view policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'This %s already has that edit policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht(
'All users are already subscribed to this %s.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_EDGE:
return pht('Edges already exist; transaction has no effect.');
}
return pht('Transaction has no effect.');
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht(
'%s added a comment.',
$this->renderHandleLink($author_phid));
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'%s changed the visibility of this %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName(),
PhabricatorPolicy::newFromPolicyAndHandle(
$old,
$this->getHandleIfExists($old))->renderDescription(),
PhabricatorPolicy::newFromPolicyAndHandle(
$new,
$this->getHandleIfExists($new))->renderDescription());
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'%s changed the edit policy of this %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName(),
PhabricatorPolicy::newFromPolicyAndHandle(
$old,
$this->getHandleIfExists($old))->renderDescription(),
PhabricatorPolicy::newFromPolicyAndHandle(
$new,
$this->getHandleIfExists($new))->renderDescription());
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s edited subscriber(s), added %d: %s; removed %d: %s.',
$this->renderHandleLink($author_phid),
count($add),
$this->renderHandleList($add),
count($rem),
$this->renderHandleList($rem));
} else if ($add) {
return pht(
'%s added %d subscriber(s): %s.',
$this->renderHandleLink($author_phid),
count($add),
$this->renderHandleList($add));
} else {
return pht(
'%s removed %d subscriber(s): %s.',
$this->renderHandleLink($author_phid),
count($rem),
$this->renderHandleList($rem));
}
break;
case PhabricatorTransactions::TYPE_EDGE:
+ $new = ipull($new, 'dst');
+ $old = ipull($old, 'dst');
+ $add = array_diff($new, $old);
+ $rem = array_diff($old, $new);
$type = $this->getMetadata('edge:type');
- return pht(
- '%s edited edges of type %s.',
- $this->renderHandleLink($author_phid),
- $type);
+ $type = head($type);
+
+ if ($add && $rem) {
+ $string = PhabricatorEdgeConfig::getEditStringForEdgeType($type);
+ return pht(
+ $string,
+ $this->renderHandleLink($author_phid),
+ count($add),
+ $this->renderHandleList($add),
+ count($rem),
+ $this->renderHandleList($rem));
+ } else if ($add) {
+ $string = PhabricatorEdgeConfig::getAddStringForEdgeType($type);
+ return pht(
+ $string,
+ $this->renderHandleLink($author_phid),
+ count($add),
+ $this->renderHandleList($add));
+ } else {
+ $string = PhabricatorEdgeConfig::getRemoveStringForEdgeType($type);
+ return pht(
+ $string,
+ $this->renderHandleLink($author_phid),
+ count($rem),
+ $this->renderHandleList($rem));
+ }
+
default:
return pht(
'%s edited this %s.',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName());
}
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht(
'%s added a comment to %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'%s changed the visibility for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'%s changed the edit policy for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht(
'%s updated subscribers of %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_EDGE:
+ $type = $this->getMetadata('edge:type');
+ $type = head($type);
+ $string = PhabricatorEdgeConfig::getFeedStringForEdgeType($type);
return pht(
- '%s updated edges of %s.',
+ $string,
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
return $this->getTitle();
}
public function getActionStrength() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return 0.5;
}
return 1.0;
}
public function getActionName() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht('Commented On');
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht('Changed Policy');
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht('Changed Subscribers');
default:
return pht('Updated');
}
}
public function getMailTags() {
return array();
}
public function hasChangeDetails() {
return false;
}
public function renderChangeDetails(PhabricatorUser $viewer) {
return null;
}
public function attachTransactionGroup(array $group) {
assert_instances_of($group, 'PhabricatorApplicationTransaction');
$this->transactionGroup = $group;
return $this;
}
public function getTransactionGroup() {
return $this->transactionGroup;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
}
diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
index d57fdaf3d5..5061735a2d 100644
--- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
+++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
@@ -1,177 +1,456 @@
<?php
final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
const TABLE_NAME_EDGE = 'edge';
const TABLE_NAME_EDGEDATA = 'edgedata';
const TYPE_TASK_HAS_COMMIT = 1;
const TYPE_COMMIT_HAS_TASK = 2;
const TYPE_TASK_DEPENDS_ON_TASK = 3;
const TYPE_TASK_DEPENDED_ON_BY_TASK = 4;
const TYPE_DREV_DEPENDS_ON_DREV = 5;
const TYPE_DREV_DEPENDED_ON_BY_DREV = 6;
const TYPE_BLOG_HAS_POST = 7;
const TYPE_POST_HAS_BLOG = 8;
const TYPE_BLOG_HAS_BLOGGER = 9;
const TYPE_BLOGGER_HAS_BLOG = 10;
const TYPE_TASK_HAS_RELATED_DREV = 11;
const TYPE_DREV_HAS_RELATED_TASK = 12;
const TYPE_PROJ_MEMBER = 13;
const TYPE_MEMBER_OF_PROJ = 14;
const TYPE_COMMIT_HAS_PROJECT = 15;
const TYPE_PROJECT_HAS_COMMIT = 16;
const TYPE_QUESTION_HAS_VOTING_USER = 17;
const TYPE_VOTING_USER_HAS_QUESTION = 18;
const TYPE_ANSWER_HAS_VOTING_USER = 19;
const TYPE_VOTING_USER_HAS_ANSWER = 20;
const TYPE_OBJECT_HAS_SUBSCRIBER = 21;
const TYPE_SUBSCRIBED_TO_OBJECT = 22;
const TYPE_OBJECT_HAS_UNSUBSCRIBER = 23;
const TYPE_UNSUBSCRIBED_FROM_OBJECT = 24;
const TYPE_OBJECT_HAS_FILE = 25;
const TYPE_FILE_HAS_OBJECT = 26;
const TYPE_ACCOUNT_HAS_MEMBER = 27;
const TYPE_MEMBER_HAS_ACCOUNT = 28;
const TYPE_PURCAHSE_HAS_CHARGE = 29;
const TYPE_CHARGE_HAS_PURCHASE = 30;
const TYPE_DREV_HAS_COMMIT = 31;
const TYPE_COMMIT_HAS_DREV = 32;
const TYPE_OBJECT_HAS_CONTRIBUTOR = 33;
const TYPE_CONTRIBUTED_TO_OBJECT = 34;
const TYPE_DREV_HAS_REVIEWER = 35;
const TYPE_REVIEWER_FOR_DREV = 36;
+ const TYPE_MOCK_HAS_TASK = 37;
+ const TYPE_TASK_HAS_MOCK = 38;
+
const TYPE_TEST_NO_CYCLE = 9000;
const TYPE_PHOB_HAS_ASANATASK = 80001;
const TYPE_ASANATASK_HAS_PHOB = 80000;
const TYPE_PHOB_HAS_ASANASUBTASK = 80003;
const TYPE_ASANASUBTASK_HAS_PHOB = 80002;
public static function getInverse($edge_type) {
static $map = array(
self::TYPE_TASK_HAS_COMMIT => self::TYPE_COMMIT_HAS_TASK,
self::TYPE_COMMIT_HAS_TASK => self::TYPE_TASK_HAS_COMMIT,
self::TYPE_TASK_DEPENDS_ON_TASK => self::TYPE_TASK_DEPENDED_ON_BY_TASK,
self::TYPE_TASK_DEPENDED_ON_BY_TASK => self::TYPE_TASK_DEPENDS_ON_TASK,
self::TYPE_DREV_DEPENDS_ON_DREV => self::TYPE_DREV_DEPENDED_ON_BY_DREV,
self::TYPE_DREV_DEPENDED_ON_BY_DREV => self::TYPE_DREV_DEPENDS_ON_DREV,
self::TYPE_BLOG_HAS_POST => self::TYPE_POST_HAS_BLOG,
self::TYPE_POST_HAS_BLOG => self::TYPE_BLOG_HAS_POST,
self::TYPE_BLOG_HAS_BLOGGER => self::TYPE_BLOGGER_HAS_BLOG,
self::TYPE_BLOGGER_HAS_BLOG => self::TYPE_BLOG_HAS_BLOGGER,
self::TYPE_TASK_HAS_RELATED_DREV => self::TYPE_DREV_HAS_RELATED_TASK,
self::TYPE_DREV_HAS_RELATED_TASK => self::TYPE_TASK_HAS_RELATED_DREV,
self::TYPE_PROJ_MEMBER => self::TYPE_MEMBER_OF_PROJ,
self::TYPE_MEMBER_OF_PROJ => self::TYPE_PROJ_MEMBER,
self::TYPE_COMMIT_HAS_PROJECT => self::TYPE_PROJECT_HAS_COMMIT,
self::TYPE_PROJECT_HAS_COMMIT => self::TYPE_COMMIT_HAS_PROJECT,
self::TYPE_QUESTION_HAS_VOTING_USER =>
self::TYPE_VOTING_USER_HAS_QUESTION,
self::TYPE_VOTING_USER_HAS_QUESTION =>
self::TYPE_QUESTION_HAS_VOTING_USER,
self::TYPE_ANSWER_HAS_VOTING_USER => self::TYPE_VOTING_USER_HAS_ANSWER,
self::TYPE_VOTING_USER_HAS_ANSWER => self::TYPE_ANSWER_HAS_VOTING_USER,
self::TYPE_OBJECT_HAS_SUBSCRIBER => self::TYPE_SUBSCRIBED_TO_OBJECT,
self::TYPE_SUBSCRIBED_TO_OBJECT => self::TYPE_OBJECT_HAS_SUBSCRIBER,
self::TYPE_OBJECT_HAS_UNSUBSCRIBER => self::TYPE_UNSUBSCRIBED_FROM_OBJECT,
self::TYPE_UNSUBSCRIBED_FROM_OBJECT => self::TYPE_OBJECT_HAS_UNSUBSCRIBER,
self::TYPE_OBJECT_HAS_FILE => self::TYPE_FILE_HAS_OBJECT,
self::TYPE_FILE_HAS_OBJECT => self::TYPE_OBJECT_HAS_FILE,
self::TYPE_ACCOUNT_HAS_MEMBER => self::TYPE_MEMBER_HAS_ACCOUNT,
self::TYPE_MEMBER_HAS_ACCOUNT => self::TYPE_ACCOUNT_HAS_MEMBER,
self::TYPE_DREV_HAS_COMMIT => self::TYPE_COMMIT_HAS_DREV,
self::TYPE_COMMIT_HAS_DREV => self::TYPE_DREV_HAS_COMMIT,
self::TYPE_OBJECT_HAS_CONTRIBUTOR => self::TYPE_SUBSCRIBED_TO_OBJECT,
self::TYPE_CONTRIBUTED_TO_OBJECT => self::TYPE_OBJECT_HAS_CONTRIBUTOR,
+ self::TYPE_TASK_HAS_MOCK => self::TYPE_MOCK_HAS_TASK,
+ self::TYPE_MOCK_HAS_TASK => self::TYPE_TASK_HAS_MOCK,
+
self::TYPE_PHOB_HAS_ASANATASK => self::TYPE_ASANATASK_HAS_PHOB,
self::TYPE_ASANATASK_HAS_PHOB => self::TYPE_PHOB_HAS_ASANATASK,
self::TYPE_PHOB_HAS_ASANASUBTASK => self::TYPE_ASANASUBTASK_HAS_PHOB,
self::TYPE_ASANASUBTASK_HAS_PHOB => self::TYPE_PHOB_HAS_ASANASUBTASK,
self::TYPE_DREV_HAS_REVIEWER => self::TYPE_REVIEWER_FOR_DREV,
self::TYPE_REVIEWER_FOR_DREV => self::TYPE_DREV_HAS_REVIEWER,
);
return idx($map, $edge_type);
}
public static function shouldPreventCycles($edge_type) {
static $map = array(
self::TYPE_TEST_NO_CYCLE => true,
self::TYPE_TASK_DEPENDS_ON_TASK => true,
self::TYPE_DREV_DEPENDS_ON_DREV => true,
);
return isset($map[$edge_type]);
}
public static function establishConnection($phid_type, $conn_type) {
static $class_map = array(
PhabricatorPHIDConstants::PHID_TYPE_TASK => 'ManiphestTask',
PhabricatorPHIDConstants::PHID_TYPE_CMIT => 'PhabricatorRepository',
PhabricatorPHIDConstants::PHID_TYPE_DREV => 'DifferentialRevision',
PhabricatorPHIDConstants::PHID_TYPE_FILE => 'PhabricatorFile',
PhabricatorPHIDConstants::PHID_TYPE_USER => 'PhabricatorUser',
PhabricatorPHIDConstants::PHID_TYPE_PROJ => 'PhabricatorProject',
PhabricatorPHIDConstants::PHID_TYPE_MLST =>
'PhabricatorMetaMTAMailingList',
PhabricatorPHIDConstants::PHID_TYPE_TOBJ => 'HarbormasterObject',
PhabricatorPHIDConstants::PHID_TYPE_BLOG => 'PhameBlog',
PhabricatorPHIDConstants::PHID_TYPE_POST => 'PhamePost',
PhabricatorPHIDConstants::PHID_TYPE_QUES => 'PonderQuestion',
PhabricatorPHIDConstants::PHID_TYPE_ANSW => 'PonderAnswer',
PhabricatorPHIDConstants::PHID_TYPE_MOCK => 'PholioMock',
PhabricatorPHIDConstants::PHID_TYPE_MCRO => 'PhabricatorFileImageMacro',
PhabricatorPHIDConstants::PHID_TYPE_CONP => 'ConpherenceThread',
PhabricatorPHIDConstants::PHID_TYPE_WIKI => 'PhrictionDocument',
PhabricatorPHIDConstants::PHID_TYPE_ACNT => 'PhortuneAccount',
PhabricatorPHIDConstants::PHID_TYPE_PRCH => 'PhortunePurchase',
PhabricatorPHIDConstants::PHID_TYPE_CHRG => 'PhortuneCharge',
PhabricatorPHIDConstants::PHID_TYPE_XOBJ => 'DoorkeeperExternalObject',
PhabricatorPHIDConstants::PHID_TYPE_LEGD => 'LegalpadDocument',
PhabricatorPHIDConstants::PHID_TYPE_POLL => 'PhabricatorSlowvotePoll',
);
$class = idx($class_map, $phid_type);
if (!$class) {
throw new Exception(
"Edges are not available for objects of type '{$phid_type}'!");
}
return newv($class, array())->establishConnection($conn_type);
}
+ public static function getEditStringForEdgeType($type) {
+ switch ($type) {
+ case self::TYPE_TASK_HAS_COMMIT:
+ case self::TYPE_PROJECT_HAS_COMMIT:
+ case self::TYPE_DREV_HAS_COMMIT:
+ return '%s edited commit(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_COMMIT_HAS_TASK:
+ case self::TYPE_TASK_DEPENDS_ON_TASK:
+ case self::TYPE_TASK_DEPENDED_ON_BY_TASK:
+ case self::TYPE_DREV_HAS_RELATED_TASK:
+ case self::TYPE_MOCK_HAS_TASK:
+ return '%s edited task(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_DREV_DEPENDS_ON_DREV:
+ case self::TYPE_DREV_DEPENDED_ON_BY_DREV:
+ case self::TYPE_TASK_HAS_RELATED_DREV:
+ case self::TYPE_COMMIT_HAS_DREV:
+ case self::TYPE_REVIEWER_FOR_DREV:
+ return '%s edited revision(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_BLOG_HAS_POST:
+ return '%s edited post(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_POST_HAS_BLOG:
+ case self::TYPE_BLOGGER_HAS_BLOG:
+ return '%s edited blog(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_BLOG_HAS_BLOGGER:
+ return '%s edited blogger(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_PROJ_MEMBER:
+ return '%s edited member(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_MEMBER_OF_PROJ:
+ case self::TYPE_COMMIT_HAS_PROJECT:
+ return '%s edited project(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_QUESTION_HAS_VOTING_USER:
+ case self::TYPE_ANSWER_HAS_VOTING_USER:
+ return '%s edited voting user(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_VOTING_USER_HAS_QUESTION:
+ return '%s edited question(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_VOTING_USER_HAS_ANSWER:
+ return '%s edited answer(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_OBJECT_HAS_SUBSCRIBER:
+ return '%s edited subscriber(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_SUBSCRIBED_TO_OBJECT:
+ case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
+ case self::TYPE_FILE_HAS_OBJECT:
+ case self::TYPE_CONTRIBUTED_TO_OBJECT:
+ return '%s edited object(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_OBJECT_HAS_UNSUBSCRIBER:
+ return '%s edited unsubcriber(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_OBJECT_HAS_FILE:
+ return '%s edited file(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_ACCOUNT_HAS_MEMBER:
+ return '%s edited member(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_MEMBER_HAS_ACCOUNT:
+ return '%s edited account(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_PURCAHSE_HAS_CHARGE:
+ return '%s edited charge(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_CHARGE_HAS_PURCHASE:
+ return '%s edited purchase(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_OBJECT_HAS_CONTRIBUTOR:
+ return '%s edited contributor(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_DREV_HAS_REVIEWER:
+ return '%s edited reviewer(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_TASK_HAS_MOCK:
+ return '%s edited mock(s), added %d: %s; removed %d: %s.';
+ case self::TYPE_SUBSCRIBED_TO_OBJECT:
+ case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
+ case self::TYPE_FILE_HAS_OBJECT:
+ case self::TYPE_CONTRIBUTED_TO_OBJECT:
+ default:
+ return '%s edited object(s), added %d: %s; removed %d: %s.';
+
+ }
+ }
+
+ public static function getAddStringForEdgeType($type) {
+ switch ($type) {
+ case self::TYPE_TASK_HAS_COMMIT:
+ case self::TYPE_PROJECT_HAS_COMMIT:
+ case self::TYPE_DREV_HAS_COMMIT:
+ return '%s added %d commit(s): %s.';
+ case self::TYPE_COMMIT_HAS_TASK:
+ case self::TYPE_TASK_DEPENDS_ON_TASK:
+ case self::TYPE_TASK_DEPENDED_ON_BY_TASK:
+ case self::TYPE_DREV_HAS_RELATED_TASK:
+ case self::TYPE_MOCK_HAS_TASK:
+ return '%s added %d task(s): %s.';
+ case self::TYPE_DREV_DEPENDS_ON_DREV:
+ case self::TYPE_DREV_DEPENDED_ON_BY_DREV:
+ case self::TYPE_TASK_HAS_RELATED_DREV:
+ case self::TYPE_COMMIT_HAS_DREV:
+ case self::TYPE_REVIEWER_FOR_DREV:
+ return '%s added %d revision(s): %s.';
+ case self::TYPE_BLOG_HAS_POST:
+ return '%s added %d post(s): %s.';
+ case self::TYPE_POST_HAS_BLOG:
+ case self::TYPE_BLOGGER_HAS_BLOG:
+ return '%s added %d blog(s): %s.';
+ case self::TYPE_BLOG_HAS_BLOGGER:
+ return '%s added %d blogger(s): %s.';
+ case self::TYPE_PROJ_MEMBER:
+ return '%s added %d member(s): %s.';
+ case self::TYPE_MEMBER_OF_PROJ:
+ case self::TYPE_COMMIT_HAS_PROJECT:
+ return '%s added %d project(s): %s.';
+ case self::TYPE_QUESTION_HAS_VOTING_USER:
+ case self::TYPE_ANSWER_HAS_VOTING_USER:
+ return '%s added %d voting user(s): %s.';
+ case self::TYPE_VOTING_USER_HAS_QUESTION:
+ return '%s added %d question(s): %s.';
+ case self::TYPE_VOTING_USER_HAS_ANSWER:
+ return '%s added %d answer(s): %s.';
+ case self::TYPE_OBJECT_HAS_SUBSCRIBER:
+ return '%s added %d subscriber(s): %s.';
+ case self::TYPE_OBJECT_HAS_UNSUBSCRIBER:
+ return '%s added %d unsubcriber(s): %s.';
+ case self::TYPE_OBJECT_HAS_FILE:
+ return '%s added %d file(s): %s.';
+ case self::TYPE_ACCOUNT_HAS_MEMBER:
+ return '%s added %d member(s): %s.';
+ case self::TYPE_MEMBER_HAS_ACCOUNT:
+ return '%s added %d account(s): %s.';
+ case self::TYPE_PURCAHSE_HAS_CHARGE:
+ return '%s added %d charge(s): %s.';
+ case self::TYPE_CHARGE_HAS_PURCHASE:
+ return '%s added %d purchase(s): %s.';
+ case self::TYPE_OBJECT_HAS_CONTRIBUTOR:
+ return '%s added %d contributor(s): %s.';
+ case self::TYPE_DREV_HAS_REVIEWER:
+ return '%s added %d reviewer(s): %s.';
+ case self::TYPE_TASK_HAS_MOCK:
+ return '%s added %d mock(s): %s.';
+ case self::TYPE_SUBSCRIBED_TO_OBJECT:
+ case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
+ case self::TYPE_FILE_HAS_OBJECT:
+ case self::TYPE_CONTRIBUTED_TO_OBJECT:
+ default:
+ return '%s added %d object(s): %s.';
+
+ }
+ }
+
+ public static function getRemoveStringForEdgeType($type) {
+ switch ($type) {
+ case self::TYPE_TASK_HAS_COMMIT:
+ case self::TYPE_PROJECT_HAS_COMMIT:
+ case self::TYPE_DREV_HAS_COMMIT:
+ return '%s removed %d commit(s): %s.';
+ case self::TYPE_COMMIT_HAS_TASK:
+ case self::TYPE_TASK_DEPENDS_ON_TASK:
+ case self::TYPE_TASK_DEPENDED_ON_BY_TASK:
+ case self::TYPE_DREV_HAS_RELATED_TASK:
+ case self::TYPE_MOCK_HAS_TASK:
+ return '%s removed %d task(s): %s.';
+ case self::TYPE_DREV_DEPENDS_ON_DREV:
+ case self::TYPE_DREV_DEPENDED_ON_BY_DREV:
+ case self::TYPE_TASK_HAS_RELATED_DREV:
+ case self::TYPE_COMMIT_HAS_DREV:
+ case self::TYPE_REVIEWER_FOR_DREV:
+ return '%s removed %d revision(s): %s.';
+ case self::TYPE_BLOG_HAS_POST:
+ return '%s removed %d post(s): %s.';
+ case self::TYPE_POST_HAS_BLOG:
+ case self::TYPE_BLOGGER_HAS_BLOG:
+ return '%s removed %d blog(s): %s.';
+ case self::TYPE_BLOG_HAS_BLOGGER:
+ return '%s removed %d blogger(s): %s.';
+ case self::TYPE_PROJ_MEMBER:
+ return '%s removed %d member(s): %s.';
+ case self::TYPE_MEMBER_OF_PROJ:
+ case self::TYPE_COMMIT_HAS_PROJECT:
+ return '%s removed %d project(s): %s.';
+ case self::TYPE_QUESTION_HAS_VOTING_USER:
+ case self::TYPE_ANSWER_HAS_VOTING_USER:
+ return '%s removed %d voting user(s): %s.';
+ case self::TYPE_VOTING_USER_HAS_QUESTION:
+ return '%s removed %d question(s): %s.';
+ case self::TYPE_VOTING_USER_HAS_ANSWER:
+ return '%s removed %d answer(s): %s.';
+ case self::TYPE_OBJECT_HAS_SUBSCRIBER:
+ return '%s removed %d subscriber(s): %s.';
+ case self::TYPE_OBJECT_HAS_UNSUBSCRIBER:
+ return '%s removed %d unsubcriber(s): %s.';
+ case self::TYPE_OBJECT_HAS_FILE:
+ return '%s removed %d file(s): %s.';
+ case self::TYPE_ACCOUNT_HAS_MEMBER:
+ return '%s removed %d member(s): %s.';
+ case self::TYPE_MEMBER_HAS_ACCOUNT:
+ return '%s removed %d account(s): %s.';
+ case self::TYPE_PURCAHSE_HAS_CHARGE:
+ return '%s removed %d charge(s): %s.';
+ case self::TYPE_CHARGE_HAS_PURCHASE:
+ return '%s removed %d purchase(s): %s.';
+ case self::TYPE_OBJECT_HAS_CONTRIBUTOR:
+ return '%s removed %d contributor(s): %s.';
+ case self::TYPE_DREV_HAS_REVIEWER:
+ return '%s removed %d reviewer(s): %s.';
+ case self::TYPE_TASK_HAS_MOCK:
+ return '%s removed %d mock(s): %s.';
+ case self::TYPE_SUBSCRIBED_TO_OBJECT:
+ case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
+ case self::TYPE_FILE_HAS_OBJECT:
+ case self::TYPE_CONTRIBUTED_TO_OBJECT:
+ default:
+ return '%s removed %d object(s): %s.';
+
+ }
+ }
+
+ public static function getFeedStringForEdgeType($type) {
+ switch ($type) {
+ case self::TYPE_TASK_HAS_COMMIT:
+ case self::TYPE_PROJECT_HAS_COMMIT:
+ case self::TYPE_DREV_HAS_COMMIT:
+ return '%s updated commits of %s.';
+ case self::TYPE_COMMIT_HAS_TASK:
+ case self::TYPE_TASK_DEPENDS_ON_TASK:
+ case self::TYPE_TASK_DEPENDED_ON_BY_TASK:
+ case self::TYPE_DREV_HAS_RELATED_TASK:
+ case self::TYPE_MOCK_HAS_TASK:
+ return '%s updated tasks of %s.';
+ case self::TYPE_DREV_DEPENDS_ON_DREV:
+ case self::TYPE_DREV_DEPENDED_ON_BY_DREV:
+ case self::TYPE_TASK_HAS_RELATED_DREV:
+ case self::TYPE_COMMIT_HAS_DREV:
+ case self::TYPE_REVIEWER_FOR_DREV:
+ return '%s updated revisions of %s.';
+ case self::TYPE_BLOG_HAS_POST:
+ return '%s updated posts of %s.';
+ case self::TYPE_POST_HAS_BLOG:
+ case self::TYPE_BLOGGER_HAS_BLOG:
+ return '%s updated blogs of %s.';
+ case self::TYPE_BLOG_HAS_BLOGGER:
+ return '%s updated bloggers of %s.';
+ case self::TYPE_PROJ_MEMBER:
+ return '%s updated members of %s.';
+ case self::TYPE_MEMBER_OF_PROJ:
+ case self::TYPE_COMMIT_HAS_PROJECT:
+ return '%s updated projects of %s.';
+ case self::TYPE_QUESTION_HAS_VOTING_USER:
+ case self::TYPE_ANSWER_HAS_VOTING_USER:
+ return '%s updated voting users of %s.';
+ case self::TYPE_VOTING_USER_HAS_QUESTION:
+ return '%s updated questions of %s.';
+ case self::TYPE_VOTING_USER_HAS_ANSWER:
+ return '%s updated answers of %s.';
+ case self::TYPE_OBJECT_HAS_SUBSCRIBER:
+ return '%s updated subscribers of %s.';
+ case self::TYPE_OBJECT_HAS_UNSUBSCRIBER:
+ return '%s updated unsubcribers of %s.';
+ case self::TYPE_OBJECT_HAS_FILE:
+ return '%s updated files of %s.';
+ case self::TYPE_ACCOUNT_HAS_MEMBER:
+ return '%s updated members of %s.';
+ case self::TYPE_MEMBER_HAS_ACCOUNT:
+ return '%s updated accounts of %s.';
+ case self::TYPE_PURCAHSE_HAS_CHARGE:
+ return '%s updated charges of %s.';
+ case self::TYPE_CHARGE_HAS_PURCHASE:
+ return '%s updated purchases of %s.';
+ case self::TYPE_OBJECT_HAS_CONTRIBUTOR:
+ return '%s updated contributors of %s.';
+ case self::TYPE_DREV_HAS_REVIEWER:
+ return '%s updated reviewers of %s.';
+ case self::TYPE_TASK_HAS_MOCK:
+ return '%s updated mocks of %s.';
+ case self::TYPE_SUBSCRIBED_TO_OBJECT:
+ case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
+ case self::TYPE_FILE_HAS_OBJECT:
+ case self::TYPE_CONTRIBUTED_TO_OBJECT:
+ default:
+ return '%s updated objects of %s.';
+
+ }
+ }
+
}
diff --git a/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php b/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php
index 5a97e4c50c..06de456159 100644
--- a/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php
+++ b/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php
@@ -1,332 +1,668 @@
<?php
abstract class PhabricatorBaseEnglishTranslation
extends PhabricatorTranslation {
final public function getLanguage() {
return 'en';
}
public function getTranslations() {
return array(
'These %d configuration value(s) are related:' => array(
'This configuration value is related:',
'These configuration values are related:',
),
'Differential Revision(s)' => array(
'Differential Revision',
'Differential Revisions',
),
'file(s)' => array('file', 'files'),
'Maniphest Task(s)' => array('Maniphest Task', 'Maniphest Tasks'),
'Task(s)' => array('Task', 'Tasks'),
'Please fix these errors and try again.' => array(
'Please fix this error and try again.',
'Please fix these errors and try again.',
),
'%d Error(s)' => array('%d Error', '%d Errors'),
'%d Warning(s)' => array('%d Warning', '%d Warnings'),
'%d Auto-Fix(es)' => array('%d Auto-Fix', '%d Auto-Fixes'),
'%d Advice(s)' => array('%d Advice', '%d Pieces of Advice'),
'%d Detail(s)' => array('%d Detail', '%d Details'),
'(%d line(s))' => array('(%d line)', '(%d lines)'),
'COMMIT(S)' => array('COMMIT', 'COMMITS'),
'%d line(s)' => array('%d line', '%d lines'),
'%d path(s)' => array('%d path', '%d paths'),
'%d diff(s)' => array('%d diff', '%d diffs'),
'added %d commit(s): %s' => array(
'added commit: %2$s',
'added commits: %2$s',
),
'removed %d commit(s): %s' => array(
'removed commit: %2$s',
'removed commits: %2$s',
),
'changed %d commit(s), added %d: %s; removed %d: %s' =>
'changed commits, added: %3$s; removed: %5$s',
'ATTACHED %d COMMIT(S)' => array(
'ATTACHED COMMIT',
'ATTACHED COMMITS',
),
+ 'added %d mock(s): %s' => array(
+ 'added a mock: %2$s',
+ 'added mocks: %2$s',
+ ),
+
+ 'removed %d mock(s): %s' => array(
+ 'removed a mock: %2$s',
+ 'removed mocks: %2$s',
+ ),
+
+ 'changed %d mock(s), added %d: %s; removed %d: %s' =>
+ 'changed mocks, added: %3$s; removed: %5$s',
+
+ 'ATTACHED %d MOCK(S)' => array(
+ 'ATTACHED MOCK',
+ 'ATTACHED MOCKS',
+ ),
+
'added %d dependencie(s): %s' => array(
'added dependency: %2$s',
'added dependencies: %2$s',
),
'added %d dependent task(s): %s' => array(
'added dependent task: %2$s',
'added dependent tasks: %2$s',
),
'removed %d dependencie(s): %s' => array(
'removed dependency: %2$s',
'removed dependencies: %2$s',
),
'removed %d dependent task(s): %s' => array(
'removed dependent task: %2$s',
'removed dependent tasks: %2$s',
),
'changed %d dependencie(s), added %d: %s; removed %d: %s' =>
'changed dependencies, added: %3$s; removed: %5$s',
'changed %d dependent task(s), added %d: %s; removed %d: %s',
'changed dependent tasks, added: %3$s; removed: %5$s',
'DEPENDENT %d TASK(s)' => array(
'DEPENDENT TASK',
'DEPENDENT TASKS',
),
'DEPENDS ON %d TASK(S)' => array(
'DEPENDS ON TASK',
'DEPENDS ON TASKS',
),
'DIFFERENTIAL %d REVISION(S)' => array(
'DIFFERENTIAL REVISION',
'DIFFERENTIAL REVISIONS',
),
'added %d revision(s): %s' => array(
'added revision: %2$s',
'added revisions: %2$s',
),
'removed %d revision(s): %s' => array(
'removed revision: %2$s',
'removed revisions: %2$s',
),
'changed %d revision(s), added %d: %s; removed %d: %s' =>
'changed revisions, added %3$s; removed %5$s',
+ '%s edited revision(s), added %d: %s; removed %d: %s.' =>
+ '%s edited revisions, added: %3$s; removed: %5$s',
+
'There are %d raw fact(s) in storage.' => array(
'There is %d raw fact in storage.',
'There are %d raw facts in storage.',
),
'There are %d aggregate fact(s) in storage.' => array(
'There is %d aggregate fact in storage.',
'There are %d aggregate facts in storage.',
),
'%d Commit(s) Awaiting Audit' => array(
'%d Commit Awaiting Audit',
'%d Commits Awaiting Audit',
),
'%d Problem Commit(s)' => array(
'%d Problem Commit',
'%d Problem Commits',
),
'%d Review(s) Blocking Others' => array(
'%d Review Blocking Others',
'%d Reviews Blocking Others',
),
'%d Review(s) Need Attention' => array(
'%d Review Needs Attention',
'%d Reviews Need Attention',
),
'%d Review(s) Waiting on Others' => array(
'%d Review Waiting on Others',
'%d Reviews Waiting on Others',
),
'%d Flagged Object(s)' => array(
'%d Flagged Object',
'%d Flagged Objects',
),
'%d Unbreak Now Task(s)!' => array(
'%d Unbreak Now Task!',
'%d Unbreak Now Tasks!',
),
'%d Assigned Task(s)' => array(
'%d Assigned Task',
'%d Assigned Tasks',
),
'Show %d Lint Message(s)' => array(
'Show %d Lint Message',
'Show %d Lint Messages',
),
'Hide %d Lint Message(s)' => array(
'Hide %d Lint Message',
'Hide %d Lint Messages',
),
'Switch for %d Lint Message(s)' => array(
'Switch for %d Lint Message',
'Switch for %d Lint Messages',
),
'%d Lint Message(s)' => array(
'%d Lint Message',
'%d Lint Messages',
),
'This is a binary file. It is %s byte(s) in length.' => array(
'This is a binary file. It is %s byte in length.',
'This is a binary file. It is %s bytes in length.',
),
'%d Action(s) Have No Effect' => array(
'Action Has No Effect',
'Actions Have No Effect',
),
'%d Action(s) With No Effect' => array(
'Action With No Effect',
'Actions With No Effect',
),
+ '%s edited post(s), added %d: %s; removed %d: %s.' =>
+ '%s edited posts, added: %3$s; removed: %5$s',
+
+ '%s added %d post(s): %s.' => array(
+ array(
+ '%s added a post: %3$s.',
+ '%s added posts: %3$s.',
+ ),
+ ),
+
+ '%s removed %d post(s): %s.' => array(
+ array(
+ '%s removed a post: %3$s.',
+ '%s removed posts: %3$s.',
+ ),
+ ),
+
+ '%s edited blog(s), added %d: %s; removed %d: %s.' =>
+ '%s edited blogs, added: %3$s; removed: %5$s',
+
+ '%s added %d blog(s): %s.' => array(
+ array(
+ '%s added a blog: %3$s.',
+ '%s added blogs: %3$s.',
+ ),
+ ),
+
+ '%s removed %d blog(s): %s.' => array(
+ array(
+ '%s removed a blog: %3$s.',
+ '%s removed blogs: %3$s.',
+ ),
+ ),
+
+ '%s edited blogger(s), added %d: %s; removed %d: %s.' =>
+ '%s edited bloggers, added: %3$s; removed: %5$s',
+
+ '%s added %d blogger(s): %s.' => array(
+ array(
+ '%s added a blogger: %3$s.',
+ '%s added bloggers: %3$s.',
+ ),
+ ),
+
+ '%s removed %d blogger(s): %s.' => array(
+ array(
+ '%s removed a blogger: %3$s.',
+ '%s removed bloggers: %3$s.',
+ ),
+ ),
+
+ '%s edited member(s), added %d: %s; removed %d: %s.' =>
+ '%s edited members, added: %3$s; removed: %5$s',
+
+ '%s added %d member(s): %s.' => array(
+ array(
+ '%s added a member: %3$s.',
+ '%s added members: %3$s.',
+ ),
+ ),
+
+ '%s removed %d member(s): %s.' => array(
+ array(
+ '%s removed a member: %3$s.',
+ '%s removed members: %3$s.',
+ ),
+ ),
+
+ '%s edited project(s), added %d: %s; removed %d: %s.' =>
+ '%s edited projects, added: %3$s; removed: %5$s',
+
+ '%s added %d project(s): %s.' => array(
+ array(
+ '%s added a project: %3$s.',
+ '%s added projects: %3$s.',
+ ),
+ ),
+
+ '%s removed %d project(s): %s.' => array(
+ array(
+ '%s removed a project: %3$s.',
+ '%s removed projects: %3$s.',
+ ),
+ ),
+
+ '%s edited voting user(s), added %d: %s; removed %d: %s.' =>
+ '%s edited voting users, added: %3$s; removed: %5$s',
+
+ '%s added %d voting user(s): %s.' => array(
+ array(
+ '%s added a voting user: %3$s.',
+ '%s added voting users: %3$s.',
+ ),
+ ),
+
+ '%s removed %d voting user(s): %s.' => array(
+ array(
+ '%s removed a voting user: %3$s.',
+ '%s removed voting users: %3$s.',
+ ),
+ ),
+
+ '%s edited answer(s), added %d: %s; removed %d: %s.' =>
+ '%s edited answers, added: %3$s; removed: %5$s',
+
+ '%s added %d answer(s): %s.' => array(
+ array(
+ '%s added a answer: %3$s.',
+ '%s added answers: %3$s.',
+ ),
+ ),
+
+ '%s removed %d answer(s): %s.' => array(
+ array(
+ '%s removed a answer: %3$s.',
+ '%s removed answers: %3$s.',
+ ),
+ ),
+
+ '%s edited question(s), added %d: %s; removed %d: %s.' =>
+ '%s edited questions, added: %3$s; removed: %5$s',
+
+ '%s added %d question(s): %s.' => array(
+ array(
+ '%s added a question: %3$s.',
+ '%s added questions: %3$s.',
+ ),
+ ),
+
+ '%s removed %d question(s): %s.' => array(
+ array(
+ '%s removed a question: %3$s.',
+ '%s removed questions: %3$s.',
+ ),
+ ),
+
+ '%s edited mock(s), added %d: %s; removed %d: %s.' =>
+ '%s edited mocks, added: %3$s; removed: %5$s',
+
+ '%s added %d mock(s): %s.' => array(
+ array(
+ '%s added a mock: %3$s.',
+ '%s added mocks: %3$s.',
+ ),
+ ),
+
+ '%s removed %d mock(s): %s.' => array(
+ array(
+ '%s removed a mock: %3$s.',
+ '%s removed mocks: %3$s.',
+ ),
+ ),
+
+ '%s edited task(s), added %d: %s; removed %d: %s.' =>
+ '%s edited tasks, added: %3$s; removed: %5$s',
+
+ '%s added %d task(s): %s.' => array(
+ array(
+ '%s added a task: %3$s.',
+ '%s added tasks: %3$s.',
+ ),
+ ),
+
+ '%s removed %d task(s): %s.' => array(
+ array(
+ '%s removed a task: %3$s.',
+ '%s removed tasks: %3$s.',
+ ),
+ ),
+
+ '%s edited file(s), added %d: %s; removed %d: %s.' =>
+ '%s edited files, added: %3$s; removed: %5$s',
+
+ '%s added %d file(s): %s.' => array(
+ array(
+ '%s added a file: %3$s.',
+ '%s added files: %3$s.',
+ ),
+ ),
+
+ '%s removed %d file(s): %s.' => array(
+ array(
+ '%s removed a file: %3$s.',
+ '%s removed files: %3$s.',
+ ),
+ ),
+
+ '%s edited account(s), added %d: %s; removed %d: %s.' =>
+ '%s edited accounts, added: %3$s; removed: %5$s',
+
+ '%s added %d account(s): %s.' => array(
+ array(
+ '%s added a account: %3$s.',
+ '%s added accounts: %3$s.',
+ ),
+ ),
+
+ '%s removed %d account(s): %s.' => array(
+ array(
+ '%s removed a account: %3$s.',
+ '%s removed accounts: %3$s.',
+ ),
+ ),
+
+ '%s edited charge(s), added %d: %s; removed %d: %s.' =>
+ '%s edited charges, added: %3$s; removed: %5$s',
+
+ '%s added %d charge(s): %s.' => array(
+ array(
+ '%s added a charge: %3$s.',
+ '%s added charges: %3$s.',
+ ),
+ ),
+
+ '%s removed %d charge(s): %s.' => array(
+ array(
+ '%s removed a charge: %3$s.',
+ '%s removed charges: %3$s.',
+ ),
+ ),
+
+ '%s edited purchase(s), added %d: %s; removed %d: %s.' =>
+ '%s edited purchases, added: %3$s; removed: %5$s',
+
+ '%s added %d purchase(s): %s.' => array(
+ array(
+ '%s added a purchase: %3$s.',
+ '%s added purchases: %3$s.',
+ ),
+ ),
+
+ '%s removed %d purchase(s): %s.' => array(
+ array(
+ '%s removed a purchase: %3$s.',
+ '%s removed purchases: %3$s.',
+ ),
+ ),
+
+ '%s edited contributor(s), added %d: %s; removed %d: %s.' =>
+ '%s edited contributors, added: %3$s; removed: %5$s',
+
+ '%s added %d contributor(s): %s.' => array(
+ array(
+ '%s added a contributor: %3$s.',
+ '%s added contributors: %3$s.',
+ ),
+ ),
+
+ '%s removed %d contributor(s): %s.' => array(
+ array(
+ '%s removed a contributor: %3$s.',
+ '%s removed contributors: %3$s.',
+ ),
+ ),
+
+ '%s edited reviewer(s), added %d: %s; removed %d: %s.' =>
+ '%s edited reviewers, added: %3$s; removed: %5$s',
+
+ '%s added %d reviewer(s): %s.' => array(
+ array(
+ '%s added a reviewer: %3$s.',
+ '%s added reviewers: %3$s.',
+ ),
+ ),
+
+ '%s removed %d reviewer(s): %s.' => array(
+ array(
+ '%s removed a reviewer: %3$s.',
+ '%s removed reviewers: %3$s.',
+ ),
+ ),
+
+ '%s edited object(s), added %d: %s; removed %d: %s.' =>
+ '%s edited objects, added: %3$s; removed: %5$s',
+
+ '%s added %d object(s): %s.' => array(
+ array(
+ '%s added a object: %3$s.',
+ '%s added objects: %3$s.',
+ ),
+ ),
+
+ '%s removed %d object(s): %s.' => array(
+ array(
+ '%s removed a object: %3$s.',
+ '%s removed objects: %3$s.',
+ ),
+ ),
+
+ '%s edited subscriber(s), added %d: %s; removed %d: %s.' =>
+ '%s edited subscribers, added: %3$s; removed: %5$s',
+
'%s added %d subscriber(s): %s.' => array(
array(
'%s added a subscriber: %3$s.',
'%s added subscribers: %3$s.',
),
),
'%s removed %d subscriber(s): %s.' => array(
array(
'%s removed a subscriber: %3$s.',
'%s removed subscribers: %3$s.',
),
),
+ '%s edited unsubscriber(s), added %d: %s; removed %d: %s.' =>
+ '%s edited unsubscribers, added: %3$s; removed: %5$s',
+
+ '%s added %d unsubscriber(s): %s.' => array(
+ array(
+ '%s added a unsubscriber: %3$s.',
+ '%s added unsubscribers: %3$s.',
+ ),
+ ),
+
+ '%s removed %d unsubscriber(s): %s.' => array(
+ array(
+ '%s removed a unsubscriber: %3$s.',
+ '%s removed unsubscribers: %3$s.',
+ ),
+ ),
+
+ '%s edited participant(s), added %d: %s; removed %d: %s.' =>
+ '%s edited participants, added: %3$s; removed: %5$s',
+
'%s added %d participant(s): %s.' => array(
array(
'%s added a participant: %3$s.',
'%s added participants: %3$s.',
),
),
'%s removed %d participant(s): %s.' => array(
array(
'%s removed a participant: %3$s.',
'%s removed participants: %3$s.',
),
),
+ '%s edited image(s), added %d: %s; removed %d: %s.' =>
+ '%s edited images, added: %3$s; removed: %5$s',
+
'%s added %d image(s): %s.' => array(
array(
'%s added an image: %3$s.',
'%s added images: %3$s.',
),
),
'%s removed %d image(s): %s.' => array(
array(
'%s removed an image: %3$s.',
'%s removed images: %3$s.',
),
),
'%d people(s)' => array(
array(
'%d person',
'%d people',
),
),
'%s Line(s)' => array(
'%s Line',
'%s Lines',
),
"Indexing %d object(s) of type %s." => array(
"Indexing %d object of type %s.",
"Indexing %d object of type %s.",
),
'Run these %d command(s):' => array(
'Run this command:',
'Run these commands:',
),
'Install these %d PHP extension(s):' => array(
'Install this PHP extension:',
'Install these PHP extensions:',
),
'The current Phabricator configuration has these %d value(s):' => array(
'The current Phabricator configuration has this value:',
'The current Phabricator configuration has these values:',
),
'To update these %d value(s), run these command(s) from the command line:'
=> array(
'To update this value, run this command from the command line:',
'To update these values, run these commands from the command line:',
),
'You can update these %d value(s) here:' => array(
'You can update this value here:',
'You can update these values here:',
),
'The current PHP configuration has these %d value(s):' => array(
'The current PHP configuration has this value:',
'The current PHP configuration has these values:',
),
'To update these %d value(s), edit your PHP configuration file.' => array(
'To update this %d value, edit your PHP configuration file.',
'To update these %d values, edit your PHP configuration file.',
),
'To update these %d value(s), edit your PHP configuration file, located '.
'here:' => array(
'To update this value, edit your PHP configuration file, located '.
'here:',
'To update these values, edit your PHP configuration file, located '.
'here:',
),
'PHP also loaded these configuration file(s):' => array(
'PHP also loaded this configuration file:',
'PHP also loaded these configuration files:',
),
'You have %d unresolved setup issue(s)...' => array(
'You have an unresolved setup issue...',
'You have %d unresolved setup issues...',
),
'%s added %d inline comment(s).' => array(
array(
'%s added an inline comment.',
'%s added inline comments.',
),
),
'%d comment(s)' => array('%d comment', '%d comments'),
'%d rejection(s)' => array('%d rejection', '%d rejections'),
'%d update(s)' => array('%d update', '%d updates'),
'This configuration value is defined in these %d '.
'configuration source(s): %s.' => array(
'This configuration value is defined in this '.
'configuration source: %2$s.',
'This configuration value is defined in these %d '.
'configuration sources: %s.',
),
);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Aug 15, 12:41 PM (6 d, 9 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
202440
Default Alt Text
(171 KB)

Event Timeline