Page MenuHomestyx hydra

No OneTemporary

diff --git a/resources/sql/autopatches/20140210.herald.rule-condition-mig.php b/resources/sql/autopatches/20140210.herald.rule-condition-mig.php
new file mode 100644
index 0000000000..c653fa9d50
--- /dev/null
+++ b/resources/sql/autopatches/20140210.herald.rule-condition-mig.php
@@ -0,0 +1,31 @@
+<?php
+
+$table = new HeraldCondition();
+$conn_w = $table->establishConnection('w');
+
+echo "Migrating Herald conditions of type Herald rule from IDs to PHIDs...\n";
+foreach (new LiskMigrationIterator($table) as $condition) {
+ if ($condition->getFieldName() != HeraldAdapter::FIELD_RULE) {
+ continue;
+ }
+
+ $value = $condition->getValue();
+ if (!is_numeric($value)) {
+ continue;
+ }
+ $id = $condition->getID();
+ echo "Updating condition {$id}...\n";
+
+ $rule = id(new HeraldRuleQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withIDs(array($value))
+ ->executeOne();
+
+ queryfx(
+ $conn_w,
+ 'UPDATE %T SET value = %s WHERE id = %d',
+ $table->getTableName(),
+ json_encode($rule->getPHID()),
+ $id);
+}
+echo "Done.\n";
diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
index a7ea3844ff..0427f6587b 100644
--- a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
+++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
@@ -1,317 +1,316 @@
<?php
final class HeraldPreCommitContentAdapter extends HeraldPreCommitAdapter {
private $changesets;
private $commitRef;
private $fields;
private $revision = false;
public function getAdapterContentName() {
return pht('Commit Hook: Commit Content');
}
public function getAdapterSortOrder() {
return 2500;
}
public function getAdapterContentDescription() {
return pht(
"React to commits being pushed to hosted repositories.\n".
"Hook rules can block changes.");
}
public function getFields() {
return array_merge(
array(
self::FIELD_BODY,
self::FIELD_AUTHOR,
self::FIELD_AUTHOR_RAW,
self::FIELD_COMMITTER,
self::FIELD_COMMITTER_RAW,
self::FIELD_BRANCHES,
self::FIELD_DIFF_FILE,
self::FIELD_DIFF_CONTENT,
self::FIELD_DIFF_ADDED_CONTENT,
self::FIELD_DIFF_REMOVED_CONTENT,
self::FIELD_DIFF_ENORMOUS,
self::FIELD_REPOSITORY,
self::FIELD_REPOSITORY_PROJECTS,
self::FIELD_PUSHER,
self::FIELD_PUSHER_PROJECTS,
self::FIELD_DIFFERENTIAL_REVISION,
self::FIELD_DIFFERENTIAL_ACCEPTED,
self::FIELD_DIFFERENTIAL_REVIEWERS,
self::FIELD_DIFFERENTIAL_CCS,
self::FIELD_IS_MERGE_COMMIT,
- self::FIELD_RULE,
),
parent::getFields());
}
public function getHeraldName() {
return pht('Push Log (Content)');
}
public function getHeraldField($field) {
$log = $this->getObject();
switch ($field) {
case self::FIELD_BODY:
return $this->getCommitRef()->getMessage();
case self::FIELD_AUTHOR:
return $this->getAuthorPHID();
case self::FIELD_AUTHOR_RAW:
return $this->getAuthorRaw();
case self::FIELD_COMMITTER:
return $this->getCommitterPHID();
case self::FIELD_COMMITTER_RAW:
return $this->getCommitterRaw();
case self::FIELD_BRANCHES:
return $this->getBranches();
case self::FIELD_DIFF_FILE:
return $this->getDiffContent('name');
case self::FIELD_DIFF_CONTENT:
return $this->getDiffContent('*');
case self::FIELD_DIFF_ADDED_CONTENT:
return $this->getDiffContent('+');
case self::FIELD_DIFF_REMOVED_CONTENT:
return $this->getDiffContent('-');
case self::FIELD_DIFF_ENORMOUS:
$this->getDiffContent('*');
return ($this->changesets instanceof Exception);
case self::FIELD_REPOSITORY:
return $this->getHookEngine()->getRepository()->getPHID();
case self::FIELD_REPOSITORY_PROJECTS:
return $this->getHookEngine()->getRepository()->getProjectPHIDs();
case self::FIELD_PUSHER:
return $this->getHookEngine()->getViewer()->getPHID();
case self::FIELD_PUSHER_PROJECTS:
return $this->getHookEngine()->loadViewerProjectPHIDsForHerald();
case self::FIELD_DIFFERENTIAL_REVISION:
$revision = $this->getRevision();
if (!$revision) {
return null;
}
return $revision->getPHID();
case self::FIELD_DIFFERENTIAL_ACCEPTED:
$revision = $this->getRevision();
if (!$revision) {
return null;
}
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
if ($revision->getStatus() != $status_accepted) {
return null;
}
return $revision->getPHID();
case self::FIELD_DIFFERENTIAL_REVIEWERS:
$revision = $this->getRevision();
if (!$revision) {
return array();
}
return $revision->getReviewers();
case self::FIELD_DIFFERENTIAL_CCS:
$revision = $this->getRevision();
if (!$revision) {
return array();
}
return $revision->getCCPHIDs();
case self::FIELD_IS_MERGE_COMMIT:
return $this->getIsMergeCommit();
}
return parent::getHeraldField($field);
}
private function getDiffContent($type) {
if ($this->changesets === null) {
try {
$this->changesets = $this->getHookEngine()->loadChangesetsForCommit(
$this->getObject()->getRefNew());
} catch (Exception $ex) {
$this->changesets = $ex;
}
}
if ($this->changesets instanceof Exception) {
$ex_class = get_class($this->changesets);
$ex_message = $this->changesets->getmessage();
if ($type === 'name') {
return array("<{$ex_class}: {$ex_message}>");
} else {
return array("<{$ex_class}>" => $ex_message);
}
}
$result = array();
if ($type === 'name') {
foreach ($this->changesets as $change) {
$result[] = $change->getFilename();
}
} else {
foreach ($this->changesets as $change) {
$lines = array();
foreach ($change->getHunks() as $hunk) {
switch ($type) {
case '-':
$lines[] = $hunk->makeOldFile();
break;
case '+':
$lines[] = $hunk->makeNewFile();
break;
case '*':
default:
$lines[] = $hunk->makeChanges();
break;
}
}
$result[$change->getFilename()] = implode('', $lines);
}
}
return $result;
}
private function getCommitRef() {
if ($this->commitRef === null) {
$this->commitRef = $this->getHookEngine()->loadCommitRefForCommit(
$this->getObject()->getRefNew());
}
return $this->commitRef;
}
private function getAuthorPHID() {
$repository = $this->getHookEngine()->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$ref = $this->getCommitRef();
$author = $ref->getAuthor();
if (!strlen($author)) {
return null;
}
return $this->lookupUser($author);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// In Subversion, the pusher is always the author.
return $this->getHookEngine()->getViewer()->getPHID();
}
}
private function getCommitterPHID() {
$repository = $this->getHookEngine()->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// Here, if there's no committer, we're going to return the author
// instead.
$ref = $this->getCommitRef();
$committer = $ref->getCommitter();
if (!strlen($committer)) {
return $this->getAuthorPHID();
}
$phid = $this->lookupUser($committer);
if (!$phid) {
return $this->getAuthorPHID();
}
return $phid;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// In Subversion, the pusher is always the committer.
return $this->getHookEngine()->getViewer()->getPHID();
}
}
private function getAuthorRaw() {
$repository = $this->getHookEngine()->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$ref = $this->getCommitRef();
return $ref->getAuthor();
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// In Subversion, the pusher is always the author.
return $this->getHookEngine()->getViewer()->getUsername();
}
}
private function getCommitterRaw() {
$repository = $this->getHookEngine()->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// Here, if there's no committer, we're going to return the author
// instead.
$ref = $this->getCommitRef();
$committer = $ref->getCommitter();
if (strlen($committer)) {
return $committer;
}
return $ref->getAuthor();
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// In Subversion, the pusher is always the committer.
return $this->getHookEngine()->getViewer()->getUsername();
}
}
private function lookupUser($author) {
return id(new DiffusionResolveUserQuery())
->withName($author)
->execute();
}
private function getCommitFields() {
if ($this->fields === null) {
$this->fields = id(new DiffusionLowLevelCommitFieldsQuery())
->setRepository($this->getHookEngine()->getRepository())
->withCommitRef($this->getCommitRef())
->execute();
}
return $this->fields;
}
private function getRevision() {
if ($this->revision === false) {
$fields = $this->getCommitFields();
$revision_id = idx($fields, 'revisionID');
if (!$revision_id) {
$this->revision = null;
} else {
$this->revision = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($revision_id))
->needRelationships(true)
->executeOne();
}
}
return $this->revision;
}
private function getIsMergeCommit() {
$repository = $this->getHookEngine()->getRepository();
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$parents = id(new DiffusionLowLevelParentsQuery())
->setRepository($repository)
->withIdentifier($this->getObject()->getRefNew())
->execute();
return (count($parents) > 1);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
// NOTE: For now, we ignore "svn:mergeinfo" at all levels. We might
// change this some day, but it's not nearly as clear a signal as
// ancestry is in Git/Mercurial.
return false;
}
}
private function getBranches() {
return $this->getHookEngine()->loadBranches(
$this->getObject()->getRefNew());
}
}
diff --git a/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php
index 75ae07a5e9..be7b42e648 100644
--- a/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php
+++ b/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php
@@ -1,109 +1,108 @@
<?php
final class HeraldPreCommitRefAdapter extends HeraldPreCommitAdapter {
const FIELD_REF_TYPE = 'ref-type';
const FIELD_REF_NAME = 'ref-name';
const FIELD_REF_CHANGE = 'ref-change';
const VALUE_REF_TYPE = 'value-ref-type';
const VALUE_REF_CHANGE = 'value-ref-change';
public function getAdapterContentName() {
return pht('Commit Hook: Branches/Tags/Bookmarks');
}
public function getAdapterSortOrder() {
return 2000;
}
public function getAdapterContentDescription() {
return pht(
"React to branches and tags being pushed to hosted repositories.\n".
"Hook rules can block changes.");
}
public function getFieldNameMap() {
return array(
self::FIELD_REF_TYPE => pht('Ref type'),
self::FIELD_REF_NAME => pht('Ref name'),
self::FIELD_REF_CHANGE => pht('Ref change type'),
) + parent::getFieldNameMap();
}
public function getFields() {
return array_merge(
array(
self::FIELD_REF_TYPE,
self::FIELD_REF_NAME,
self::FIELD_REF_CHANGE,
self::FIELD_REPOSITORY,
self::FIELD_REPOSITORY_PROJECTS,
self::FIELD_PUSHER,
self::FIELD_PUSHER_PROJECTS,
- self::FIELD_RULE,
),
parent::getFields());
}
public function getConditionsForField($field) {
switch ($field) {
case self::FIELD_REF_NAME:
return array(
self::CONDITION_IS,
self::CONDITION_IS_NOT,
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
);
case self::FIELD_REF_TYPE:
return array(
self::CONDITION_IS,
self::CONDITION_IS_NOT,
);
case self::FIELD_REF_CHANGE:
return array(
self::CONDITION_HAS_BIT,
self::CONDITION_NOT_BIT,
);
}
return parent::getConditionsForField($field);
}
public function getValueTypeForFieldAndCondition($field, $condition) {
switch ($field) {
case self::FIELD_REF_TYPE:
return self::VALUE_REF_TYPE;
case self::FIELD_REF_CHANGE:
return self::VALUE_REF_CHANGE;
}
return parent::getValueTypeForFieldAndCondition($field, $condition);
}
public function getHeraldName() {
return pht('Push Log (Ref)');
}
public function getHeraldField($field) {
$log = $this->getObject();
switch ($field) {
case self::FIELD_REF_TYPE:
return $log->getRefType();
case self::FIELD_REF_NAME:
return $log->getRefName();
case self::FIELD_REF_CHANGE:
return $log->getChangeFlags();
case self::FIELD_REPOSITORY:
return $this->getHookEngine()->getRepository()->getPHID();
case self::FIELD_REPOSITORY_PROJECTS:
return $this->getHookEngine()->getRepository()->getProjectPHIDs();
case self::FIELD_PUSHER:
return $this->getHookEngine()->getViewer()->getPHID();
case self::FIELD_PUSHER_PROJECTS:
return $this->getHookEngine()->loadViewerProjectPHIDsForHerald();
}
return parent::getHeraldField($field);
}
}
diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php
index 570fffe5de..f4a6d214cf 100644
--- a/src/applications/herald/controller/HeraldRuleController.php
+++ b/src/applications/herald/controller/HeraldRuleController.php
@@ -1,643 +1,652 @@
<?php
final class HeraldRuleController extends HeraldController {
private $id;
private $filter;
public function willProcessRequest(array $data) {
$this->id = (int)idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$content_type_map = HeraldAdapter::getEnabledAdapterMap($user);
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
if ($this->id) {
$id = $this->id;
$rule = id(new HeraldRuleQuery())
->setViewer($user)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$rule) {
return new Aphront404Response();
}
$cancel_uri = $this->getApplicationURI("rule/{$id}/");
} else {
$rule = new HeraldRule();
$rule->setAuthorPHID($user->getPHID());
$rule->setMustMatchAll(1);
$content_type = $request->getStr('content_type');
$rule->setContentType($content_type);
$rule_type = $request->getStr('rule_type');
if (!isset($rule_type_map[$rule_type])) {
$rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL;
}
$rule->setRuleType($rule_type);
$adapter = HeraldAdapter::getAdapterForContentType(
$rule->getContentType());
if (!$adapter->supportsRuleType($rule->getRuleType())) {
throw new Exception(
pht(
"This rule's content type does not support the selected rule ".
"type."));
}
if ($rule->isObjectRule()) {
$rule->setTriggerObjectPHID($request->getStr('targetPHID'));
$object = id(new PhabricatorObjectQuery())
->setViewer($user)
->withPHIDs(array($rule->getTriggerObjectPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
throw new Exception(
pht('No valid object provided for object rule!'));
}
if (!$adapter->canTriggerOnObject($object)) {
throw new Exception(
pht('Object is of wrong type for adapter!'));
}
}
$cancel_uri = $this->getApplicationURI();
}
if ($rule->isGlobalRule()) {
$this->requireApplicationCapability(
HeraldCapabilityManageGlobalRules::CAPABILITY);
}
$adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType());
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
throw new Exception(
pht(
"This rule was created with a newer version of Herald. You can not ".
"view or edit it in this older version. Upgrade your Phabricator ".
"deployment."));
}
// Upgrade rule version to our version, since we might add newly-defined
// conditions, etc.
$rule->setConfigVersion($local_version);
$rule_conditions = $rule->loadConditions();
$rule_actions = $rule->loadActions();
$rule->attachConditions($rule_conditions);
$rule->attachActions($rule_actions);
$e_name = true;
$errors = array();
if ($request->isFormPost() && $request->getStr('save')) {
list($e_name, $errors) = $this->saveRule($adapter, $rule, $request);
if (!$errors) {
$id = $rule->getID();
$uri = $this->getApplicationURI("rule/{$id}/");
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
$must_match_selector = $this->renderMustMatchSelector($rule);
$repetition_selector = $this->renderRepetitionSelector($rule, $adapter);
$handles = $this->loadHandlesForRule($rule);
require_celerity_resource('herald-css');
$content_type_name = $content_type_map[$rule->getContentType()];
$rule_type_name = $rule_type_map[$rule->getRuleType()];
$form = id(new AphrontFormView())
->setUser($user)
->setID('herald-rule-edit-form')
->addHiddenInput('content_type', $rule->getContentType())
->addHiddenInput('rule_type', $rule->getRuleType())
->addHiddenInput('save', 1)
->appendChild(
// Build this explicitly (instead of using addHiddenInput())
// so we can add a sigil to it.
javelin_tag(
'input',
array(
'type' => 'hidden',
'name' => 'rule',
'sigil' => 'rule',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Rule Name'))
->setName('name')
->setError($e_name)
->setValue($rule->getName()));
$trigger_object_control = false;
if ($rule->isObjectRule()) {
$trigger_object_control = id(new AphrontFormStaticControl())
->setValue(
pht(
'This rule triggers for %s.',
$handles[$rule->getTriggerObjectPHID()]->renderLink()));
}
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setValue(pht(
"This %s rule triggers for %s.",
phutil_tag('strong', array(), $rule_type_name),
phutil_tag('strong', array(), $content_type_name))))
->appendChild($trigger_object_control)
->appendChild(
id(new AphrontFormInsetView())
->setTitle(pht('Conditions'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-condition',
'mustcapture' => true
),
pht('New Condition')))
->setDescription(
pht('When %s these conditions are met:', $must_match_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-conditions',
'class' => 'herald-condition-table'
),
'')))
->appendChild(
id(new AphrontFormInsetView())
->setTitle(pht('Action'))
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-action',
'mustcapture' => true,
),
pht('New Action')))
->setDescription(pht(
'Take these actions %s this rule matches:',
$repetition_selector))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'rule-actions',
'class' => 'herald-action-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Rule'))
->addCancelButton($cancel_uri));
$this->setupEditorBehavior($rule, $handles, $adapter);
$title = $rule->getID()
? pht('Edit Herald Rule')
: pht('Create Herald Rule');
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setForm($form);
$crumbs = $this
->buildApplicationCrumbs()
->addTextCrumb($title);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => pht('Edit Rule'),
'device' => true,
));
}
private function saveRule(HeraldAdapter $adapter, $rule, $request) {
$rule->setName($request->getStr('name'));
$match_all = ($request->getStr('must_match') == 'all');
$rule->setMustMatchAll((int)$match_all);
$repetition_policy_param = $request->getStr('repetition_policy');
$rule->setRepetitionPolicy(
HeraldRepetitionPolicyConfig::toInt($repetition_policy_param));
$e_name = true;
$errors = array();
if (!strlen($rule->getName())) {
$e_name = pht("Required");
$errors[] = pht("Rule must have a name.");
}
$data = json_decode($request->getStr('rule'), true);
if (!is_array($data) ||
!$data['conditions'] ||
!$data['actions']) {
throw new Exception("Failed to decode rule data.");
}
$conditions = array();
foreach ($data['conditions'] as $condition) {
if ($condition === null) {
// We manage this as a sparse array on the client, so may receive
// NULL if conditions have been removed.
continue;
}
$obj = new HeraldCondition();
$obj->setFieldName($condition[0]);
$obj->setFieldCondition($condition[1]);
if (is_array($condition[2])) {
$obj->setValue(array_keys($condition[2]));
} else {
$obj->setValue($condition[2]);
}
try {
$adapter->willSaveCondition($obj);
} catch (HeraldInvalidConditionException $ex) {
$errors[] = $ex->getMessage();
}
$conditions[] = $obj;
}
$actions = array();
foreach ($data['actions'] as $action) {
if ($action === null) {
// Sparse on the client; removals can give us NULLs.
continue;
}
if (!isset($action[1])) {
// Legitimate for any action which doesn't need a target, like
// "Do nothing".
$action[1] = null;
}
$obj = new HeraldAction();
$obj->setAction($action[0]);
$obj->setTarget($action[1]);
try {
$adapter->willSaveAction($rule, $obj);
} catch (HeraldInvalidActionException $ex) {
$errors[] = $ex;
}
$actions[] = $obj;
}
$rule->attachConditions($conditions);
$rule->attachActions($actions);
if (!$errors) {
try {
$edit_action = $rule->getID() ? 'edit' : 'create';
$rule->openTransaction();
$rule->save();
$rule->saveConditions($conditions);
$rule->saveActions($actions);
$rule->logEdit($request->getUser()->getPHID(), $edit_action);
$rule->saveTransaction();
} catch (AphrontQueryDuplicateKeyException $ex) {
$e_name = pht("Not Unique");
$errors[] = pht("Rule name is not unique. Choose a unique name.");
}
}
return array($e_name, $errors);
}
private function setupEditorBehavior(
HeraldRule $rule,
array $handles,
HeraldAdapter $adapter) {
$serial_conditions = array(
array('default', 'default', ''),
);
if ($rule->getConditions()) {
$serial_conditions = array();
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
switch ($condition->getFieldName()) {
case HeraldAdapter::FIELD_TASK_PRIORITY:
$value_map = array();
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
foreach ($value as $priority) {
$value_map[$priority] = idx($priority_map, $priority);
}
$value = $value_map;
break;
default:
if (is_array($value)) {
$value_map = array();
foreach ($value as $k => $fbid) {
$value_map[$fbid] = $handles[$fbid]->getName();
}
$value = $value_map;
}
break;
}
$serial_conditions[] = array(
$condition->getFieldName(),
$condition->getFieldCondition(),
$value,
);
}
}
$serial_actions = array(
array('default', ''),
);
if ($rule->getActions()) {
$serial_actions = array();
foreach ($rule->getActions() as $action) {
switch ($action->getAction()) {
case HeraldAdapter::ACTION_FLAG:
case HeraldAdapter::ACTION_BLOCK:
$current_value = $action->getTarget();
break;
default:
$target_map = array();
foreach ((array)$action->getTarget() as $fbid) {
$target_map[$fbid] = $handles[$fbid]->getName();
}
$current_value = $target_map;
break;
}
$serial_actions[] = array(
$action->getAction(),
$current_value,
);
}
}
$all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
- $all_rules = mpull($all_rules, 'getName', 'getID');
+ $all_rules = mpull($all_rules, 'getName', 'getPHID');
asort($all_rules);
$all_fields = $adapter->getFieldNameMap();
$all_conditions = $adapter->getConditionNameMap();
$all_actions = $adapter->getActionNameMap($rule->getRuleType());
$fields = $adapter->getFields();
$field_map = array_select_keys($all_fields, $fields);
$actions = $adapter->getActions($rule->getRuleType());
$action_map = array_select_keys($all_actions, $actions);
$config_info = array();
$config_info['fields'] = $field_map;
$config_info['conditions'] = $all_conditions;
$config_info['actions'] = $action_map;
foreach ($config_info['fields'] as $field => $name) {
$field_conditions = $adapter->getConditionsForField($field);
$config_info['conditionMap'][$field] = $field_conditions;
}
foreach ($config_info['fields'] as $field => $fname) {
foreach ($config_info['conditionMap'][$field] as $condition) {
$value_type = $adapter->getValueTypeForFieldAndCondition(
$field,
$condition);
$config_info['values'][$field][$condition] = $value_type;
}
}
$config_info['rule_type'] = $rule->getRuleType();
foreach ($config_info['actions'] as $action => $name) {
$config_info['targets'][$action] = $adapter->getValueTypeForAction(
$action,
$rule->getRuleType());
}
Javelin::initBehavior(
'herald-rule-editor',
array(
'root' => 'herald-rule-edit-form',
'conditions' => (object)$serial_conditions,
'actions' => (object)$serial_actions,
'select' => array(
HeraldAdapter::VALUE_CONTENT_SOURCE => array(
'options' => PhabricatorContentSource::getSourceNameMap(),
'default' => PhabricatorContentSource::SOURCE_WEB,
),
HeraldAdapter::VALUE_FLAG_COLOR => array(
'options' => PhabricatorFlagColor::getColorNameMap(),
'default' => PhabricatorFlagColor::COLOR_BLUE,
),
HeraldPreCommitRefAdapter::VALUE_REF_TYPE => array(
'options' => array(
PhabricatorRepositoryPushLog::REFTYPE_BRANCH
=> pht('branch (git/hg)'),
PhabricatorRepositoryPushLog::REFTYPE_TAG
=> pht('tag (git)'),
PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK
=> pht('bookmark (hg)'),
),
'default' => PhabricatorRepositoryPushLog::REFTYPE_BRANCH,
),
HeraldPreCommitRefAdapter::VALUE_REF_CHANGE => array(
'options' => array(
PhabricatorRepositoryPushLog::CHANGEFLAG_ADD =>
pht('change creates ref'),
PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE =>
pht('change deletes ref'),
PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE =>
pht('change rewrites ref'),
PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS =>
pht('dangerous change'),
),
'default' => PhabricatorRepositoryPushLog::CHANGEFLAG_ADD,
),
),
'template' => $this->buildTokenizerTemplates() + array(
'rules' => $all_rules,
),
'author' => array($rule->getAuthorPHID() =>
$handles[$rule->getAuthorPHID()]->getName()),
'info' => $config_info,
));
}
private function loadHandlesForRule($rule) {
$phids = array();
foreach ($rule->getActions() as $action) {
if (!is_array($action->getTarget())) {
continue;
}
foreach ($action->getTarget() as $target) {
$target = (array)$target;
foreach ($target as $phid) {
$phids[] = $phid;
}
}
}
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (is_array($value)) {
foreach ($value as $phid) {
$phids[] = $phid;
}
}
}
$phids[] = $rule->getAuthorPHID();
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $this->loadViewerHandles($phids);
}
/**
* Render the selector for the "When (all of | any of) these conditions are
* met:" element.
*/
private function renderMustMatchSelector($rule) {
return AphrontFormSelectControl::renderSelectTag(
$rule->getMustMatchAll() ? 'all' : 'any',
array(
'all' => pht('all of'),
'any' => pht('any of'),
),
array(
'name' => 'must_match',
));
}
/**
* Render the selector for "Take these actions (every time | only the first
* time) this rule matches..." element.
*/
private function renderRepetitionSelector($rule, HeraldAdapter $adapter) {
$repetition_policy = HeraldRepetitionPolicyConfig::toString(
$rule->getRepetitionPolicy());
$repetition_options = $adapter->getRepetitionOptions();
$repetition_names = HeraldRepetitionPolicyConfig::getMap();
$repetition_map = array_select_keys($repetition_names, $repetition_options);
if (count($repetition_map) < 2) {
return head($repetition_names);
} else {
return AphrontFormSelectControl::renderSelectTag(
$repetition_policy,
$repetition_map,
array(
'name' => 'repetition_policy',
));
}
}
protected function buildTokenizerTemplates() {
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
return array(
'source' => array(
'email' => '/typeahead/common/mailable/',
'user' => '/typeahead/common/accounts/',
'repository' => '/typeahead/common/repositories/',
'package' => '/typeahead/common/packages/',
'project' => '/typeahead/common/projects/',
'userorproject' => '/typeahead/common/accountsorprojects/',
'buildplan' => '/typeahead/common/buildplans/',
'taskpriority' => '/typeahead/common/taskpriority/',
),
'markup' => $template,
);
}
/**
* Load rules for the "Another Herald rule..." condition dropdown, which
* allows one rule to depend upon the success or failure of another rule.
*/
private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) {
$viewer = $this->getRequest()->getUser();
// Any rule can depend on a global rule.
$all_rules = id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL))
->withContentTypes(array($rule->getContentType()))
->execute();
if ($rule->isObjectRule()) {
// Object rules may depend on other rules for the same object.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT))
->withContentTypes(array($rule->getContentType()))
->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID()))
->execute();
}
if ($rule->isPersonalRule()) {
// Personal rules may depend upon your other personal rules.
$all_rules += id(new HeraldRuleQuery())
->setViewer($viewer)
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL))
->withContentTypes(array($rule->getContentType()))
->withAuthorPHIDs(array($rule->getAuthorPHID()))
->execute();
}
+ // mark disabled rules as disabled since they are not useful as such;
+ // don't filter though to keep edit cases sane / expected
+ foreach ($all_rules as $current_rule) {
+ if ($current_rule->getIsDisabled()) {
+ $current_rule->makeEphemeral();
+ $current_rule->setName($rule->getName(). ' '.pht('(Disabled)'));
+ }
+ }
+
// A rule can not depend upon itself.
unset($all_rules[$rule->getID()]);
return $all_rules;
}
}
diff --git a/src/applications/herald/engine/HeraldEngine.php b/src/applications/herald/engine/HeraldEngine.php
index f2915d703e..acc9556c97 100644
--- a/src/applications/herald/engine/HeraldEngine.php
+++ b/src/applications/herald/engine/HeraldEngine.php
@@ -1,434 +1,434 @@
<?php
final class HeraldEngine {
protected $rules = array();
protected $results = array();
protected $stack = array();
protected $activeRule = null;
protected $fieldCache = array();
protected $object = null;
private $dryRun;
public function setDryRun($dry_run) {
$this->dryRun = $dry_run;
return $this;
}
public function getDryRun() {
return $this->dryRun;
}
- public function getRule($id) {
- return idx($this->rules, $id);
+ public function getRule($phid) {
+ return idx($this->rules, $phid);
}
public function loadRulesForAdapter(HeraldAdapter $adapter) {
return id(new HeraldRuleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withDisabled(false)
->withContentTypes(array($adapter->getAdapterContentType()))
->needConditionsAndActions(true)
->needAppliedToPHIDs(array($adapter->getPHID()))
->needValidateAuthors(true)
->execute();
}
public static function loadAndApplyRules(HeraldAdapter $adapter) {
$engine = new HeraldEngine();
$rules = $engine->loadRulesForAdapter($adapter);
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
return $engine->getTranscript();
}
public function applyRules(array $rules, HeraldAdapter $object) {
assert_instances_of($rules, 'HeraldRule');
$t_start = microtime(true);
- $rules = mpull($rules, null, 'getID');
+ $rules = mpull($rules, null, 'getPHID');
$this->transcript = new HeraldTranscript();
$this->transcript->setObjectPHID((string)$object->getPHID());
$this->fieldCache = array();
$this->results = array();
$this->rules = $rules;
$this->object = $object;
$effects = array();
- foreach ($rules as $id => $rule) {
+ foreach ($rules as $phid => $rule) {
$this->stack = array();
try {
if (!$this->getDryRun() &&
($rule->getRepetitionPolicy() ==
HeraldRepetitionPolicyConfig::FIRST) &&
$rule->getRuleApplied($object->getPHID())) {
// This is not a dry run, and this rule is only supposed to be
// applied a single time, and it's already been applied...
// That means automatic failure.
$xscript = id(new HeraldRuleTranscript())
- ->setRuleID($id)
+ ->setRuleID($rule->getID())
->setResult(false)
->setRuleName($rule->getName())
->setRuleOwner($rule->getAuthorPHID())
->setReason(
"This rule is only supposed to be repeated a single time, ".
"and it has already been applied.");
$this->transcript->addRuleTranscript($xscript);
$rule_matches = false;
} else {
$rule_matches = $this->doesRuleMatch($rule, $object);
}
} catch (HeraldRecursiveConditionsException $ex) {
$names = array();
foreach ($this->stack as $rule_id => $ignored) {
$names[] = '"'.$rules[$rule_id]->getName().'"';
}
$names = implode(', ', $names);
foreach ($this->stack as $rule_id => $ignored) {
$xscript = new HeraldRuleTranscript();
$xscript->setRuleID($rule_id);
$xscript->setResult(false);
$xscript->setReason(
"Rules {$names} are recursively dependent upon one another! ".
"Don't do this! You have formed an unresolvable cycle in the ".
"dependency graph!");
$xscript->setRuleName($rules[$rule_id]->getName());
$xscript->setRuleOwner($rules[$rule_id]->getAuthorPHID());
$this->transcript->addRuleTranscript($xscript);
}
$rule_matches = false;
}
- $this->results[$id] = $rule_matches;
+ $this->results[$phid] = $rule_matches;
if ($rule_matches) {
foreach ($this->getRuleEffects($rule, $object) as $effect) {
$effects[] = $effect;
}
}
}
$object_transcript = new HeraldObjectTranscript();
$object_transcript->setPHID($object->getPHID());
$object_transcript->setName($object->getHeraldName());
$object_transcript->setType($object->getAdapterContentType());
$object_transcript->setFields($this->fieldCache);
$this->transcript->setObjectTranscript($object_transcript);
$t_end = microtime(true);
$this->transcript->setDuration($t_end - $t_start);
return $effects;
}
public function applyEffects(
array $effects,
HeraldAdapter $adapter,
array $rules) {
assert_instances_of($effects, 'HeraldEffect');
assert_instances_of($rules, 'HeraldRule');
$this->transcript->setDryRun((int)$this->getDryRun());
if ($this->getDryRun()) {
$xscripts = array();
foreach ($effects as $effect) {
$xscripts[] = new HeraldApplyTranscript(
$effect,
false,
pht('This was a dry run, so no actions were actually taken.'));
}
} else {
$xscripts = $adapter->applyHeraldEffects($effects);
}
assert_instances_of($xscripts, 'HeraldApplyTranscript');
foreach ($xscripts as $apply_xscript) {
$this->transcript->addApplyTranscript($apply_xscript);
}
// For dry runs, don't mark the rule as having applied to the object.
if ($this->getDryRun()) {
return;
}
$rules = mpull($rules, null, 'getID');
$applied_ids = array();
$first_policy = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::FIRST);
// Mark all the rules that have had their effects applied as having been
// executed for the current object.
$rule_ids = mpull($xscripts, 'getRuleID');
foreach ($rule_ids as $rule_id) {
if (!$rule_id) {
// Some apply transcripts are purely informational and not associated
// with a rule, e.g. carryover emails from earlier revisions.
continue;
}
$rule = idx($rules, $rule_id);
if (!$rule) {
continue;
}
if ($rule->getRepetitionPolicy() == $first_policy) {
$applied_ids[] = $rule_id;
}
}
if ($applied_ids) {
$conn_w = id(new HeraldRule())->establishConnection('w');
$sql = array();
foreach ($applied_ids as $id) {
$sql[] = qsprintf(
$conn_w,
'(%s, %d)',
$adapter->getPHID(),
$id);
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q',
HeraldRule::TABLE_RULE_APPLIED,
implode(', ', $sql));
}
}
public function getTranscript() {
$this->transcript->save();
return $this->transcript;
}
public function doesRuleMatch(
HeraldRule $rule,
HeraldAdapter $object) {
- $id = $rule->getID();
+ $phid = $rule->getPHID();
- if (isset($this->results[$id])) {
+ if (isset($this->results[$phid])) {
// If we've already evaluated this rule because another rule depends
// on it, we don't need to reevaluate it.
- return $this->results[$id];
+ return $this->results[$phid];
}
- if (isset($this->stack[$id])) {
+ if (isset($this->stack[$phid])) {
// We've recursed, fail all of the rules on the stack. This happens when
// there's a dependency cycle with "Rule conditions match for rule ..."
// conditions.
- foreach ($this->stack as $rule_id => $ignored) {
- $this->results[$rule_id] = false;
+ foreach ($this->stack as $rule_phid => $ignored) {
+ $this->results[$rule_phid] = false;
}
throw new HeraldRecursiveConditionsException();
}
- $this->stack[$id] = true;
+ $this->stack[$phid] = true;
$all = $rule->getMustMatchAll();
$conditions = $rule->getConditions();
$result = null;
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
$reason = pht(
"Rule could not be processed, it was created with a newer version ".
"of Herald.");
$result = false;
} else if (!$conditions) {
$reason = pht(
"Rule failed automatically because it has no conditions.");
$result = false;
} else if (!$rule->hasValidAuthor()) {
$reason = pht(
"Rule failed automatically because its owner is invalid ".
"or disabled.");
$result = false;
} else if (!$this->canAuthorViewObject($rule, $object)) {
$reason = pht(
"Rule failed automatically because it is a personal rule and its ".
"owner can not see the object.");
$result = false;
} else if (!$this->canRuleApplyToObject($rule, $object)) {
$reason = pht(
"Rule failed automatically because it is an object rule which is ".
"not relevant for this object.");
$result = false;
} else {
foreach ($conditions as $condition) {
$match = $this->doesConditionMatch($rule, $condition, $object);
if (!$all && $match) {
$reason = "Any condition matched.";
$result = true;
break;
}
if ($all && !$match) {
$reason = "Not all conditions matched.";
$result = false;
break;
}
}
if ($result === null) {
if ($all) {
$reason = "All conditions matched.";
$result = true;
} else {
$reason = "No conditions matched.";
$result = false;
}
}
}
$rule_transcript = new HeraldRuleTranscript();
$rule_transcript->setRuleID($rule->getID());
$rule_transcript->setResult($result);
$rule_transcript->setReason($reason);
$rule_transcript->setRuleName($rule->getName());
$rule_transcript->setRuleOwner($rule->getAuthorPHID());
$this->transcript->addRuleTranscript($rule_transcript);
return $result;
}
protected function doesConditionMatch(
HeraldRule $rule,
HeraldCondition $condition,
HeraldAdapter $object) {
$object_value = $this->getConditionObjectValue($condition, $object);
$test_value = $condition->getValue();
$cond = $condition->getFieldCondition();
$transcript = new HeraldConditionTranscript();
$transcript->setRuleID($rule->getID());
$transcript->setConditionID($condition->getID());
$transcript->setFieldName($condition->getFieldName());
$transcript->setCondition($cond);
$transcript->setTestValue($test_value);
try {
$result = $object->doesConditionMatch(
$this,
$rule,
$condition,
$object_value);
} catch (HeraldInvalidConditionException $ex) {
$result = false;
$transcript->setNote($ex->getMessage());
}
$transcript->setResult($result);
$this->transcript->addConditionTranscript($transcript);
return $result;
}
protected function getConditionObjectValue(
HeraldCondition $condition,
HeraldAdapter $object) {
$field = $condition->getFieldName();
return $this->getObjectFieldValue($field);
}
public function getObjectFieldValue($field) {
if (isset($this->fieldCache[$field])) {
return $this->fieldCache[$field];
}
$result = $this->object->getHeraldField($field);
$this->fieldCache[$field] = $result;
return $result;
}
protected function getRuleEffects(
HeraldRule $rule,
HeraldAdapter $object) {
$effects = array();
foreach ($rule->getActions() as $action) {
$effect = new HeraldEffect();
$effect->setObjectPHID($object->getPHID());
$effect->setAction($action->getAction());
$effect->setTarget($action->getTarget());
$effect->setRuleID($rule->getID());
$effect->setRulePHID($rule->getPHID());
$name = $rule->getName();
$id = $rule->getID();
$effect->setReason(
pht(
'Conditions were met for %s',
"H{$id} {$name}"));
$effects[] = $effect;
}
return $effects;
}
private function canAuthorViewObject(
HeraldRule $rule,
HeraldAdapter $adapter) {
// Authorship is irrelevant for global rules and object rules.
if ($rule->isGlobalRule() || $rule->isObjectRule()) {
return true;
}
// The author must be able to create rules for the adapter's content type.
// In particular, this means that the application must be installed and
// accessible to the user. For example, if a user writes a Differential
// rule and then loses access to Differential, this disables the rule.
$enabled = HeraldAdapter::getEnabledAdapterMap($rule->getAuthor());
if (empty($enabled[$adapter->getAdapterContentType()])) {
return false;
}
// Finally, the author must be able to see the object itself. You can't
// write a personal rule that CC's you on revisions you wouldn't otherwise
// be able to see, for example.
$object = $adapter->getObject();
return PhabricatorPolicyFilter::hasCapability(
$rule->getAuthor(),
$object,
PhabricatorPolicyCapability::CAN_VIEW);
}
private function canRuleApplyToObject(
HeraldRule $rule,
HeraldAdapter $adapter) {
// Rules which are not object rules can apply to anything.
if (!$rule->isObjectRule()) {
return true;
}
$trigger_phid = $rule->getTriggerObjectPHID();
$object_phids = $adapter->getTriggerObjectPHIDs();
if ($object_phids) {
if (in_array($trigger_phid, $object_phids)) {
return true;
}
}
return false;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jun 9, 11:13 PM (9 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
140162
Default Alt Text
(49 KB)

Event Timeline