Index: src/__celerity_resource_map__.php =================================================================== --- src/__celerity_resource_map__.php +++ src/__celerity_resource_map__.php @@ -1211,7 +1211,7 @@ ), 'herald-rule-editor' => array( - 'uri' => '/res/928275b4/rsrc/js/application/herald/HeraldRuleEditor.js', + 'uri' => '/res/92c05b75/rsrc/js/application/herald/HeraldRuleEditor.js', 'type' => 'js', 'requires' => array( Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -756,6 +756,7 @@ 'HeraldObjectTranscript' => 'applications/herald/storage/transcript/HeraldObjectTranscript.php', 'HeraldPHIDTypeRule' => 'applications/herald/phid/HeraldPHIDTypeRule.php', 'HeraldPholioMockAdapter' => 'applications/herald/adapter/HeraldPholioMockAdapter.php', + 'HeraldPreCommitRefAdapter' => 'applications/diffusion/herald/HeraldPreCommitRefAdapter.php', 'HeraldRecursiveConditionsException' => 'applications/herald/engine/exception/HeraldRecursiveConditionsException.php', 'HeraldRepetitionPolicyConfig' => 'applications/herald/config/HeraldRepetitionPolicyConfig.php', 'HeraldRule' => 'applications/herald/storage/HeraldRule.php', @@ -3177,6 +3178,7 @@ 'HeraldNewController' => 'HeraldController', 'HeraldPHIDTypeRule' => 'PhabricatorPHIDType', 'HeraldPholioMockAdapter' => 'HeraldAdapter', + 'HeraldPreCommitRefAdapter' => 'HeraldAdapter', 'HeraldRecursiveConditionsException' => 'Exception', 'HeraldRule' => array( Index: src/applications/diffusion/engine/DiffusionCommitHookEngine.php =================================================================== --- src/applications/diffusion/engine/DiffusionCommitHookEngine.php +++ src/applications/diffusion/engine/DiffusionCommitHookEngine.php @@ -26,6 +26,8 @@ private $transactionKey; private $mercurialHook; + private $heraldViewerProjects; + /* -( Config )------------------------------------------------------------- */ @@ -130,7 +132,7 @@ throw $ex; } - // TODO: Fire ref herald rules. + $this->applyHeraldRefRules($ref_updates); $content_updates = $this->findContentUpdates($ref_updates); $all_updates = array_merge($all_updates, $content_updates); @@ -221,6 +223,84 @@ } +/* -( Herald )------------------------------------------------------------- */ + + + private function applyHeraldRefRules(array $ref_updates) { + if (!$ref_updates) { + return; + } + + $engine = new HeraldEngine(); + $rules = null; + $blocking_effect = null; + foreach ($ref_updates as $ref_update) { + $adapter = id(new HeraldPreCommitRefAdapter()) + ->setPushLog($ref_update) + ->setHookEngine($this); + + if ($rules === null) { + $rules = $engine->loadRulesForAdapter($adapter); + } + + $effects = $engine->applyRules($rules, $adapter); + $engine->applyEffects($effects, $adapter, $rules); + $xscript = $engine->getTranscript(); + + if ($blocking_effect === null) { + foreach ($effects as $effect) { + if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) { + $blocking_effect = $effect; + break; + } + } + } + } + + if ($blocking_effect) { + foreach ($ref_updates as $ref_update) { + $ref_update->setRejectCode(PhabricatorRepositoryPushLog::REJECT_HERALD); + $ref_update->setRejectDetails($blocking_effect->getRulePHID()); + } + + $message = $blocking_effect->getTarget(); + if (!strlen($message)) { + $message = pht('(None.)'); + } + + $rules = mpull($rules, null, 'getID'); + $rule = idx($rules, $effect->getRuleID()); + if ($rule && strlen($rule->getName())) { + $rule_name = $rule->getName(); + } else { + $rule_name = pht('Unnamed Herald Rule'); + } + + throw new DiffusionCommitHookRejectException( + pht( + "This commit was rejected by Herald pre-commit rule %s.\n". + "Rule: %s\n". + "Reason: %s", + 'H'.$blocking_effect->getRuleID(), + $rule_name, + $message)); + } + } + + public function loadViewerProjectPHIDsForHerald() { + // This just caches the viewer's projects so we don't need to load them + // over and over again when applying Herald rules. + if ($this->heraldViewerProjects === null) { + $this->heraldViewerProjects = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->withMemberPHIDs(array($this->getViewer()->getPHID())) + ->execute(); + } + + return mpull($this->heraldViewerProjects, 'getPHID'); + } + + /* -( Git )---------------------------------------------------------------- */ @@ -246,8 +326,10 @@ if (preg_match('(^refs/heads/)', $ref_raw)) { $ref_type = PhabricatorRepositoryPushLog::REFTYPE_BRANCH; + $ref_raw = substr($ref_raw, strlen('refs/heads/')); } else if (preg_match('(^refs/tags/)', $ref_raw)) { $ref_type = PhabricatorRepositoryPushLog::REFTYPE_TAG; + $ref_raw = substr($ref_raw, strlen('refs/tags/')); } else { throw new Exception( pht( @@ -764,7 +846,12 @@ // code. This indicates a broken hook, and covers the case where we // encounter some unexpected exception and consequently reject the changes. + // NOTE: We generate PHIDs up front so the Herald transcripts can pick them + // up. + $phid = id(new PhabricatorRepositoryPushLog())->generatePHID(); + return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer()) + ->setPHID($phid) ->attachRepository($this->getRepository()) ->setRepositoryPHID($this->getRepository()->getPHID()) ->setEpoch(time()) Index: src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php =================================================================== --- /dev/null +++ src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php @@ -0,0 +1,163 @@ +log = $log; + return $this; + } + + public function setHookEngine(DiffusionCommitHookEngine $engine) { + $this->hookEngine = $engine; + return $this; + } + + public function getAdapterApplicationClass() { + return 'PhabricatorApplicationDiffusion'; + } + + public function getObject() { + return $this->log; + } + + public function getAdapterContentName() { + return pht('Commit Hook: Branches/Tags/Bookmarks'); + } + + 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_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 getActions($rule_type) { + switch ($rule_type) { + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + return array( + self::ACTION_BLOCK, + self::ACTION_NOTHING + ); + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: + return array( + self::ACTION_NOTHING, + ); + } + } + + 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 getPHID() { + return $this->getObject()->getPHID(); + } + + public function getHeraldName() { + return pht('Push Log'); + } + + 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->hookEngine->getRepository()->getPHID(); + case self::FIELD_PUSHER: + return $this->hookEngine->getViewer()->getPHID(); + case self::FIELD_PUSHER_PROJECTS: + return $this->hookEngine->loadViewerProjectPHIDsForHerald(); + } + + return parent::getHeraldField($field); + } + + + public function applyHeraldEffects(array $effects) { + assert_instances_of($effects, 'HeraldEffect'); + + $result = array(); + foreach ($effects as $effect) { + $action = $effect->getAction(); + switch ($action) { + case self::ACTION_NOTHING: + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Did nothing.')); + break; + case self::ACTION_BLOCK: + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Blocked push.')); + break; + default: + throw new Exception(pht('No rules to handle action "%s"!', $action)); + } + } + + return $result; + } + +} Index: src/applications/herald/adapter/HeraldAdapter.php =================================================================== --- src/applications/herald/adapter/HeraldAdapter.php +++ src/applications/herald/adapter/HeraldAdapter.php @@ -25,6 +25,8 @@ const FIELD_ALWAYS = 'always'; const FIELD_AUTHOR_PROJECTS = 'authorprojects'; const FIELD_PROJECTS = 'projects'; + const FIELD_PUSHER = 'pusher'; + const FIELD_PUSHER_PROJECTS = 'pusher-projects'; const CONDITION_CONTAINS = 'contains'; const CONDITION_NOT_CONTAINS = '!contains'; @@ -44,6 +46,8 @@ const CONDITION_NOT_EXISTS = '!exists'; const CONDITION_UNCONDITIONALLY = 'unconditionally'; const CONDITION_REGEXP_PAIR = 'regexp-pair'; + const CONDITION_HAS_BIT = 'bit'; + const CONDITION_NOT_BIT = '!bit'; const ACTION_ADD_CC = 'addcc'; const ACTION_REMOVE_CC = 'remcc'; @@ -56,6 +60,7 @@ const ACTION_ADD_REVIEWERS = 'addreviewers'; const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers'; const ACTION_APPLY_BUILD_PLANS = 'applybuildplans'; + const ACTION_BLOCK = 'block'; const VALUE_TEXT = 'text'; const VALUE_NONE = 'none'; @@ -156,6 +161,8 @@ self::FIELD_ALWAYS => pht('Always'), self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"), self::FIELD_PROJECTS => pht("Projects"), + self::FIELD_PUSHER => pht('Pusher'), + self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"), ); } @@ -183,6 +190,8 @@ self::CONDITION_NOT_EXISTS => pht('does not exist'), self::CONDITION_UNCONDITIONALLY => '', // don't show anything! self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'), + self::CONDITION_HAS_BIT => pht('has bit'), + self::CONDITION_NOT_BIT => pht('lacks bit'), ); } @@ -200,6 +209,7 @@ case self::FIELD_AUTHOR: case self::FIELD_COMMITTER: case self::FIELD_REVIEWER: + case self::FIELD_PUSHER: return array( self::CONDITION_IS_ANY, self::CONDITION_IS_NOT_ANY, @@ -218,6 +228,7 @@ case self::FIELD_PROJECTS: case self::FIELD_AFFECTED_PACKAGE: case self::FIELD_AFFECTED_PACKAGE_OWNER: + case self::FIELD_PUSHER_PROJECTS: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, @@ -392,6 +403,10 @@ $result = !$result; } return $result; + case self::CONDITION_HAS_BIT: + return (($condition_value & $field_value) === $condition_value); + case self::CONDITION_NOT_BIT: + return (($condition_value & $field_value) !== $condition_value); default: throw new HeraldInvalidConditionException( "Unknown condition '{$condition_type}'."); @@ -469,6 +484,8 @@ case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: + case self::CONDITION_HAS_BIT: + case self::CONDITION_NOT_BIT: // No explicit validation for these types, although there probably // should be in some cases. break; @@ -500,6 +517,7 @@ self::ACTION_ADD_REVIEWERS => pht('Add reviewers'), self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'), self::ACTION_APPLY_BUILD_PLANS => pht('Apply build plans'), + self::ACTION_BLOCK => pht('Block change with message'), ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( @@ -551,6 +569,7 @@ $target = PhabricatorFlagColor::COLOR_BLUE; } break; + case self::ACTION_BLOCK: case self::ACTION_NOTHING: break; default: @@ -607,6 +626,7 @@ case self::FIELD_AFFECTED_PACKAGE: return self::VALUE_OWNERS_PACKAGE; case self::FIELD_AUTHOR_PROJECTS: + case self::FIELD_PUSHER_PROJECTS: case self::FIELD_PROJECTS: return self::VALUE_PROJECT; case self::FIELD_REVIEWERS: @@ -670,6 +690,8 @@ return self::VALUE_USER_OR_PROJECT; case self::ACTION_APPLY_BUILD_PLANS: return self::VALUE_BUILD_PLAN; + case self::ACTION_BLOCK: + return self::VALUE_TEXT; default: throw new Exception("Unknown or invalid action '{$action}'."); } Index: src/applications/herald/controller/HeraldRuleController.php =================================================================== --- src/applications/herald/controller/HeraldRuleController.php +++ src/applications/herald/controller/HeraldRuleController.php @@ -349,6 +349,7 @@ switch ($action->getAction()) { case HeraldAdapter::ACTION_FLAG: + case HeraldAdapter::ACTION_BLOCK: $current_value = $action->getTarget(); break; default: @@ -414,12 +415,42 @@ '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, - 'colors' => PhabricatorFlagColor::getColorNameMap(), - 'defaultColor' => PhabricatorFlagColor::COLOR_BLUE, - 'contentSources' => PhabricatorContentSource::getSourceNameMap(), - 'defaultSource' => PhabricatorContentSource::SOURCE_WEB ), 'author' => array($rule->getAuthorPHID() => $handles[$rule->getAuthorPHID()]->getName()), Index: src/applications/herald/controller/HeraldTranscriptController.php =================================================================== --- src/applications/herald/controller/HeraldTranscriptController.php +++ src/applications/herald/controller/HeraldTranscriptController.php @@ -293,6 +293,10 @@ case HeraldAdapter::ACTION_FLAG: $target = PhabricatorFlagColor::getColorName($target); break; + case HeraldAdapter::ACTION_BLOCK: + // Target is a text string. + $target = $target; + break; default: if ($target) { foreach ($target as $k => $phid) { Index: src/applications/herald/engine/HeraldEngine.php =================================================================== --- src/applications/herald/engine/HeraldEngine.php +++ src/applications/herald/engine/HeraldEngine.php @@ -24,8 +24,8 @@ return idx($this->rules, $id); } - public static function loadAndApplyRules(HeraldAdapter $adapter) { - $rules = id(new HeraldRuleQuery()) + public function loadRulesForAdapter(HeraldAdapter $adapter) { + return id(new HeraldRuleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDisabled(false) ->withContentTypes(array($adapter->getAdapterContentType())) @@ -33,8 +33,12 @@ ->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); Index: src/applications/herald/storage/HeraldRule.php =================================================================== --- src/applications/herald/storage/HeraldRule.php +++ src/applications/herald/storage/HeraldRule.php @@ -16,9 +16,10 @@ protected $ruleType; protected $isDisabled = 0; - protected $configVersion = 15; + protected $configVersion = 16; - private $ruleApplied = self::ATTACHABLE; // phids for which this rule has been applied + // phids for which this rule has been applied + private $ruleApplied = self::ATTACHABLE; private $validAuthor = self::ATTACHABLE; private $author = self::ATTACHABLE; private $conditions; Index: src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php =================================================================== --- src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php +++ src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php @@ -4,6 +4,7 @@ extends PhabricatorCursorPagedPolicyAwareQuery { private $ids; + private $phids; private $repositoryPHIDs; private $pusherPHIDs; private $refTypes; @@ -14,6 +15,11 @@ return $this; } + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + public function withRepositoryPHIDs(array $repository_phids) { $this->repositoryPHIDs = $repository_phids; return $this; @@ -84,6 +90,13 @@ $this->ids); } + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + if ($this->repositoryPHIDs) { $where[] = qsprintf( $conn_r, Index: webroot/rsrc/js/application/herald/HeraldRuleEditor.js =================================================================== --- webroot/rsrc/js/application/herald/HeraldRuleEditor.js +++ webroot/rsrc/js/application/herald/HeraldRuleEditor.js @@ -233,16 +233,13 @@ set_fn = JX.bag; break; case 'contentsource': - input = this._renderSelect(this._config.template.contentSources); - get_fn = function() { return input.value; }; - set_fn = function(v) { input.value = v; }; - set_fn(this._config.template.defaultSource); - break; case 'flagcolor': - input = this._renderSelect(this._config.template.colors); + case 'value-ref-type': + case 'value-ref-change': + input = this._renderSelect(this._config.select[type].options); get_fn = function() { return input.value; }; set_fn = function(v) { input.value = v; }; - set_fn(this._config.template.defaultColor); + set_fn(this._config.select[type]['default']); break; default: input = JX.$N('input', {type: 'text'});