diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php index aeff27ad1..d0472bdaa 100644 --- a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php +++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php @@ -1,400 +1,420 @@ 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: 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 supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return true; case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: - case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: default: return false; } } + public function canTriggerOnObject($object) { + if ($object instanceof PhabricatorRepository) { + return true; + } + return false; + } + + public function explainValidTriggerObjects() { + return pht( + 'This rule can trigger for **repositories**.'); + } + + public function getTriggerObjectPHIDs() { + return array( + $this->hookEngine->getRepository()->getPHID(), + $this->getPHID(), + ); + } + public function getFieldNameMap() { return array( ) + parent::getFieldNameMap(); } 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_REPOSITORY, 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 getConditionsForField($field) { switch ($field) { } return parent::getConditionsForField($field); } public function getActions($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return array( self::ACTION_BLOCK, self::ACTION_NOTHING ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( self::ACTION_NOTHING, ); } } public function getValueTypeForFieldAndCondition($field, $condition) { 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_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_REPOSITORY: return $this->hookEngine->getRepository()->getPHID(); case self::FIELD_PUSHER: return $this->hookEngine->getViewer()->getPHID(); case self::FIELD_PUSHER_PROJECTS: return $this->hookEngine->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); } 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; } private function getDiffContent($type) { if ($this->changesets === null) { try { $this->changesets = $this->hookEngine->loadChangesetsForCommit( $this->log->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->hookEngine->loadCommitRefForCommit( $this->log->getRefNew()); } return $this->commitRef; } private function getAuthorPHID() { $repository = $this->hookEngine->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->hookEngine->getViewer()->getPHID(); } } private function getCommitterPHID() { $repository = $this->hookEngine->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->hookEngine->getViewer()->getPHID(); } } private function getAuthorRaw() { $repository = $this->hookEngine->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->hookEngine->getViewer()->getUsername(); } } private function getCommitterRaw() { $repository = $this->hookEngine->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->hookEngine->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->hookEngine->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->hookEngine->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $parents = id(new DiffusionLowLevelParentsQuery()) ->setRepository($repository) ->withIdentifier($this->log->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->hookEngine->loadBranches($this->log->getRefNew()); } } diff --git a/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php index 90f674857..dbbf6f659 100644 --- a/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php +++ b/src/applications/diffusion/herald/HeraldPreCommitRefAdapter.php @@ -1,184 +1,204 @@ 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 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 supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return true; case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: - case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: default: return false; } } + public function canTriggerOnObject($object) { + if ($object instanceof PhabricatorRepository) { + return true; + } + return false; + } + + public function explainValidTriggerObjects() { + return pht( + 'This rule can trigger for **repositories**.'); + } + + public function getTriggerObjectPHIDs() { + return array( + $this->hookEngine->getRepository()->getPHID(), + $this->getPHID(), + ); + } + 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: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: 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; } } diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index 5735321b4..1a2fc9a37 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -1,1009 +1,1027 @@ contentSource = $content_source; return $this; } public function getContentSource() { return $this->contentSource; } abstract public function getPHID(); abstract public function getHeraldName(); public function getHeraldField($field_name) { switch ($field_name) { case self::FIELD_RULE: return null; case self::FIELD_CONTENT_SOURCE: return $this->getContentSource()->getSource(); case self::FIELD_ALWAYS: return true; default: throw new Exception( "Unknown field '{$field_name}'!"); } } abstract public function applyHeraldEffects(array $effects); public function isAvailableToUser(PhabricatorUser $viewer) { $applications = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withInstalled(true) ->withClasses(array($this->getAdapterApplicationClass())) ->execute(); return !empty($applications); } /** * NOTE: You generally should not override this; it exists to support legacy * adapters which had hard-coded content types. */ public function getAdapterContentType() { return get_class($this); } abstract public function getAdapterContentName(); abstract public function getAdapterContentDescription(); abstract public function getAdapterApplicationClass(); abstract public function getObject(); public function supportsRuleType($rule_type) { return false; } + public function canTriggerOnObject($object) { + return false; + } + + public function explainValidTriggerObjects() { + return pht('This adapter can not trigger on objects.'); + } + + public function getTriggerObjectPHIDs() { + return array($this->getPHID()); + } + public function getAdapterSortKey() { return sprintf( '%08d%s', $this->getAdapterSortOrder(), $this->getAdapterContentName()); } public function getAdapterSortOrder() { return 1000; } /* -( Fields )------------------------------------------------------------- */ public function getFields() { return array( self::FIELD_ALWAYS, ); } public function getFieldNameMap() { return array( self::FIELD_TITLE => pht('Title'), self::FIELD_BODY => pht('Body'), self::FIELD_AUTHOR => pht('Author'), self::FIELD_ASSIGNEE => pht('Assignee'), self::FIELD_COMMITTER => pht('Committer'), self::FIELD_REVIEWER => pht('Reviewer'), self::FIELD_REVIEWERS => pht('Reviewers'), self::FIELD_CC => pht('CCs'), self::FIELD_TAGS => pht('Tags'), self::FIELD_DIFF_FILE => pht('Any changed filename'), self::FIELD_DIFF_CONTENT => pht('Any changed file content'), self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'), self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'), self::FIELD_REPOSITORY => pht('Repository'), self::FIELD_RULE => pht('Another Herald rule'), self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'), self::FIELD_AFFECTED_PACKAGE_OWNER => pht("Any affected package's owner"), self::FIELD_CONTENT_SOURCE => pht('Content Source'), 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"), self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'), self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'), self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'), self::FIELD_DIFFERENTIAL_ACCEPTED => pht('Accepted Differential revision'), self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'), self::FIELD_BRANCHES => pht('Commit\'s branches'), self::FIELD_AUTHOR_RAW => pht('Raw author name'), self::FIELD_COMMITTER_RAW => pht('Raw committer name'), ); } /* -( Conditions )--------------------------------------------------------- */ public function getConditionNameMap() { return array( self::CONDITION_CONTAINS => pht('contains'), self::CONDITION_NOT_CONTAINS => pht('does not contain'), self::CONDITION_IS => pht('is'), self::CONDITION_IS_NOT => pht('is not'), self::CONDITION_IS_ANY => pht('is any of'), self::CONDITION_IS_TRUE => pht('is true'), self::CONDITION_IS_FALSE => pht('is false'), self::CONDITION_IS_NOT_ANY => pht('is not any of'), self::CONDITION_INCLUDE_ALL => pht('include all of'), self::CONDITION_INCLUDE_ANY => pht('include any of'), self::CONDITION_INCLUDE_NONE => pht('do not include'), self::CONDITION_IS_ME => pht('is myself'), self::CONDITION_IS_NOT_ME => pht('is not myself'), self::CONDITION_REGEXP => pht('matches regexp'), self::CONDITION_RULE => pht('matches:'), self::CONDITION_NOT_RULE => pht('does not match:'), self::CONDITION_EXISTS => pht('exists'), 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'), ); } public function getConditionsForField($field) { switch ($field) { case self::FIELD_TITLE: case self::FIELD_BODY: case self::FIELD_COMMITTER_RAW: case self::FIELD_AUTHOR_RAW: return array( self::CONDITION_CONTAINS, self::CONDITION_NOT_CONTAINS, self::CONDITION_IS, self::CONDITION_IS_NOT, self::CONDITION_REGEXP, ); 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, ); case self::FIELD_REPOSITORY: case self::FIELD_ASSIGNEE: return array( self::CONDITION_IS_ANY, self::CONDITION_IS_NOT_ANY, self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_TAGS: case self::FIELD_REVIEWERS: case self::FIELD_CC: case self::FIELD_AUTHOR_PROJECTS: 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, self::CONDITION_INCLUDE_NONE, self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_DIFF_FILE: case self::FIELD_BRANCHES: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, ); case self::FIELD_DIFF_CONTENT: case self::FIELD_DIFF_ADDED_CONTENT: case self::FIELD_DIFF_REMOVED_CONTENT: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, self::CONDITION_REGEXP_PAIR, ); case self::FIELD_RULE: return array( self::CONDITION_RULE, self::CONDITION_NOT_RULE, ); case self::FIELD_CONTENT_SOURCE: return array( self::CONDITION_IS, self::CONDITION_IS_NOT, ); case self::FIELD_ALWAYS: return array( self::CONDITION_UNCONDITIONALLY, ); case self::FIELD_DIFFERENTIAL_REVIEWERS: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFFERENTIAL_CCS: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFFERENTIAL_REVISION: case self::FIELD_DIFFERENTIAL_ACCEPTED: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_IS_MERGE_COMMIT: return array( self::CONDITION_IS_TRUE, self::CONDITION_IS_FALSE, ); default: throw new Exception( "This adapter does not define conditions for field '{$field}'!"); } } public function doesConditionMatch( HeraldEngine $engine, HeraldRule $rule, HeraldCondition $condition, $field_value) { $condition_type = $condition->getFieldCondition(); $condition_value = $condition->getValue(); switch ($condition_type) { case self::CONDITION_CONTAINS: // "Contains" can take an array of strings, as in "Any changed // filename" for diffs. foreach ((array)$field_value as $value) { if (stripos($value, $condition_value) !== false) { return true; } } return false; case self::CONDITION_NOT_CONTAINS: return (stripos($field_value, $condition_value) === false); case self::CONDITION_IS: return ($field_value == $condition_value); case self::CONDITION_IS_NOT: return ($field_value != $condition_value); case self::CONDITION_IS_ME: return ($field_value == $rule->getAuthorPHID()); case self::CONDITION_IS_NOT_ME: return ($field_value != $rule->getAuthorPHID()); case self::CONDITION_IS_ANY: if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( "Expected condition value to be an array."); } $condition_value = array_fuse($condition_value); return isset($condition_value[$field_value]); case self::CONDITION_IS_NOT_ANY: if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( "Expected condition value to be an array."); } $condition_value = array_fuse($condition_value); return !isset($condition_value[$field_value]); case self::CONDITION_INCLUDE_ALL: if (!is_array($field_value)) { throw new HeraldInvalidConditionException( "Object produced non-array value!"); } if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( "Expected condition value to be an array."); } $have = array_select_keys(array_fuse($field_value), $condition_value); return (count($have) == count($condition_value)); case self::CONDITION_INCLUDE_ANY: return (bool)array_select_keys( array_fuse($field_value), $condition_value); case self::CONDITION_INCLUDE_NONE: return !array_select_keys( array_fuse($field_value), $condition_value); case self::CONDITION_EXISTS: case self::CONDITION_IS_TRUE: return (bool)$field_value; case self::CONDITION_NOT_EXISTS: case self::CONDITION_IS_FALSE: return !$field_value; case self::CONDITION_UNCONDITIONALLY: return (bool)$field_value; case self::CONDITION_REGEXP: foreach ((array)$field_value as $value) { // We add the 'S' flag because we use the regexp multiple times. // It shouldn't cause any troubles if the flag is already there // - /.*/S is evaluated same as /.*/SS. $result = @preg_match($condition_value . 'S', $value); if ($result === false) { throw new HeraldInvalidConditionException( "Regular expression is not valid!"); } if ($result) { return true; } } return false; case self::CONDITION_REGEXP_PAIR: // Match a JSON-encoded pair of regular expressions against a // dictionary. The first regexp must match the dictionary key, and the // second regexp must match the dictionary value. If any key/value pair // in the dictionary matches both regexps, the condition is satisfied. $regexp_pair = json_decode($condition_value, true); if (!is_array($regexp_pair)) { throw new HeraldInvalidConditionException( "Regular expression pair is not valid JSON!"); } if (count($regexp_pair) != 2) { throw new HeraldInvalidConditionException( "Regular expression pair is not a pair!"); } $key_regexp = array_shift($regexp_pair); $value_regexp = array_shift($regexp_pair); foreach ((array)$field_value as $key => $value) { $key_matches = @preg_match($key_regexp, $key); if ($key_matches === false) { throw new HeraldInvalidConditionException( "First regular expression is invalid!"); } if ($key_matches) { $value_matches = @preg_match($value_regexp, $value); if ($value_matches === false) { throw new HeraldInvalidConditionException( "Second regular expression is invalid!"); } if ($value_matches) { return true; } } } return false; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: $rule = $engine->getRule($condition_value); if (!$rule) { throw new HeraldInvalidConditionException( "Condition references a rule which does not exist!"); } $is_not = ($condition_type == self::CONDITION_NOT_RULE); $result = $engine->doesRuleMatch($rule, $this); if ($is_not) { $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}'."); } } public function willSaveCondition(HeraldCondition $condition) { $condition_type = $condition->getFieldCondition(); $condition_value = $condition->getValue(); switch ($condition_type) { case self::CONDITION_REGEXP: $ok = @preg_match($condition_value, ''); if ($ok === false) { throw new HeraldInvalidConditionException( pht( 'The regular expression "%s" is not valid. Regular expressions '. 'must have enclosing characters (e.g. "@/path/to/file@", not '. '"/path/to/file") and be syntactically correct.', $condition_value)); } break; case self::CONDITION_REGEXP_PAIR: $json = json_decode($condition_value, true); if (!is_array($json)) { throw new HeraldInvalidConditionException( pht( 'The regular expression pair "%s" is not valid JSON. Enter a '. 'valid JSON array with two elements.', $condition_value)); } if (count($json) != 2) { throw new HeraldInvalidConditionException( pht( 'The regular expression pair "%s" must have exactly two '. 'elements.', $condition_value)); } $key_regexp = array_shift($json); $val_regexp = array_shift($json); $key_ok = @preg_match($key_regexp, ''); if ($key_ok === false) { throw new HeraldInvalidConditionException( pht( 'The first regexp in the regexp pair, "%s", is not a valid '. 'regexp.', $key_regexp)); } $val_ok = @preg_match($val_regexp, ''); if ($val_ok === false) { throw new HeraldInvalidConditionException( pht( 'The second regexp in the regexp pair, "%s", is not a valid '. 'regexp.', $val_regexp)); } break; case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: case self::CONDITION_IS: case self::CONDITION_IS_NOT: case self::CONDITION_IS_ANY: case self::CONDITION_IS_NOT_ANY: case self::CONDITION_INCLUDE_ALL: case self::CONDITION_INCLUDE_ANY: case self::CONDITION_INCLUDE_NONE: case self::CONDITION_IS_ME: case self::CONDITION_IS_NOT_ME: case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: case self::CONDITION_HAS_BIT: case self::CONDITION_NOT_BIT: case self::CONDITION_IS_TRUE: case self::CONDITION_IS_FALSE: // No explicit validation for these types, although there probably // should be in some cases. break; default: throw new HeraldInvalidConditionException( pht( 'Unknown condition "%s"!', $condition_type)); } } /* -( Actions )------------------------------------------------------------ */ abstract public function getActions($rule_type); public function getActionNameMap($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return array( self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_ADD_CC => pht('Add emails to CC'), self::ACTION_REMOVE_CC => pht('Remove emails from CC'), self::ACTION_EMAIL => pht('Send an email to'), self::ACTION_AUDIT => pht('Trigger an Audit by'), self::ACTION_FLAG => pht('Mark with flag'), self::ACTION_ASSIGN_TASK => pht('Assign task to'), self::ACTION_ADD_PROJECTS => pht('Add projects'), 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( self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_ADD_CC => pht('Add me to CC'), self::ACTION_REMOVE_CC => pht('Remove me from CC'), self::ACTION_EMAIL => pht('Send me an email'), self::ACTION_AUDIT => pht('Trigger an Audit by me'), self::ACTION_FLAG => pht('Mark with flag'), self::ACTION_ASSIGN_TASK => pht('Assign task to me'), self::ACTION_ADD_PROJECTS => pht('Add projects'), self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'), self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add me as a blocking reviewer'), ); default: throw new Exception("Unknown rule type '{$rule_type}'!"); } } public function willSaveAction( HeraldRule $rule, HeraldAction $action) { $target = $action->getTarget(); if (is_array($target)) { $target = array_keys($target); } $author_phid = $rule->getAuthorPHID(); $rule_type = $rule->getRuleType(); if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) { switch ($action->getAction()) { case self::ACTION_EMAIL: case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_AUDIT: case self::ACTION_ASSIGN_TASK: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: // For personal rules, force these actions to target the rule owner. $target = array($author_phid); break; case self::ACTION_FLAG: // Make sure flag color is valid; set to blue if not. $color_map = PhabricatorFlagColor::getColorNameMap(); if (empty($color_map[$target])) { $target = PhabricatorFlagColor::COLOR_BLUE; } break; case self::ACTION_BLOCK: case self::ACTION_NOTHING: break; default: throw new HeraldInvalidActionException( pht( 'Unrecognized action type "%s"!', $action->getAction())); } } $action->setTarget($target); } /* -( Values )------------------------------------------------------------- */ public function getValueTypeForFieldAndCondition($field, $condition) { switch ($condition) { case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: case self::CONDITION_REGEXP: case self::CONDITION_REGEXP_PAIR: return self::VALUE_TEXT; case self::CONDITION_IS: case self::CONDITION_IS_NOT: switch ($field) { case self::FIELD_CONTENT_SOURCE: return self::VALUE_CONTENT_SOURCE; default: return self::VALUE_TEXT; } break; case self::CONDITION_IS_ANY: case self::CONDITION_IS_NOT_ANY: switch ($field) { case self::FIELD_REPOSITORY: return self::VALUE_REPOSITORY; default: return self::VALUE_USER; } break; case self::CONDITION_INCLUDE_ALL: case self::CONDITION_INCLUDE_ANY: case self::CONDITION_INCLUDE_NONE: switch ($field) { case self::FIELD_REPOSITORY: return self::VALUE_REPOSITORY; case self::FIELD_CC: return self::VALUE_EMAIL; case self::FIELD_TAGS: return self::VALUE_TAG; 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: return self::VALUE_USER_OR_PROJECT; default: return self::VALUE_USER; } break; case self::CONDITION_IS_ME: case self::CONDITION_IS_NOT_ME: case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: case self::CONDITION_IS_TRUE: case self::CONDITION_IS_FALSE: return self::VALUE_NONE; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: return self::VALUE_RULE; default: throw new Exception("Unknown condition '{$condition}'."); } } public static function getValueTypeForAction($action, $rule_type) { $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); if ($is_personal) { switch ($action) { case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_EMAIL: case self::ACTION_NOTHING: case self::ACTION_AUDIT: case self::ACTION_ASSIGN_TASK: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: return self::VALUE_NONE; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; case self::ACTION_ADD_PROJECTS: return self::VALUE_PROJECT; default: throw new Exception("Unknown or invalid action '{$action}'."); } } else { switch ($action) { case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_EMAIL: return self::VALUE_EMAIL; case self::ACTION_NOTHING: return self::VALUE_NONE; case self::ACTION_ADD_PROJECTS: return self::VALUE_PROJECT; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; case self::ACTION_ASSIGN_TASK: return self::VALUE_USER; case self::ACTION_AUDIT: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: 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}'."); } } } /* -( Repetition )--------------------------------------------------------- */ public function getRepetitionOptions() { return array( HeraldRepetitionPolicyConfig::EVERY, ); } public static function applyFlagEffect(HeraldEffect $effect, $phid) { $color = $effect->getTarget(); // TODO: Silly that we need to load this again here. $rule = id(new HeraldRule())->load($effect->getRuleID()); $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $rule->getAuthorPHID()); $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid); if ($flag) { return new HeraldApplyTranscript( $effect, false, pht('Object already flagged.')); } $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($phid)) ->executeOne(); $flag = new PhabricatorFlag(); $flag->setOwnerPHID($user->getPHID()); $flag->setType($handle->getType()); $flag->setObjectPHID($handle->getPHID()); // TOOD: Should really be transcript PHID, but it doesn't exist yet. $flag->setReasonPHID($user->getPHID()); $flag->setColor($color); $flag->setNote( pht('Flagged by Herald Rule "%s".', $rule->getName())); $flag->save(); return new HeraldApplyTranscript( $effect, true, pht('Added flag.')); } public static function getAllAdapters() { static $adapters; if (!$adapters) { $adapters = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $adapters = msort($adapters, 'getAdapterSortKey'); } return $adapters; } public static function getAdapterForContentType($content_type) { $adapters = self::getAllAdapters(); foreach ($adapters as $adapter) { if ($adapter->getAdapterContentType() == $content_type) { return $adapter; } } throw new Exception( pht( 'No adapter exists for Herald content type "%s".', $content_type)); } public static function getEnabledAdapterMap(PhabricatorUser $viewer) { $map = array(); $adapters = HeraldAdapter::getAllAdapters(); foreach ($adapters as $adapter) { if (!$adapter->isAvailableToUser($viewer)) { continue; } $type = $adapter->getAdapterContentType(); $name = $adapter->getAdapterContentName(); $map[$type] = $name; } return $map; } public function renderRuleAsText(HeraldRule $rule, array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $out = array(); if ($rule->getMustMatchAll()) { $out[] = pht('When all of these conditions are met:'); } else { $out[] = pht('When any of these conditions are met:'); } $out[] = null; foreach ($rule->getConditions() as $condition) { $out[] = $this->renderConditionAsText($condition, $handles); } $out[] = null; $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt( HeraldRepetitionPolicyConfig::EVERY); if ($rule->getRepetitionPolicy() == $integer_code_for_every) { $out[] = pht('Take these actions every time this rule matches:'); } else { $out[] = pht('Take these actions the first time this rule matches:'); } $out[] = null; foreach ($rule->getActions() as $action) { $out[] = $this->renderActionAsText($action, $handles); } return phutil_implode_html("\n", $out); } private function renderConditionAsText( HeraldCondition $condition, array $handles) { $field_type = $condition->getFieldName(); $field_name = idx($this->getFieldNameMap(), $field_type); $condition_type = $condition->getFieldCondition(); $condition_name = idx($this->getConditionNameMap(), $condition_type); $value = $this->renderConditionValueAsText($condition, $handles); return hsprintf(' %s %s %s', $field_name, $condition_name, $value); } private function renderActionAsText( HeraldAction $action, array $handles) { $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; $action_type = $action->getAction(); $action_name = idx($this->getActionNameMap($rule_global), $action_type); $target = $this->renderActionTargetAsText($action, $handles); return hsprintf(' %s %s', $action_name, $target); } private function renderConditionValueAsText( HeraldCondition $condition, array $handles) { $value = $condition->getValue(); if (!is_array($value)) { $value = array($value); } foreach ($value as $index => $val) { $handle = idx($handles, $val); if ($handle) { $value[$index] = $handle->renderLink(); } } $value = phutil_implode_html(', ', $value); return $value; } private function renderActionTargetAsText( HeraldAction $action, array $handles) { $target = $action->getTarget(); if (!is_array($target)) { $target = array($target); } foreach ($target as $index => $val) { $handle = idx($handles, $val); if ($handle) { $target[$index] = $handle->renderLink(); } } $target = phutil_implode_html(', ', $target); return $target; } /** * Given a @{class:HeraldRule}, this function extracts all the phids that * we'll want to load as handles later. * * This function performs a somewhat hacky approach to figuring out what * is and is not a phid - try to get the phid type and if the type is * *not* unknown assume its a valid phid. * * Don't try this at home. Use more strongly typed data at home. * * Think of the children. */ public static function getHandlePHIDs(HeraldRule $rule) { $phids = array($rule->getAuthorPHID()); foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (!is_array($value)) { $value = array($value); } foreach ($value as $val) { if (phid_get_type($val) != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { $phids[] = $val; } } } foreach ($rule->getActions() as $action) { $target = $action->getTarget(); if (!is_array($target)) { $target = array($target); } foreach ($target as $val) { if (phid_get_type($val) != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { $phids[] = $val; } } } + + if ($rule->isObjectRule()) { + $phids[] = $rule->getTriggerObjectPHID(); + } + return $phids; } } diff --git a/src/applications/herald/adapter/HeraldCommitAdapter.php b/src/applications/herald/adapter/HeraldCommitAdapter.php index 5206860ba..85811ebfa 100644 --- a/src/applications/herald/adapter/HeraldCommitAdapter.php +++ b/src/applications/herald/adapter/HeraldCommitAdapter.php @@ -1,455 +1,475 @@ commit; } public function getAdapterContentType() { return 'commit'; } public function getAdapterContentName() { return pht('Commits'); } public function getAdapterContentDescription() { return pht( "React to new commits appearing in tracked repositories.\n". "Commit rules can send email, flag commits, trigger audits, ". "and run build plans."); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: - return true; case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: + return true; default: return false; } } + public function canTriggerOnObject($object) { + if ($object instanceof PhabricatorRepository) { + return true; + } + return false; + } + + public function getTriggerObjectPHIDs() { + return array( + $this->repository->getPHID(), + $this->getPHID(), + ); + } + + public function explainValidTriggerObjects() { + return pht( + 'This rule can trigger for **repositories**.'); + } + public function getFieldNameMap() { return array( self::FIELD_NEED_AUDIT_FOR_PACKAGE => pht('Affected packages that need audit'), self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH => pht('On autoclose branch'), ) + parent::getFieldNameMap(); } public function getFields() { return array_merge( array( self::FIELD_BODY, self::FIELD_AUTHOR, self::FIELD_COMMITTER, self::FIELD_REVIEWER, self::FIELD_REPOSITORY, self::FIELD_DIFF_FILE, self::FIELD_DIFF_CONTENT, self::FIELD_DIFF_ADDED_CONTENT, self::FIELD_DIFF_REMOVED_CONTENT, self::FIELD_RULE, self::FIELD_AFFECTED_PACKAGE, self::FIELD_AFFECTED_PACKAGE_OWNER, self::FIELD_NEED_AUDIT_FOR_PACKAGE, self::FIELD_DIFFERENTIAL_REVISION, self::FIELD_DIFFERENTIAL_ACCEPTED, self::FIELD_DIFFERENTIAL_REVIEWERS, self::FIELD_DIFFERENTIAL_CCS, self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH, ), parent::getFields()); } public function getConditionsForField($field) { switch ($field) { case self::FIELD_NEED_AUDIT_FOR_PACKAGE: return array( self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH: return array( self::CONDITION_UNCONDITIONALLY, ); } return parent::getConditionsForField($field); } public function getActions($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return array( self::ACTION_ADD_CC, self::ACTION_EMAIL, self::ACTION_AUDIT, self::ACTION_APPLY_BUILD_PLANS, self::ACTION_NOTHING ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( self::ACTION_ADD_CC, self::ACTION_EMAIL, self::ACTION_FLAG, self::ACTION_AUDIT, self::ACTION_NOTHING, ); } } public function getValueTypeForFieldAndCondition($field, $condition) { switch ($field) { case self::FIELD_DIFFERENTIAL_CCS: return self::VALUE_EMAIL; case self::FIELD_NEED_AUDIT_FOR_PACKAGE: return self::VALUE_OWNERS_PACKAGE; } return parent::getValueTypeForFieldAndCondition($field, $condition); } public static function newLegacyAdapter( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $commit_data) { $object = new HeraldCommitAdapter(); $commit->attachRepository($repository); $object->repository = $repository; $object->commit = $commit; $object->commitData = $commit_data; return $object; } public function getPHID() { return $this->commit->getPHID(); } public function getEmailPHIDs() { return array_keys($this->emailPHIDs); } public function getAddCCMap() { return $this->addCCPHIDs; } public function getAuditMap() { return $this->auditMap; } public function getBuildPlans() { return $this->buildPlans; } public function getHeraldName() { return 'r'. $this->repository->getCallsign(). $this->commit->getCommitIdentifier(); } public function loadAffectedPaths() { if ($this->affectedPaths === null) { $result = PhabricatorOwnerPathQuery::loadAffectedPaths( $this->repository, $this->commit, PhabricatorUser::getOmnipotentUser()); $this->affectedPaths = $result; } return $this->affectedPaths; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $this->repository, $this->loadAffectedPaths()); $this->affectedPackages = $packages; } return $this->affectedPackages; } public function loadAuditNeededPackage() { if ($this->auditNeededPackages === null) { $status_arr = array( PhabricatorAuditStatusConstants::AUDIT_REQUIRED, PhabricatorAuditStatusConstants::CONCERNED, ); $requests = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere( "commitPHID = %s AND auditStatus IN (%Ls)", $this->commit->getPHID(), $status_arr); $packages = mpull($requests, 'getAuditorPHID'); $this->auditNeededPackages = $packages; } return $this->auditNeededPackages; } public function loadDifferentialRevision() { if ($this->affectedRevision === null) { $this->affectedRevision = false; $data = $this->commitData; $revision_id = $data->getCommitDetail('differential.revisionID'); if ($revision_id) { // NOTE: The Herald rule owner might not actually have access to // the revision, and can control which revision a commit is // associated with by putting text in the commit message. However, // the rules they can write against revisions don't actually expose // anything interesting, so it seems reasonable to load unconditionally // here. $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); if ($revision) { $this->affectedRevision = $revision; } } } return $this->affectedRevision; } private function loadCommitDiff() { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => PhabricatorUser::getOmnipotentUser(), 'repository' => $this->repository, 'commit' => $this->commit->getCommitIdentifier(), )); $raw = DiffusionQuery::callConduitWithDiffusionRequest( PhabricatorUser::getOmnipotentUser(), $drequest, 'diffusion.rawdiffquery', array( 'commit' => $this->commit->getCommitIdentifier(), 'timeout' => 60 * 60 * 15, 'linesOfContext' => 0)); $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw); $diff = DifferentialDiff::newFromRawChanges($changes); return $diff; } private function getDiffContent($type) { if ($this->commitDiff === null) { try { $this->commitDiff = $this->loadCommitDiff(); } catch (Exception $ex) { $this->commitDiff = $ex; phlog($ex); } } if ($this->commitDiff instanceof Exception) { $ex = $this->commitDiff; $ex_class = get_class($ex); $ex_message = pht('Failed to load changes: %s', $ex->getMessage()); return array( '<'.$ex_class.'>' => $ex_message, ); } $changes = $this->commitDiff->getChangesets(); $result = array(); foreach ($changes as $change) { $lines = array(); foreach ($change->getHunks() as $hunk) { switch ($type) { case '-': $lines[] = $hunk->makeOldFile(); break; case '+': $lines[] = $hunk->makeNewFile(); break; case '*': $lines[] = $hunk->makeChanges(); break; default: throw new Exception("Unknown content selection '{$type}'!"); } } $result[$change->getFilename()] = implode("\n", $lines); } return $result; } public function getHeraldField($field) { $data = $this->commitData; switch ($field) { case self::FIELD_BODY: return $data->getCommitMessage(); case self::FIELD_AUTHOR: return $data->getCommitDetail('authorPHID'); case self::FIELD_COMMITTER: return $data->getCommitDetail('committerPHID'); case self::FIELD_REVIEWER: return $data->getCommitDetail('reviewerPHID'); case self::FIELD_DIFF_FILE: return $this->loadAffectedPaths(); case self::FIELD_REPOSITORY: return $this->repository->getPHID(); 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_AFFECTED_PACKAGE: $packages = $this->loadAffectedPackages(); return mpull($packages, 'getPHID'); case self::FIELD_AFFECTED_PACKAGE_OWNER: $packages = $this->loadAffectedPackages(); $owners = PhabricatorOwnersOwner::loadAllForPackages($packages); return mpull($owners, 'getUserPHID'); case self::FIELD_NEED_AUDIT_FOR_PACKAGE: return $this->loadAuditNeededPackage(); case self::FIELD_DIFFERENTIAL_REVISION: $revision = $this->loadDifferentialRevision(); if (!$revision) { return null; } return $revision->getID(); case self::FIELD_DIFFERENTIAL_ACCEPTED: $revision = $this->loadDifferentialRevision(); 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->loadDifferentialRevision(); if (!$revision) { return array(); } return $revision->getReviewers(); case self::FIELD_DIFFERENTIAL_CCS: $revision = $this->loadDifferentialRevision(); if (!$revision) { return array(); } return $revision->getCCPHIDs(); case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH: return $this->repository->shouldAutocloseCommit( $this->commit, $this->commitData); } 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('Great success at doing nothing.')); break; case self::ACTION_EMAIL: foreach ($effect->getTarget() as $phid) { $this->emailPHIDs[$phid] = true; } $result[] = new HeraldApplyTranscript( $effect, true, pht('Added address to email targets.')); break; case self::ACTION_ADD_CC: foreach ($effect->getTarget() as $phid) { if (empty($this->addCCPHIDs[$phid])) { $this->addCCPHIDs[$phid] = array(); } $this->addCCPHIDs[$phid][] = $effect->getRuleID(); } $result[] = new HeraldApplyTranscript( $effect, true, pht('Added address to CC.')); break; case self::ACTION_AUDIT: foreach ($effect->getTarget() as $phid) { if (empty($this->auditMap[$phid])) { $this->auditMap[$phid] = array(); } $this->auditMap[$phid][] = $effect->getRuleID(); } $result[] = new HeraldApplyTranscript( $effect, true, pht('Triggered an audit.')); break; case self::ACTION_APPLY_BUILD_PLANS: foreach ($effect->getTarget() as $phid) { $this->buildPlans[] = $phid; } $result[] = new HeraldApplyTranscript( $effect, true, pht('Applied build plans.')); break; case self::ACTION_FLAG: $result[] = parent::applyFlagEffect( $effect, $this->commit->getPHID()); break; default: throw new Exception("No rules to handle action '{$action}'."); } } return $result; } } diff --git a/src/applications/herald/controller/HeraldNewController.php b/src/applications/herald/controller/HeraldNewController.php index 610c74d8c..bfc37d9ed 100644 --- a/src/applications/herald/controller/HeraldNewController.php +++ b/src/applications/herald/controller/HeraldNewController.php @@ -1,219 +1,320 @@ getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); - $content_type_map = HeraldAdapter::getEnabledAdapterMap($user); + $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); $errors = array(); $e_type = null; $e_rule = null; + $e_object = null; - $step = 0; + $step = $request->getInt('step'); if ($request->isFormPost()) { - $step = $request->getInt('step'); $content_type = $request->getStr('content_type'); if (empty($content_type_map[$content_type])) { $errors[] = pht('You must choose a content type for this rule.'); $e_type = pht('Required'); $step = 0; } if (!$errors && $step > 1) { $rule_type = $request->getStr('rule_type'); if (empty($rule_type_map[$rule_type])) { $errors[] = pht('You must choose a rule type for this rule.'); $e_rule = pht('Required'); $step = 1; } } - if (!$errors && $step == 2) { - $uri = id(new PhutilURI('edit/')) - ->setQueryParams( - array( - 'content_type' => $content_type, - 'rule_type' => $rule_type, - )); - $uri = $this->getApplicationURI($uri); - return id(new AphrontRedirectResponse())->setURI($uri); + if (!$errors && $step >= 2) { + $target_phid = null; + $object_name = $request->getStr('objectName'); + $done = false; + if ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_OBJECT) { + $done = true; + } else if (strlen($object_name)) { + $target_object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withNames(array($object_name)) + ->executeOne(); + if ($target_object) { + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $target_object, + PhabricatorPolicyCapability::CAN_EDIT); + if (!$can_edit) { + $errors[] = pht( + 'You can not create a rule for that object, because you do '. + 'not have permission to edit it. You can only create rules '. + 'for objects you can edit.'); + $e_object = pht('Not Editable'); + $step = 2; + } else { + $adapter = HeraldAdapter::getAdapterForContentType($content_type); + if (!$adapter->canTriggerOnObject($target_object)) { + $errors[] = pht( + 'This object is not of an allowed type for the rule. '. + 'Rules can only trigger on certain objects.'); + $e_object = pht('Invalid'); + $step = 2; + } else { + $target_phid = $target_object->getPHID(); + $done = true; + } + } + } else { + $errors[] = pht('No object exists by that name.'); + $e_object = pht('Invalid'); + $step = 2; + } + } else if ($step > 2) { + $errors[] = pht( + 'You must choose an object to associate this rule with.'); + $e_object = pht('Required'); + $step = 2; + } + + if (!$errors && $done) { + $uri = id(new PhutilURI('edit/')) + ->setQueryParams( + array( + 'content_type' => $content_type, + 'rule_type' => $rule_type, + 'targetPHID' => $target_phid, + )); + $uri = $this->getApplicationURI($uri); + return id(new AphrontRedirectResponse())->setURI($uri); + } } } + $content_type = $request->getStr('content_type'); + $rule_type = $request->getStr('rule_type'); + if ($errors) { $errors = id(new AphrontErrorView())->setErrors($errors); } $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->setAction($this->getApplicationURI('new/')); switch ($step) { case 0: default: $content_types = $this->renderContentTypeControl( $content_type_map, $e_type); $form ->addHiddenInput('step', 1) ->appendChild($content_types); $cancel_text = null; $cancel_uri = $this->getApplicationURI(); break; case 1: $rule_types = $this->renderRuleTypeControl( $rule_type_map, $e_rule); $form - ->addHiddenInput('content_type', $request->getStr('content_type')) + ->addHiddenInput('content_type', $content_type) ->addHiddenInput('step', 2) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Rule for')) ->setValue( phutil_tag( 'strong', array(), idx($content_type_map, $content_type)))) ->appendChild($rule_types); $cancel_text = pht('Back'); $cancel_uri = id(new PhutilURI('new/')) ->setQueryParams( array( - 'content_type' => $request->getStr('content_type'), + 'content_type' => $content_type, + 'step' => 0, + )); + $cancel_uri = $this->getApplicationURI($cancel_uri); + break; + case 2: + $adapter = HeraldAdapter::getAdapterForContentType($content_type); + $form + ->addHiddenInput('content_type', $content_type) + ->addHiddenInput('rule_type', $rule_type) + ->addHiddenInput('step', 3) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Rule for')) + ->setValue( + phutil_tag( + 'strong', + array(), + idx($content_type_map, $content_type)))) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Rule Type')) + ->setValue( + phutil_tag( + 'strong', + array(), + idx($rule_type_map, $rule_type)))) + ->appendRemarkupInstructions( + pht( + 'Choose the object this rule will act on (for example, enter '. + '`rX` to act on the `rX` repository).')) + ->appendRemarkupInstructions( + $adapter->explainValidTriggerObjects()) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('objectName') + ->setError($e_object) + ->setValue($request->getStr('objectName')) + ->setLabel(pht('Object'))); + + $cancel_text = pht('Back'); + $cancel_uri = id(new PhutilURI('new/')) + ->setQueryParams( + array( + 'content_type' => $content_type, + 'rule_type' => $rule_type, 'step' => 1, )); $cancel_uri = $this->getApplicationURI($cancel_uri); break; } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Continue')) ->addCancelButton($cancel_uri, $cancel_text)); $form_box = id(new PHUIObjectBoxView()) ->setFormError($errors) ->setHeaderText(pht('Create Herald Rule')) ->setForm($form); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb(pht('Create Rule')); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Create Herald Rule'), 'device' => true, )); } private function renderContentTypeControl(array $content_type_map, $e_type) { $request = $this->getRequest(); $radio = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('New Rule for')) ->setName('content_type') ->setValue($request->getStr('content_type')) ->setError($e_type); foreach ($content_type_map as $value => $name) { $adapter = HeraldAdapter::getAdapterForContentType($value); $radio->addButton( $value, $name, phutil_escape_html_newlines($adapter->getAdapterContentDescription())); } return $radio; } private function renderRuleTypeControl(array $rule_type_map, $e_rule) { $request = $this->getRequest(); // Reorder array to put less powerful rules first. $rule_type_map = array_select_keys( $rule_type_map, array( HeraldRuleTypeConfig::RULE_TYPE_PERSONAL, HeraldRuleTypeConfig::RULE_TYPE_OBJECT, HeraldRuleTypeConfig::RULE_TYPE_GLOBAL, )) + $rule_type_map; - // TODO: Enable this. - unset($rule_type_map[HeraldRuleTypeConfig::RULE_TYPE_OBJECT]); - list($can_global, $global_link) = $this->explainApplicationCapability( HeraldCapabilityManageGlobalRules::CAPABILITY, pht('You have permission to create and manage global rules.'), pht('You do not have permission to create or manage global rules.')); $captions = array( HeraldRuleTypeConfig::RULE_TYPE_PERSONAL => pht( 'Personal rules notify you about events. You own them, but they can '. 'only affect you. Personal rules only trigger for objects you have '. 'permission to see.'), + HeraldRuleTypeConfig::RULE_TYPE_OBJECT => + pht( + 'Object rules notify anyone about events. They are bound to an '. + 'object (like a repository) and can only act on that object. You '. + 'must be able to edit an object to create object rules for it. '. + 'Other users who an edit the object can edit its rules.'), HeraldRuleTypeConfig::RULE_TYPE_GLOBAL => array( pht( 'Global rules notify anyone about events. Global rules can '. 'bypass access control policies and act on any object.'), $global_link, ), ); $radio = id(new AphrontFormRadioButtonControl()) - ->setLabel(pht('Type')) + ->setLabel(pht('Rule Type')) ->setName('rule_type') ->setValue($request->getStr('rule_type')) ->setError($e_rule); $adapter = HeraldAdapter::getAdapterForContentType( $request->getStr('content_type')); foreach ($rule_type_map as $value => $name) { $caption = idx($captions, $value); $disabled = ($value == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) && (!$can_global); if (!$adapter->supportsRuleType($value)) { $disabled = true; $caption = array( $caption, "\n\n", phutil_tag( 'em', array(), pht( 'This rule type is not supported by the selected content type.')), ); } $radio->addButton( $value, $name, phutil_escape_html_newlines($caption), $disabled ? 'disabled' : null, $disabled); } return $radio; } } diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index e3ddabae0..a5f11edfd 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -1,598 +1,639 @@ 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.")); } - if (!$adapter->supportsRuleType($rule->getRuleType())) { - throw new Exception( - pht( - "This rule's content type does not support the selected rule type.")); - } - // 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); } } if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } else { $error_view = null; } $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) ->setFormError($error_view) ->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(); if (is_array($value)) { $value_map = array(); foreach ($value as $k => $fbid) { $value_map[$fbid] = $handles[$fbid]->getName(); } $value = $value_map; } $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'); 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/', ), '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(); } // A rule can not depend upon itself. unset($all_rules[$rule->getID()]); return $all_rules; } } diff --git a/src/applications/herald/controller/HeraldRuleViewController.php b/src/applications/herald/controller/HeraldRuleViewController.php index 115f85b71..ea5c6b44e 100644 --- a/src/applications/herald/controller/HeraldRuleViewController.php +++ b/src/applications/herald/controller/HeraldRuleViewController.php @@ -1,184 +1,191 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $rule = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->needConditionsAndActions(true) ->executeOne(); if (!$rule) { return new Aphront404Response(); } $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($rule->getName()) ->setPolicyObject($rule); if ($rule->getIsDisabled()) { $header->setStatus( 'oh-open', 'red', pht('Disabled')); } else { $header->setStatus( 'oh-open', null, pht('Active')); } $actions = $this->buildActionView($rule); $properties = $this->buildPropertyView($rule, $actions); $id = $rule->getID(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb("H{$id}"); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $timeline = $this->buildTimeline($rule); return $this->buildApplicationPage( array( $crumbs, $object_box, $timeline, ), array( 'title' => $rule->getName(), 'device' => true, )); } private function buildActionView(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); $id = $rule->getID(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($rule) ->setObjectURI($this->getApplicationURI("rule/{$id}/")); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $rule, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Rule')) ->setHref($this->getApplicationURI("edit/{$id}/")) ->setIcon('edit') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($rule->getIsDisabled()) { $disable_uri = "disable/{$id}/enable/"; $disable_icon = 'enable'; $disable_name = pht('Enable Rule'); } else { $disable_uri = "disable/{$id}/disable/"; $disable_icon = 'disable'; $disable_name = pht('Disable Rule'); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Disable Rule')) ->setHref($this->getApplicationURI($disable_uri)) ->setIcon($disable_icon) ->setName($disable_name) ->setDisabled(!$can_edit) ->setWorkflow(true)); return $view; } private function buildPropertyView( HeraldRule $rule, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $this->loadHandles(HeraldAdapter::getHandlePHIDs($rule)); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($rule) ->setActionList($actions); $view->addProperty( pht('Rule Type'), idx(HeraldRuleTypeConfig::getRuleTypeMap(), $rule->getRuleType())); if ($rule->isPersonalRule()) { $view->addProperty( pht('Author'), $this->getHandle($rule->getAuthorPHID())->renderLink()); } + $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); if ($adapter) { $view->addProperty( pht('Applies To'), idx( HeraldAdapter::getEnabledAdapterMap($viewer), $rule->getContentType())); + if ($rule->isObjectRule()) { + $view->addProperty( + pht('Trigger Object'), + $this->getHandle($rule->getTriggerObjectPHID())->renderLink()); + } + $view->invokeWillRenderEvent(); $view->addSectionHeader(pht('Rule Description')); $view->addTextContent( phutil_tag( 'div', array( 'style' => 'white-space: pre-wrap;', ), $adapter->renderRuleAsText($rule, $this->getLoadedHandles()))); } return $view; } private function buildTimeline(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); $xactions = id(new HeraldTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($rule->getPHID())) ->needComments(true) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); return id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($rule->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); } } diff --git a/src/applications/herald/controller/HeraldTranscriptController.php b/src/applications/herald/controller/HeraldTranscriptController.php index 7e320b235..c08f54d3e 100644 --- a/src/applications/herald/controller/HeraldTranscriptController.php +++ b/src/applications/herald/controller/HeraldTranscriptController.php @@ -1,514 +1,514 @@ id = $data['id']; $map = $this->getFilterMap(); $this->filter = idx($data, 'filter'); if (empty($map[$this->filter])) { - $this->filter = self::FILTER_AFFECTED; + $this->filter = self::FILTER_ALL; } } private function getAdapter() { return $this->adapter; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $xscript = id(new HeraldTranscriptQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$xscript) { return new Aphront404Response(); } require_celerity_resource('herald-test-css'); $nav = $this->buildSideNav(); $object_xscript = $xscript->getObjectTranscript(); if (!$object_xscript) { $notice = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Old Transcript')) ->appendChild(phutil_tag( 'p', array(), pht('Details of this transcript have been garbage collected.'))); $nav->appendChild($notice); } else { $map = HeraldAdapter::getEnabledAdapterMap($viewer); $object_type = $object_xscript->getType(); if (empty($map[$object_type])) { // TODO: We should filter these out in the Query, but we have to load // the objectTranscript right now, which is potentially enormous. We // should denormalize the object type, or move the data into a separate // table, and then filter this earlier (and thus raise a better error). // For now, just block access so we don't violate policies. throw new Exception( pht("This transcript has an invalid or inaccessible adapter.")); } $this->adapter = HeraldAdapter::getAdapterForContentType($object_type); $filter = $this->getFilterPHIDs(); $this->filterTranscript($xscript, $filter); $phids = array_merge($filter, $this->getTranscriptPHIDs($xscript)); $phids = array_unique($phids); $phids = array_filter($phids); $handles = $this->loadViewerHandles($phids); $this->handles = $handles; if ($xscript->getDryRun()) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('Dry Run')); $notice->appendChild(pht('This was a dry run to test Herald '. 'rules, no actions were executed.')); $nav->appendChild($notice); } $apply_xscript_panel = $this->buildApplyTranscriptPanel( $xscript); $nav->appendChild($apply_xscript_panel); $action_xscript_panel = $this->buildActionTranscriptPanel( $xscript); $nav->appendChild($action_xscript_panel); $object_xscript_panel = $this->buildObjectTranscriptPanel( $xscript); $nav->appendChild($object_xscript_panel); } $crumbs = id($this->buildApplicationCrumbs()) ->addTextCrumb( pht('Transcripts'), $this->getApplicationURI('/transcript/')) ->addTextCrumb($xscript->getID()); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => pht('Transcript'), 'device' => true, )); } protected function renderConditionTestValue($condition, $handles) { $value = $condition->getTestValue(); if (!is_scalar($value) && $value !== null) { foreach ($value as $key => $phid) { $handle = idx($handles, $phid); if ($handle) { $value[$key] = $handle->getName(); } else { // This shouldn't ever really happen as we are supposed to have // grabbed handles for everything, but be super liberal in what // we accept here since we expect all sorts of weird issues as we // version the system. $value[$key] = 'Unknown Object #'.$phid; } } sort($value); $value = implode(', ', $value); } return phutil_tag('span', array('class' => 'condition-test-value'), $value); } private function buildSideNav() { $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/herald/transcript/'.$this->id.'/')); $items = array(); $filters = $this->getFilterMap(); foreach ($filters as $key => $name) { $nav->addFilter($key, $name); } $nav->selectFilter($this->filter, null); return $nav; } protected function getFilterMap() { return array( - self::FILTER_AFFECTED => pht('Rules that Affected Me'), - self::FILTER_OWNED => pht('Rules I Own'), self::FILTER_ALL => pht('All Rules'), + self::FILTER_OWNED => pht('Rules I Own'), + self::FILTER_AFFECTED => pht('Rules that Affected Me'), ); } protected function getFilterPHIDs() { return array($this->getRequest()->getUser()->getPHID()); } protected function getTranscriptPHIDs($xscript) { $phids = array(); $object_xscript = $xscript->getObjectTranscript(); if (!$object_xscript) { return array(); } $phids[] = $object_xscript->getPHID(); foreach ($xscript->getApplyTranscripts() as $apply_xscript) { // TODO: This is total hacks. Add another amazing layer of abstraction. $target = (array)$apply_xscript->getTarget(); foreach ($target as $phid) { if ($phid) { $phids[] = $phid; } } } foreach ($xscript->getRuleTranscripts() as $rule_xscript) { $phids[] = $rule_xscript->getRuleOwner(); } $condition_xscripts = $xscript->getConditionTranscripts(); if ($condition_xscripts) { $condition_xscripts = call_user_func_array( 'array_merge', $condition_xscripts); } foreach ($condition_xscripts as $condition_xscript) { $value = $condition_xscript->getTestValue(); // TODO: Also total hacks. if (is_array($value)) { foreach ($value as $phid) { if ($phid) { // TODO: Probably need to make sure this "looks like" a // PHID or decrease the level of hacks here; this used // to be an is_numeric() check in Facebook land. $phids[] = $phid; } } } } return $phids; } protected function filterTranscript($xscript, $filter_phids) { $filter_owned = ($this->filter == self::FILTER_OWNED); $filter_affected = ($this->filter == self::FILTER_AFFECTED); if (!$filter_owned && !$filter_affected) { // No filtering to be done. return; } if (!$xscript->getObjectTranscript()) { return; } $user_phid = $this->getRequest()->getUser()->getPHID(); $keep_apply_xscripts = array(); $keep_rule_xscripts = array(); $filter_phids = array_fill_keys($filter_phids, true); $rule_xscripts = $xscript->getRuleTranscripts(); foreach ($xscript->getApplyTranscripts() as $id => $apply_xscript) { $rule_id = $apply_xscript->getRuleID(); if ($filter_owned) { if (empty($rule_xscripts[$rule_id])) { // No associated rule so you can't own this effect. continue; } if ($rule_xscripts[$rule_id]->getRuleOwner() != $user_phid) { continue; } } else if ($filter_affected) { $targets = (array)$apply_xscript->getTarget(); if (!array_select_keys($filter_phids, $targets)) { continue; } } $keep_apply_xscripts[$id] = true; if ($rule_id) { $keep_rule_xscripts[$rule_id] = true; } } foreach ($rule_xscripts as $rule_id => $rule_xscript) { if ($filter_owned && $rule_xscript->getRuleOwner() == $user_phid) { $keep_rule_xscripts[$rule_id] = true; } } $xscript->setRuleTranscripts( array_intersect_key( $xscript->getRuleTranscripts(), $keep_rule_xscripts)); $xscript->setApplyTranscripts( array_intersect_key( $xscript->getApplyTranscripts(), $keep_apply_xscripts)); $xscript->setConditionTranscripts( array_intersect_key( $xscript->getConditionTranscripts(), $keep_rule_xscripts)); } private function buildApplyTranscriptPanel($xscript) { $handles = $this->handles; $adapter = $this->getAdapter(); $rule_type_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; $action_names = $adapter->getActionNameMap($rule_type_global); $rows = array(); foreach ($xscript->getApplyTranscripts() as $apply_xscript) { $target = $apply_xscript->getTarget(); switch ($apply_xscript->getAction()) { case HeraldAdapter::ACTION_NOTHING: $target = ''; break; 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) { $target[$k] = $handles[$phid]->getName(); } $target = implode("\n", $target); } else { $target = ''; } break; } if ($apply_xscript->getApplied()) { $outcome = phutil_tag( 'span', array('class' => 'outcome-success'), pht('SUCCESS')); } else { $outcome = phutil_tag( 'span', array('class' => 'outcome-failure'), pht('FAILURE')); } $rows[] = array( idx($action_names, $apply_xscript->getAction(), pht('Unknown')), $target, hsprintf( 'Taken because: %s
'. 'Outcome: %s %s', $apply_xscript->getReason(), $outcome, $apply_xscript->getAppliedReason()), ); } $table = new AphrontTableView($rows); $table->setNoDataString(pht('No actions were taken.')); $table->setHeaders( array( pht('Action'), pht('Target'), pht('Details'), )); $table->setColumnClasses( array( '', '', 'wide', )); $panel = new AphrontPanelView(); $panel->setHeader(pht('Actions Taken')); $panel->appendChild($table); $panel->setNoBackground(); return $panel; } private function buildActionTranscriptPanel($xscript) { $action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID'); $adapter = $this->getAdapter(); $field_names = $adapter->getFieldNameMap(); $condition_names = $adapter->getConditionNameMap(); $handles = $this->handles; $rule_markup = array(); foreach ($xscript->getRuleTranscripts() as $rule_id => $rule) { $cond_markup = array(); foreach ($xscript->getConditionTranscriptsForRule($rule_id) as $cond) { if ($cond->getNote()) { $note = phutil_tag_div('herald-condition-note', $cond->getNote()); } else { $note = null; } if ($cond->getResult()) { $result = phutil_tag( 'span', array('class' => 'herald-outcome condition-pass'), "\xE2\x9C\x93"); } else { $result = phutil_tag( 'span', array('class' => 'herald-outcome condition-fail'), "\xE2\x9C\x98"); } $cond_markup[] = phutil_tag( 'li', array(), pht( '%s Condition: %s %s %s%s', $result, idx($field_names, $cond->getFieldName(), pht('Unknown')), idx($condition_names, $cond->getCondition(), pht('Unknown')), $this->renderConditionTestValue($cond, $handles), $note)); } if ($rule->getResult()) { $result = phutil_tag( 'span', array('class' => 'herald-outcome rule-pass'), pht('PASS')); $class = 'herald-rule-pass'; } else { $result = phutil_tag( 'span', array('class' => 'herald-outcome rule-fail'), pht('FAIL')); $class = 'herald-rule-fail'; } $cond_markup[] = phutil_tag( 'li', array(), array($result, $rule->getReason())); $user_phid = $this->getRequest()->getUser()->getPHID(); $name = $rule->getRuleName(); $rule_markup[] = phutil_tag( 'li', array( 'class' => $class, ), phutil_tag_div('rule-name', array( phutil_tag('strong', array(), $name), ' ', phutil_tag('ul', array(), $cond_markup), ))); } $panel = ''; if ($rule_markup) { $panel = new AphrontPanelView(); $panel->setHeader(pht('Rule Details')); $panel->setNoBackground(); $panel->appendChild(phutil_tag( 'ul', array('class' => 'herald-explain-list'), $rule_markup)); } return $panel; } private function buildObjectTranscriptPanel($xscript) { $adapter = $this->getAdapter(); $field_names = $adapter->getFieldNameMap(); $object_xscript = $xscript->getObjectTranscript(); $data = array(); if ($object_xscript) { $phid = $object_xscript->getPHID(); $handles = $this->loadViewerHandles(array($phid)); $data += array( pht('Object Name') => $object_xscript->getName(), pht('Object Type') => $object_xscript->getType(), pht('Object PHID') => $phid, pht('Object Link') => $handles[$phid]->renderLink(), ); } $data += $xscript->getMetadataMap(); if ($object_xscript) { foreach ($object_xscript->getFields() as $field => $value) { $field = idx($field_names, $field, '['.$field.'?]'); $data['Field: '.$field] = $value; } } $rows = array(); foreach ($data as $name => $value) { if (!($value instanceof PhutilSafeHTML)) { if (!is_scalar($value) && !is_null($value)) { $value = implode("\n", $value); } if (strlen($value) > 256) { $value = phutil_tag( 'textarea', array( 'class' => 'herald-field-value-transcript', ), $value); } } $rows[] = array($name, $value); } $table = new AphrontTableView($rows); $table->setColumnClasses( array( 'header', 'wide', )); $panel = new AphrontPanelView(); $panel->setHeader(pht('Object Transcript')); $panel->setNoBackground(); $panel->appendChild($table); return $panel; } } diff --git a/src/applications/herald/engine/HeraldEngine.php b/src/applications/herald/engine/HeraldEngine.php index d3d3899b9..f2915d703 100644 --- a/src/applications/herald/engine/HeraldEngine.php +++ b/src/applications/herald/engine/HeraldEngine.php @@ -1,434 +1,434 @@ dryRun = $dry_run; return $this; } public function getDryRun() { return $this->dryRun; } public function getRule($id) { return idx($this->rules, $id); } 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'); $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) { $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) ->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; 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(); if (isset($this->results[$id])) { // If we've already evaluated this rule because another rule depends // on it, we don't need to reevaluate it. return $this->results[$id]; } if (isset($this->stack[$id])) { // 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; } throw new HeraldRecursiveConditionsException(); } $this->stack[$id] = 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_phid = $adapter->getPHID(); + $object_phids = $adapter->getTriggerObjectPHIDs(); - if ($trigger_phid == $object_phid) { - return true; + if ($object_phids) { + if (in_array($trigger_phid, $object_phids)) { + return true; + } } - // TODO: We should also handle projects. - return false; } } diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index a441dab77..2eb442a8c 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -1,254 +1,254 @@ true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(HeraldPHIDTypeRule::TYPECONST); } public function getRuleApplied($phid) { return $this->assertAttachedKey($this->ruleApplied, $phid); } public function setRuleApplied($phid, $applied) { if ($this->ruleApplied === self::ATTACHABLE) { $this->ruleApplied = array(); } $this->ruleApplied[$phid] = $applied; return $this; } public function loadConditions() { if (!$this->getID()) { return array(); } return id(new HeraldCondition())->loadAllWhere( 'ruleID = %d', $this->getID()); } public function attachConditions(array $conditions) { assert_instances_of($conditions, 'HeraldCondition'); $this->conditions = $conditions; return $this; } public function getConditions() { // TODO: validate conditions have been attached. return $this->conditions; } public function loadActions() { if (!$this->getID()) { return array(); } return id(new HeraldAction())->loadAllWhere( 'ruleID = %d', $this->getID()); } public function attachActions(array $actions) { // TODO: validate actions have been attached. assert_instances_of($actions, 'HeraldAction'); $this->actions = $actions; return $this; } public function getActions() { return $this->actions; } public function loadEdits() { if (!$this->getID()) { return array(); } $edits = id(new HeraldRuleEdit())->loadAllWhere( 'ruleID = %d ORDER BY dateCreated DESC', $this->getID()); return $edits; } public function logEdit($editor_phid, $action) { id(new HeraldRuleEdit()) ->setRuleID($this->getID()) ->setRuleName($this->getName()) ->setEditorPHID($editor_phid) ->setAction($action) ->save(); } public function saveConditions(array $conditions) { assert_instances_of($conditions, 'HeraldCondition'); return $this->saveChildren( id(new HeraldCondition())->getTableName(), $conditions); } public function saveActions(array $actions) { assert_instances_of($actions, 'HeraldAction'); return $this->saveChildren( id(new HeraldAction())->getTableName(), $actions); } protected function saveChildren($table_name, array $children) { assert_instances_of($children, 'HeraldDAO'); if (!$this->getID()) { throw new Exception("Save rule before saving children."); } foreach ($children as $child) { $child->setRuleID($this->getID()); } // TODO: // $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', $table_name, $this->getID()); foreach ($children as $child) { $child->save(); } // $this->saveTransaction(); } public function delete() { $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', id(new HeraldCondition())->getTableName(), $this->getID()); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', id(new HeraldAction())->getTableName(), $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function hasValidAuthor() { return $this->assertAttached($this->validAuthor); } public function attachValidAuthor($valid) { $this->validAuthor = $valid; return $this; } public function getAuthor() { return $this->assertAttached($this->author); } public function attachAuthor(PhabricatorUser $user) { $this->author = $user; return $this; } public function isGlobalRule() { return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_GLOBAL); } public function isPersonalRule() { return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); } public function isObjectRule() { return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT); } public function attachTriggerObject($trigger_object) { $this->triggerObject = $trigger_object; return $this; } public function getTriggerObject() { return $this->assertAttached($this->triggerObject); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($this->isGlobalRule()) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_USER; case PhabricatorPolicyCapability::CAN_EDIT: $app = 'PhabricatorApplicationHerald'; $herald = PhabricatorApplication::getByClass($app); $global = HeraldCapabilityManageGlobalRules::CAPABILITY; return $herald->getPolicy($global); } } else if ($this->isObjectRule()) { return $this->getTriggerObject()->getPolicy($capability); } else { return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->isPersonalRule()) { return ($viewer->getPHID() == $this->getAuthorPHID()); } else { return false; } } public function describeAutomaticCapability($capability) { if ($this->isPersonalRule()) { return pht("A personal rule's owner can always view and edit it."); } else if ($this->isObjectRule()) { return pht("Object rules inherit the policies of their objects."); } return null; } } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php index 8c4201aaf..4d1d702e4 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php @@ -1,447 +1,449 @@ applyHeraldRules($repository, $commit); $commit->writeImportStatusFlag( PhabricatorRepositoryCommit::IMPORTED_HERALD); return $result; } private function applyHeraldRules( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { // Don't take any actions on an importing repository. Principally, this // avoids generating thousands of audits or emails when you import an // established repository on an existing install. if ($repository->isImporting()) { return; } $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); if (!$data) { - // TODO: Permanent failure. - return; + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Unable to load commit data. The data for this task is invalid '. + 'or no longer exists.')); } $adapter = HeraldCommitAdapter::newLegacyAdapter( $repository, $commit, $data); $rules = id(new HeraldRuleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withContentTypes(array($adapter->getAdapterContentType())) ->withDisabled(false) ->needConditionsAndActions(true) ->needAppliedToPHIDs(array($adapter->getPHID())) ->needValidateAuthors(true) ->execute(); $engine = new HeraldEngine(); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $xscript = $engine->getTranscript(); $audit_phids = $adapter->getAuditMap(); $cc_phids = $adapter->getAddCCMap(); if ($audit_phids || $cc_phids) { $this->createAudits($commit, $audit_phids, $cc_phids, $rules); } HarbormasterBuildable::applyBuildPlans( $commit->getPHID(), $repository->getPHID(), $adapter->getBuildPlans()); $explicit_auditors = $this->createAuditsFromCommitMessage($commit, $data); if ($repository->getDetail('herald-disabled')) { // This just means "disable email"; audits are (mostly) idempotent. return; } $this->publishFeedStory($repository, $commit, $data); $herald_targets = $adapter->getEmailPHIDs(); $email_phids = array_unique( array_merge( $explicit_auditors, array_keys($cc_phids), $herald_targets)); if (!$email_phids) { return; } $revision = $adapter->loadDifferentialRevision(); if ($revision) { $name = $revision->getTitle(); } else { $name = $data->getSummary(); } $author_phid = $data->getCommitDetail('authorPHID'); $reviewer_phid = $data->getCommitDetail('reviewerPHID'); $phids = array_filter( array( $author_phid, $reviewer_phid, $commit->getPHID(), )); $handles = id(new PhabricatorHandleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($phids) ->execute(); $commit_handle = $handles[$commit->getPHID()]; $commit_name = $commit_handle->getName(); if ($author_phid) { $author_name = $handles[$author_phid]->getName(); } else { $author_name = $data->getAuthorName(); } if ($reviewer_phid) { $reviewer_name = $handles[$reviewer_phid]->getName(); } else { $reviewer_name = null; } $who = implode(', ', array_filter(array($author_name, $reviewer_name))); $description = $data->getCommitMessage(); $commit_uri = PhabricatorEnv::getProductionURI($commit_handle->getURI()); $differential = $revision ? PhabricatorEnv::getProductionURI('/D'.$revision->getID()) : 'No revision.'; $files = $adapter->loadAffectedPaths(); sort($files); $files = implode("\n", $files); $xscript_id = $xscript->getID(); $why_uri = '/herald/transcript/'.$xscript_id.'/'; $reply_handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit( $commit); $template = new PhabricatorMetaMTAMail(); $inline_patch_text = $this->buildPatch($template, $repository, $commit); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection($description); $body->addTextSection(pht('DETAILS'), $commit_uri); $body->addTextSection(pht('DIFFERENTIAL REVISION'), $differential); $body->addTextSection(pht('AFFECTED FILES'), $files); $body->addReplySection($reply_handler->getReplyHandlerInstructions()); $body->addHeraldSection($why_uri); $body->addRawSection($inline_patch_text); $body = $body->render(); $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); $threading = PhabricatorAuditCommentEditor::getMailThreading( $repository, $commit); list($thread_id, $thread_topic) = $threading; $template->setRelatedPHID($commit->getPHID()); $template->setSubject("{$commit_name}: {$name}"); $template->setSubjectPrefix($prefix); $template->setVarySubjectPrefix("[Commit]"); $template->setBody($body); $template->setThreadID($thread_id, $is_new = true); $template->addHeader('Thread-Topic', $thread_topic); $template->setIsBulk(true); $template->addHeader('X-Herald-Rules', $xscript->getXHeraldRulesHeader()); if ($author_phid) { $template->setFrom($author_phid); } // TODO: We should verify that each recipient can actually see the // commit before sending them email (T603). $mails = $reply_handler->multiplexMail( $template, id(new PhabricatorHandleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($email_phids) ->execute(), array()); foreach ($mails as $mail) { $mail->saveAndSend(); } } private function createAudits( PhabricatorRepositoryCommit $commit, array $map, array $ccmap, array $rules) { assert_instances_of($rules, 'HeraldRule'); $requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere( 'commitPHID = %s', $commit->getPHID()); $requests = mpull($requests, null, 'getAuditorPHID'); $rules = mpull($rules, null, 'getID'); $maps = array( PhabricatorAuditStatusConstants::AUDIT_REQUIRED => $map, PhabricatorAuditStatusConstants::CC => $ccmap, ); foreach ($maps as $status => $map) { foreach ($map as $phid => $rule_ids) { $request = idx($requests, $phid); if ($request) { continue; } $reasons = array(); foreach ($rule_ids as $id) { $rule_name = '?'; if ($rules[$id]) { $rule_name = $rules[$id]->getName(); } if ($status == PhabricatorAuditStatusConstants::AUDIT_REQUIRED) { $reasons[] = pht( '%s Triggered Audit', "H{$id} {$rule_name}"); } else { $reasons[] = pht( '%s Triggered CC', "H{$id} {$rule_name}"); } } $request = new PhabricatorRepositoryAuditRequest(); $request->setCommitPHID($commit->getPHID()); $request->setAuditorPHID($phid); $request->setAuditStatus($status); $request->setAuditReasons($reasons); $request->save(); } } $commit->updateAuditStatus($requests); $commit->save(); } /** * Find audit requests in the "Auditors" field if it is present and trigger * explicit audit requests. */ private function createAuditsFromCommitMessage( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { $message = $data->getCommitMessage(); $matches = null; if (!preg_match('/^Auditors:\s*(.*)$/im', $message, $matches)) { return array(); } $phids = DifferentialFieldSpecification::parseCommitMessageObjectList( $matches[1], $include_mailables = false, $allow_partial = true); if (!$phids) { return array(); } $requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere( 'commitPHID = %s', $commit->getPHID()); $requests = mpull($requests, null, 'getAuditorPHID'); foreach ($phids as $phid) { if (isset($requests[$phid])) { continue; } $request = new PhabricatorRepositoryAuditRequest(); $request->setCommitPHID($commit->getPHID()); $request->setAuditorPHID($phid); $request->setAuditStatus( PhabricatorAuditStatusConstants::AUDIT_REQUESTED); $request->setAuditReasons( array( 'Requested by Author', )); $request->save(); $requests[$phid] = $request; } $commit->updateAuditStatus($requests); $commit->save(); return $phids; } private function publishFeedStory( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { if (time() > $commit->getEpoch() + (24 * 60 * 60)) { // Don't publish stories that are more than 24 hours old, to avoid // ridiculous levels of feed spam if a repository is imported without // disabling feed publishing. return; } $author_phid = $commit->getAuthorPHID(); $committer_phid = $data->getCommitDetail('committerPHID'); $publisher = new PhabricatorFeedStoryPublisher(); $publisher->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_COMMIT); $publisher->setStoryData( array( 'commitPHID' => $commit->getPHID(), 'summary' => $data->getSummary(), 'authorName' => $data->getAuthorName(), 'authorPHID' => $author_phid, 'committerName' => $data->getCommitDetail('committer'), 'committerPHID' => $committer_phid, )); $publisher->setStoryTime($commit->getEpoch()); $publisher->setRelatedPHIDs( array_filter( array( $author_phid, $committer_phid, ))); if ($author_phid) { $publisher->setStoryAuthorPHID($author_phid); } $publisher->publish(); } private function buildPatch( PhabricatorMetaMTAMail $template, PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $attach_key = 'metamta.diffusion.attach-patches'; $inline_key = 'metamta.diffusion.inline-patches'; $attach_patches = PhabricatorEnv::getEnvConfig($attach_key); $inline_patches = PhabricatorEnv::getEnvConfig($inline_key); if (!$attach_patches && !$inline_patches) { return; } $encoding = $repository->getDetail('encoding', 'UTF-8'); $result = null; $patch_error = null; try { $raw_patch = $this->loadRawPatchText($repository, $commit); if ($attach_patches) { $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $template->addAttachment( new PhabricatorMetaMTAAttachment( $raw_patch, $commit_name.'.patch', 'text/x-patch; charset='.$encoding)); } } catch (Exception $ex) { phlog($ex); $patch_error = 'Unable to generate: '.$ex->getMessage(); } if ($patch_error) { $result = $patch_error; } else if ($inline_patches) { $len = substr_count($raw_patch, "\n"); if ($len <= $inline_patches) { // We send email as utf8, so we need to convert the text to utf8 if // we can. if ($encoding) { $raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding); } $result = phutil_utf8ize($raw_patch); } } if ($result) { $result = "PATCH\n\n{$result}\n"; } return $result; } private function loadRawPatchText( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => PhabricatorUser::getOmnipotentUser(), 'initFromConduit' => false, 'repository' => $repository, 'commit' => $commit->getCommitIdentifier(), )); $raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest); $raw_query->setLinesOfContext(3); $time_key = 'metamta.diffusion.time-limit'; $byte_key = 'metamta.diffusion.byte-limit'; $time_limit = PhabricatorEnv::getEnvConfig($time_key); $byte_limit = PhabricatorEnv::getEnvConfig($byte_key); if ($time_limit) { $raw_query->setTimeout($time_limit); } $raw_diff = $raw_query->loadRawDiff(); $size = strlen($raw_diff); if ($byte_limit && $size > $byte_limit) { $pretty_size = phabricator_format_bytes($size); $pretty_limit = phabricator_format_bytes($byte_limit); throw new Exception( "Patch size of {$pretty_size} exceeds configured byte size limit of ". "{$pretty_limit}."); } return $raw_diff; } }