diff --git a/bin/herald b/bin/herald new file mode 120000 --- /dev/null +++ b/bin/herald @@ -0,0 +1 @@ +../scripts/setup/manage_herald.php \ No newline at end of file diff --git a/scripts/setup/manage_herald.php b/scripts/setup/manage_herald.php new file mode 100755 --- /dev/null +++ b/scripts/setup/manage_herald.php @@ -0,0 +1,21 @@ +#!/usr/bin/env php +setTagline(pht('manage Herald')); +$args->setSynopsis(<<parseStandardArguments(); + +$workflows = id(new PhutilClassMapQuery()) + ->setAncestorClass('HeraldManagementWorkflow') + ->execute(); +$workflows[] = new PhutilHelpArgumentWorkflow(); +$args->parseWorkflows($workflows); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1488,6 +1488,7 @@ 'HeraldInvalidConditionException' => 'applications/herald/engine/exception/HeraldInvalidConditionException.php', 'HeraldMailableState' => 'applications/herald/state/HeraldMailableState.php', 'HeraldManageGlobalRulesCapability' => 'applications/herald/capability/HeraldManageGlobalRulesCapability.php', + 'HeraldManagementWorkflow' => 'applications/herald/management/HeraldManagementWorkflow.php', 'HeraldManiphestTaskAdapter' => 'applications/maniphest/herald/HeraldManiphestTaskAdapter.php', 'HeraldNewController' => 'applications/herald/controller/HeraldNewController.php', 'HeraldNewObjectField' => 'applications/herald/field/HeraldNewObjectField.php', @@ -1537,6 +1538,7 @@ 'HeraldSupportActionGroup' => 'applications/herald/action/HeraldSupportActionGroup.php', 'HeraldSupportFieldGroup' => 'applications/herald/field/HeraldSupportFieldGroup.php', 'HeraldTestConsoleController' => 'applications/herald/controller/HeraldTestConsoleController.php', + 'HeraldTestManagementWorkflow' => 'applications/herald/management/HeraldTestManagementWorkflow.php', 'HeraldTextFieldValue' => 'applications/herald/value/HeraldTextFieldValue.php', 'HeraldTokenizerFieldValue' => 'applications/herald/value/HeraldTokenizerFieldValue.php', 'HeraldTransactionQuery' => 'applications/herald/query/HeraldTransactionQuery.php', @@ -6975,6 +6977,7 @@ 'HeraldInvalidConditionException' => 'Exception', 'HeraldMailableState' => 'HeraldState', 'HeraldManageGlobalRulesCapability' => 'PhabricatorPolicyCapability', + 'HeraldManagementWorkflow' => 'PhabricatorManagementWorkflow', 'HeraldManiphestTaskAdapter' => 'HeraldAdapter', 'HeraldNewController' => 'HeraldController', 'HeraldNewObjectField' => 'HeraldField', @@ -7031,6 +7034,7 @@ 'HeraldSupportActionGroup' => 'HeraldActionGroup', 'HeraldSupportFieldGroup' => 'HeraldFieldGroup', 'HeraldTestConsoleController' => 'HeraldController', + 'HeraldTestManagementWorkflow' => 'HeraldManagementWorkflow', 'HeraldTextFieldValue' => 'HeraldFieldValue', 'HeraldTokenizerFieldValue' => 'HeraldFieldValue', 'HeraldTransactionQuery' => 'PhabricatorApplicationTransactionQuery', diff --git a/src/applications/herald/management/HeraldManagementWorkflow.php b/src/applications/herald/management/HeraldManagementWorkflow.php new file mode 100644 --- /dev/null +++ b/src/applications/herald/management/HeraldManagementWorkflow.php @@ -0,0 +1,4 @@ +setName('test') + ->setExamples('**test** --object __object__ --type __type__') + ->setSynopsis( + pht( + 'Test content rules for an object. Executes a dry run, like the '. + 'web UI test console.')) + ->setArguments( + array( + array( + 'name' => 'object', + 'param' => 'object', + 'help' => pht('Run rules on this object.'), + ), + array( + 'name' => 'type', + 'param' => 'type', + 'help' => pht('Run rules for this content type.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $object_name = $args->getArg('object'); + if (!strlen($object_name)) { + throw new PhutilArgumentUsageException( + pht('Specify an object to test rules for with "--object".')); + } + + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withNames(array($object_name)) + ->execute(); + if (!$objects) { + throw new PhutilArgumentUsageException( + pht( + 'Unable to load specified object ("%s").', + $object_name)); + } + $object = head($objects); + + $adapters = HeraldAdapter::getAllAdapters(); + + $can_select = array(); + $display_adapters = array(); + foreach ($adapters as $key => $adapter) { + if (!$adapter->isTestAdapterForObject($object)) { + continue; + } + + if (!$adapter->isAvailableToUser($viewer)) { + continue; + } + + $display_adapters[$key] = $adapter; + + if ($adapter->canCreateTestAdapterForObject($object)) { + $can_select[$key] = $adapter; + } + } + + + $content_type = $args->getArg('type'); + if (!strlen($content_type)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a content type to run rules for. For this object, valid '. + 'content types are: %s.', + implode(', ', array_keys($can_select)))); + } + + if (!isset($can_select[$content_type])) { + if (!isset($display_adapters[$content_type])) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a content type to run rules for. The specified content '. + 'type ("%s") is not valid. For this object, valid content types '. + 'are: %s.', + $content_type, + implode(', ', array_keys($can_select)))); + } else { + throw new PhutilArgumentUsageException( + pht( + 'The specified content type ("%s") does not support dry runs. '. + 'Choose a testable content type. For this object, valid content '. + 'types are: %s.', + $content_type, + implode(', ', array_keys($can_select)))); + } + } + + $adapter = $can_select[$content_type]->newTestAdapter( + $viewer, + $object); + + $content_source = $this->newContentSource(); + + $adapter + ->setContentSource($content_source) + ->setIsNewObject(false) + ->setViewer($viewer); + + $rules = id(new HeraldRuleQuery()) + ->setViewer($viewer) + ->withContentTypes(array($adapter->getAdapterContentType())) + ->withDisabled(false) + ->needConditionsAndActions(true) + ->needAppliedToPHIDs(array($object->getPHID())) + ->needValidateAuthors(true) + ->execute(); + + $engine = id(new HeraldEngine()) + ->setDryRun(true); + + $effects = $engine->applyRules($rules, $adapter); + $engine->applyEffects($effects, $adapter, $rules); + + $xscript = $engine->getTranscript(); + + $uri = '/herald/transcript/'.$xscript->getID().'/'; + $uri = PhabricatorEnv::getProductionURI($uri); + + echo tsprintf( + "%s\n\n __%s__\n\n", + pht('Test run complete. Transcript:'), + $uri); + + return 0; + } + +} diff --git a/src/applications/herald/query/HeraldTranscriptQuery.php b/src/applications/herald/query/HeraldTranscriptQuery.php --- a/src/applications/herald/query/HeraldTranscriptQuery.php +++ b/src/applications/herald/query/HeraldTranscriptQuery.php @@ -30,36 +30,35 @@ protected function loadPage() { $transcript = new HeraldTranscript(); - $conn_r = $transcript->establishConnection('r'); + $conn = $transcript->establishConnection('r'); // NOTE: Transcripts include a potentially enormous amount of serialized // data, so we're loading only some of the fields here if the caller asked // for partial records. if ($this->needPartialRecords) { - $fields = implode( - ', ', - array( - 'id', - 'phid', - 'objectPHID', - 'time', - 'duration', - 'dryRun', - 'host', - )); + $fields = array( + 'id', + 'phid', + 'objectPHID', + 'time', + 'duration', + 'dryRun', + 'host', + ); + $fields = qsprintf($conn, '%LC', $fields); } else { - $fields = '*'; + $fields = qsprintf($conn, '*'); } $rows = queryfx_all( - $conn_r, + $conn, 'SELECT %Q FROM %T t %Q %Q %Q', $fields, $transcript->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); + $this->buildWhereClause($conn), + $this->buildOrderClause($conn), + $this->buildLimitClause($conn)); $transcripts = $transcript->loadAllFromArray($rows);