diff --git a/src/applications/herald/adapter/HeraldCommitAdapter.php b/src/applications/herald/adapter/HeraldCommitAdapter.php index 55bf6fc23e..34f1c51301 100644 --- a/src/applications/herald/adapter/HeraldCommitAdapter.php +++ b/src/applications/herald/adapter/HeraldCommitAdapter.php @@ -1,502 +1,539 @@ 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: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return true; default: return false; } } public function canTriggerOnObject($object) { if ($object instanceof PhabricatorRepository) { return true; } + if ($object instanceof PhabricatorProject) { + return true; + } return false; } public function getTriggerObjectPHIDs() { - return array( - $this->repository->getPHID(), - $this->getPHID(), - ); + return array_merge( + array( + $this->repository->getPHID(), + $this->getPHID(), + ), + $this->repository->getProjectPHIDs()); } public function explainValidTriggerObjects() { return pht( - 'This rule can trigger for **repositories**.'); + 'This rule can trigger for **repositories** and **projects**.'); } 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_REPOSITORY_PROJECTS, self::FIELD_DIFF_FILE, self::FIELD_DIFF_CONTENT, self::FIELD_DIFF_ADDED_CONTENT, self::FIELD_DIFF_REMOVED_CONTENT, self::FIELD_DIFF_ENORMOUS, 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 setCommit(PhabricatorRepositoryCommit $commit) { + $viewer = PhabricatorUser::getOmnipotentUser(); + + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withIDs(array($commit->getRepositoryID())) + ->needProjectPHIDs(true) + ->executeOne(); + if (!$repository) { + throw new Exception(pht('Unable to load repository!')); + } + + $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( + 'commitID = %d', + $commit->getID()); + if (!$data) { + throw new Exception(pht('Unable to load commit data!')); + } + + $this->commit = clone $commit; + $this->commit->attachRepository($repository); + $this->commit->attachCommitData($data); + + $this->repository = $repository; + $this->commitData = $data; + + return $this; + } + 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; } public static function getEnormousByteLimit() { return 1024 * 1024 * 1024; // 1GB } public static function getEnormousTimeLimit() { return 60 * 15; // 15 Minutes } private function loadCommitDiff() { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => PhabricatorUser::getOmnipotentUser(), 'repository' => $this->repository, 'commit' => $this->commit->getCommitIdentifier(), )); $byte_limit = self::getEnormousByteLimit(); $raw = DiffusionQuery::callConduitWithDiffusionRequest( PhabricatorUser::getOmnipotentUser(), $drequest, 'diffusion.rawdiffquery', array( 'commit' => $this->commit->getCommitIdentifier(), 'timeout' => self::getEnormousTimeLimit(), 'byteLimit' => $byte_limit, 'linesOfContext' => 0, )); if (strlen($raw) >= $byte_limit) { throw new Exception( pht( 'The raw text of this change is enormous (larger than %d bytes). '. 'Herald can not process it.', $byte_limit)); } $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_REPOSITORY_PROJECTS: + return $this->repository->getProjectPHIDs(); case self::FIELD_DIFF_CONTENT: return $this->getDiffContent('*'); case self::FIELD_DIFF_ADDED_CONTENT: return $this->getDiffContent('+'); case self::FIELD_DIFF_REMOVED_CONTENT: return $this->getDiffContent('-'); case self::FIELD_DIFF_ENORMOUS: $this->getDiffContent('*'); return ($this->commitDiff instanceof Exception); 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; } // after a revision is accepted, it can be closed (say via arc land) // so use this function to figure out if it was accepted at one point // *and* not later rejected... what a function! $reviewed_by = $revision->loadReviewedBy(); if (!$reviewed_by) { 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/adapter/HeraldDifferentialRevisionAdapter.php b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php index a17d6b2e6d..87c6357902 100644 --- a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php +++ b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php @@ -1,510 +1,521 @@ revision; } public function getAdapterContentType() { return 'differential'; } public function getAdapterContentName() { return pht('Differential Revisions'); } public function getAdapterContentDescription() { return pht( "React to revisions being created or updated.\n". "Revision rules can send email, flag revisions, add reviewers, ". "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: default: return false; } } public function getFields() { return array_merge( array( self::FIELD_TITLE, self::FIELD_BODY, self::FIELD_AUTHOR, self::FIELD_AUTHOR_PROJECTS, self::FIELD_REVIEWERS, self::FIELD_CC, self::FIELD_REPOSITORY, + self::FIELD_REPOSITORY_PROJECTS, 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, ), parent::getFields()); } public function getRepetitionOptions() { return array( HeraldRepetitionPolicyConfig::EVERY, HeraldRepetitionPolicyConfig::FIRST, ); } public static function newLegacyAdapter( DifferentialRevision $revision, DifferentialDiff $diff) { $object = new HeraldDifferentialRevisionAdapter(); // Reload the revision to pick up relationship information. $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision->getID())) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); $object->revision = $revision; $object->diff = $diff; return $object; } public function setExplicitCCs($explicit_ccs) { $this->explicitCCs = $explicit_ccs; return $this; } public function setExplicitReviewers($explicit_reviewers) { $this->explicitReviewers = $explicit_reviewers; return $this; } public function setForbiddenCCs($forbidden_ccs) { $this->forbiddenCCs = $forbidden_ccs; return $this; } public function getCCsAddedByHerald() { return array_diff_key($this->newCCs, $this->remCCs); } public function getCCsRemovedByHerald() { return $this->remCCs; } public function getEmailPHIDsAddedByHerald() { return $this->emailPHIDs; } public function getReviewersAddedByHerald() { return $this->addReviewerPHIDs; } public function getBlockingReviewersAddedByHerald() { return $this->blockingReviewerPHIDs; } public function getBuildPlans() { return $this->buildPlans; } public function getPHID() { return $this->revision->getPHID(); } public function getHeraldName() { return $this->revision->getTitle(); } public function loadRepository() { if ($this->repository === null) { $this->repository = false; - // TODO: (T603) Implement policy stuff in Herald. $viewer = PhabricatorUser::getOmnipotentUser(); + $repository_phid = null; $revision = $this->revision; if ($revision->getRepositoryPHID()) { - $repositories = id(new PhabricatorRepositoryQuery()) + $repository_phid = $revision->getRepositoryPHID(); + } else { + $repository = id(new DifferentialRepositoryLookup()) ->setViewer($viewer) - ->withPHIDs(array($revision->getRepositoryPHID())) - ->execute(); - if ($repositories) { - $this->repository = head($repositories); - return $this->repository; + ->setDiff($this->diff) + ->lookupRepository(); + if ($repository) { + // We want to get the projects for this repository too, so run a + // full query for it below. + $repository_phid = $repository->getPHID(); } } - $repository = id(new DifferentialRepositoryLookup()) - ->setViewer($viewer) - ->setDiff($this->diff) - ->lookupRepository(); - if ($repository) { - $this->repository = $repository; - return $this->repository; + if ($repository_phid) { + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withPHIDs(array($repository_phid)) + ->needProjectPHIDs(true) + ->executeOne(); + if ($repository) { + $this->repository = $repository; + } } - - $repository = false; } + return $this->repository; } protected function loadChangesets() { if ($this->changesets === null) { $this->changesets = $this->diff->loadChangesets(); } return $this->changesets; } protected function loadAffectedPaths() { $changesets = $this->loadChangesets(); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $this->getAbsoluteRepositoryPathForChangeset($changeset); } return $paths; } protected function getAbsoluteRepositoryPathForChangeset( DifferentialChangeset $changeset) { $repository = $this->loadRepository(); if (!$repository) { return '/'.ltrim($changeset->getFilename(), '/'); } $diff = $this->diff; return $changeset->getAbsoluteRepositoryPath($repository, $diff); } protected function loadContentDictionary() { $changesets = $this->loadChangesets(); $hunks = array(); if ($changesets) { $hunks = id(new DifferentialHunk())->loadAllWhere( 'changesetID in (%Ld)', mpull($changesets, 'getID')); } $dict = array(); $hunks = mgroup($hunks, 'getChangesetID'); $changesets = mpull($changesets, null, 'getID'); foreach ($changesets as $id => $changeset) { $path = $this->getAbsoluteRepositoryPathForChangeset($changeset); $content = array(); foreach (idx($hunks, $id, array()) as $hunk) { $content[] = $hunk->makeChanges(); } $dict[$path] = implode("\n", $content); } return $dict; } protected function loadAddedContentDictionary() { $changesets = $this->loadChangesets(); $hunks = array(); if ($changesets) { $hunks = id(new DifferentialHunk())->loadAllWhere( 'changesetID in (%Ld)', mpull($changesets, 'getID')); } $dict = array(); $hunks = mgroup($hunks, 'getChangesetID'); $changesets = mpull($changesets, null, 'getID'); foreach ($changesets as $id => $changeset) { $path = $this->getAbsoluteRepositoryPathForChangeset($changeset); $content = array(); foreach (idx($hunks, $id, array()) as $hunk) { $content[] = implode('', $hunk->getAddedLines()); } $dict[$path] = implode("\n", $content); } return $dict; } protected function loadRemovedContentDictionary() { $changesets = $this->loadChangesets(); $hunks = array(); if ($changesets) { $hunks = id(new DifferentialHunk())->loadAllWhere( 'changesetID in (%Ld)', mpull($changesets, 'getID')); } $dict = array(); $hunks = mgroup($hunks, 'getChangesetID'); $changesets = mpull($changesets, null, 'getID'); foreach ($changesets as $id => $changeset) { $path = $this->getAbsoluteRepositoryPathForChangeset($changeset); $content = array(); foreach (idx($hunks, $id, array()) as $hunk) { $content[] = implode('', $hunk->getRemovedLines()); } $dict[$path] = implode("\n", $content); } return $dict; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $this->affectedPackages = array(); $repository = $this->loadRepository(); if ($repository) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $repository, $this->loadAffectedPaths()); $this->affectedPackages = $packages; } } return $this->affectedPackages; } public function getHeraldField($field) { switch ($field) { case self::FIELD_TITLE: return $this->revision->getTitle(); break; case self::FIELD_BODY: return $this->revision->getSummary()."\n". $this->revision->getTestPlan(); break; case self::FIELD_AUTHOR: return $this->revision->getAuthorPHID(); break; case self::FIELD_AUTHOR_PROJECTS: $author_phid = $this->revision->getAuthorPHID(); if (!$author_phid) { return array(); } $projects = id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withMemberPHIDs(array($author_phid)) ->execute(); return mpull($projects, 'getPHID'); case self::FIELD_DIFF_FILE: return $this->loadAffectedPaths(); case self::FIELD_CC: if (isset($this->explicitCCs)) { return array_keys($this->explicitCCs); } else { return $this->revision->getCCPHIDs(); } case self::FIELD_REVIEWERS: if (isset($this->explicitReviewers)) { return array_keys($this->explicitReviewers); } else { return $this->revision->getReviewers(); } case self::FIELD_REPOSITORY: $repository = $this->loadRepository(); if (!$repository) { return null; } return $repository->getPHID(); + case self::FIELD_REPOSITORY_PROJECTS: + $repository = $this->loadRepository(); + if (!$repository) { + return null; + } + return $repository->getProjectPHIDs(); case self::FIELD_DIFF_CONTENT: return $this->loadContentDictionary(); case self::FIELD_DIFF_ADDED_CONTENT: return $this->loadAddedContentDictionary(); case self::FIELD_DIFF_REMOVED_CONTENT: return $this->loadRemovedContentDictionary(); case self::FIELD_AFFECTED_PACKAGE: $packages = $this->loadAffectedPackages(); return mpull($packages, 'getPHID'); case self::FIELD_AFFECTED_PACKAGE_OWNER: $packages = $this->loadAffectedPackages(); return PhabricatorOwnersOwner::loadAffiliatedUserPHIDs( mpull($packages, 'getID')); } return parent::getHeraldField($field); } public function getActions($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: return array( self::ACTION_ADD_CC, self::ACTION_REMOVE_CC, self::ACTION_EMAIL, self::ACTION_ADD_REVIEWERS, self::ACTION_ADD_BLOCKING_REVIEWERS, self::ACTION_APPLY_BUILD_PLANS, self::ACTION_NOTHING, ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( self::ACTION_ADD_CC, self::ACTION_REMOVE_CC, self::ACTION_EMAIL, self::ACTION_FLAG, self::ACTION_ADD_REVIEWERS, self::ACTION_ADD_BLOCKING_REVIEWERS, self::ACTION_NOTHING, ); } } public function applyHeraldEffects(array $effects) { assert_instances_of($effects, 'HeraldEffect'); $result = array(); if ($this->explicitCCs) { $effect = new HeraldEffect(); $effect->setAction(self::ACTION_ADD_CC); $effect->setTarget(array_keys($this->explicitCCs)); $effect->setReason( pht('CCs provided explicitly by revision author or carried over '. 'from a previous version of the revision.')); $result[] = new HeraldApplyTranscript( $effect, true, pht('Added addresses to CC list.')); } $forbidden_ccs = array_fill_keys( nonempty($this->forbiddenCCs, array()), true); foreach ($effects as $effect) { $action = $effect->getAction(); switch ($action) { case self::ACTION_NOTHING: $result[] = new HeraldApplyTranscript( $effect, true, pht('OK, did nothing.')); break; case self::ACTION_FLAG: $result[] = parent::applyFlagEffect( $effect, $this->revision->getPHID()); break; case self::ACTION_EMAIL: case self::ACTION_ADD_CC: $op = ($action == self::ACTION_EMAIL) ? 'email' : 'CC'; $base_target = $effect->getTarget(); $forbidden = array(); foreach ($base_target as $key => $fbid) { if (isset($forbidden_ccs[$fbid])) { $forbidden[] = $fbid; unset($base_target[$key]); } else { if ($action == self::ACTION_EMAIL) { $this->emailPHIDs[$fbid] = true; } else { $this->newCCs[$fbid] = true; } } } if ($forbidden) { $failed = clone $effect; $failed->setTarget($forbidden); if ($base_target) { $effect->setTarget($base_target); $result[] = new HeraldApplyTranscript( $effect, true, pht('Added these addresses to %s list. '. 'Others could not be added.', $op)); } $result[] = new HeraldApplyTranscript( $failed, false, pht('%s forbidden, these addresses have unsubscribed.', $op)); } else { $result[] = new HeraldApplyTranscript( $effect, true, pht('Added addresses to %s list.', $op)); } break; case self::ACTION_REMOVE_CC: foreach ($effect->getTarget() as $fbid) { $this->remCCs[$fbid] = true; } $result[] = new HeraldApplyTranscript( $effect, true, pht('Removed addresses from CC list.')); break; case self::ACTION_ADD_REVIEWERS: foreach ($effect->getTarget() as $phid) { $this->addReviewerPHIDs[$phid] = true; } $result[] = new HeraldApplyTranscript( $effect, true, pht('Added reviewers.')); break; case self::ACTION_ADD_BLOCKING_REVIEWERS: // This adds reviewers normally, it just also marks them blocking. foreach ($effect->getTarget() as $phid) { $this->addReviewerPHIDs[$phid] = true; $this->blockingReviewerPHIDs[$phid] = true; } $result[] = new HeraldApplyTranscript( $effect, true, pht('Added blocking reviewers.')); 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; default: throw new Exception("No rules to handle action '{$action}'."); } } return $result; } } diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php index 3cbfa47dc6..fb2f1cc996 100644 --- a/src/applications/herald/controller/HeraldTestConsoleController.php +++ b/src/applications/herald/controller/HeraldTestConsoleController.php @@ -1,128 +1,123 @@ getRequest(); $user = $request->getUser(); $request = $this->getRequest(); $object_name = trim($request->getStr('object_name')); $e_name = true; $errors = array(); if ($request->isFormPost()) { if (!$object_name) { $e_name = pht('Required'); $errors[] = pht('An object name is required.'); } if (!$errors) { $object = id(new PhabricatorObjectQuery()) ->setViewer($user) ->withNames(array($object_name)) ->executeOne(); if (!$object) { $e_name = pht('Invalid'); $errors[] = pht('No object exists with that name.'); } if (!$errors) { // TODO: Let the adapters claim objects instead. if ($object instanceof DifferentialRevision) { $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter( $object, $object->loadActiveDiff()); } else if ($object instanceof PhabricatorRepositoryCommit) { - $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( - 'commitID = %d', - $object->getID()); - $adapter = HeraldCommitAdapter::newLegacyAdapter( - $object->getRepository(), - $object, - $data); + $adapter = id(new HeraldCommitAdapter()) + ->setCommit($object); } else if ($object instanceof ManiphestTask) { $adapter = id(new HeraldManiphestTaskAdapter()) ->setTask($object); } else if ($object instanceof PholioMock) { $adapter = id(new HeraldPholioMockAdapter()) ->setMock($object); } else { throw new Exception("Can not build adapter for object!"); } $rules = id(new HeraldRuleQuery()) ->setViewer($user) ->withContentTypes(array($adapter->getAdapterContentType())) ->withDisabled(false) ->needConditionsAndActions(true) ->needAppliedToPHIDs(array($object->getPHID())) ->needValidateAuthors(true) ->execute(); $engine = id(new HeraldEngine()) ->setDryRun(true); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $xscript = $engine->getTranscript(); return id(new AphrontRedirectResponse()) ->setURI('/herald/transcript/'.$xscript->getID().'/'); } } } if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } else { $error_view = null; } $text = pht( 'Enter an object to test rules for, like a Diffusion commit (e.g., '. 'rX123) or a Differential revision (e.g., D123). You will be shown '. 'the results of a dry run on the object.'); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( phutil_tag('p', array('class' => 'aphront-form-instructions'), $text)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Object Name')) ->setName('object_name') ->setError($e_name) ->setValue($object_name)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Test Rules'))); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Herald Test Console')) ->setFormError($error_view) ->setForm($form); $crumbs = id($this->buildApplicationCrumbs()) ->addTextCrumb( pht('Transcripts'), $this->getApplicationURI('/transcript/')) ->addTextCrumb(pht('Test Console')); return $this->buildApplicationPage( $box, array( 'title' => pht('Test Console'), 'device' => true, )); } } diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index 85fd6e6c1b..cd6bfa4b9c 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 4d1d702e4f..8f4b3416d6 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php @@ -1,449 +1,447 @@ 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) { 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); + $adapter = id(new HeraldCommitAdapter()) + ->setCommit($commit); $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; } }