diff --git a/src/applications/herald/adapter/HeraldCommitAdapter.php b/src/applications/herald/adapter/HeraldCommitAdapter.php index 36e5d87375..49abc2430f 100644 --- a/src/applications/herald/adapter/HeraldCommitAdapter.php +++ b/src/applications/herald/adapter/HeraldCommitAdapter.php @@ -1,414 +1,424 @@ pht('Affected packages that need audit'), self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'), self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'), self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'), 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_REVIEWERS, self::FIELD_DIFFERENTIAL_CCS, self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH, ), parent::getFields()); } public function getConditionsForField($field) { switch ($field) { 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: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); 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: return array( self::ACTION_ADD_CC, self::ACTION_EMAIL, self::ACTION_AUDIT, 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(); $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 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) { - // TODO: (T603) Herald policy stuff. - $revision = id(new DifferentialRevision())->load($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) { - $revision->loadRelationships(); $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 loadChangesets() { try { $diff = $this->loadCommitDiff(); } catch (Exception $ex) { return array( '<<< Failed to load diff, this may mean the change was '. 'unimaginably enormous. >>>'); } return $diff->getChangesets(); } 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: $dict = array(); $lines = array(); $changes = $this->loadChangesets(); foreach ($changes as $change) { $lines = array(); foreach ($change->getHunks() as $hunk) { $lines[] = $hunk->makeChanges(); } $dict[$change->getFilename()] = implode("\n", $lines); } return $dict; case self::FIELD_DIFF_ADDED_CONTENT: $dict = array(); $lines = array(); $changes = $this->loadChangesets(); foreach ($changes as $change) { $lines = array(); foreach ($change->getHunks() as $hunk) { $lines[] = implode('', $hunk->getAddedLines()); } $dict[$change->getFilename()] = implode("\n", $lines); } return $dict; case self::FIELD_DIFF_REMOVED_CONTENT: $dict = array(); $lines = array(); $changes = $this->loadChangesets(); foreach ($changes as $change) { $lines = array(); foreach ($change->getHunks() as $hunk) { $lines[] = implode('', $hunk->getRemovedLines()); } $dict[$change->getFilename()] = implode("\n", $lines); } return $dict; 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_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_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 7776330d1a..16d29ff1d8 100644 --- a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php +++ b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php @@ -1,423 +1,429 @@ loadRelationships(); + // 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 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(); $revision = $this->revision; if ($revision->getRepositoryPHID()) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array($revision->getRepositoryPHID())) ->execute(); if ($repositories) { $this->repository = head($repositories); return $this->repository; } } $repository = id(new DifferentialRepositoryLookup()) ->setViewer($viewer) ->setDiff($this->diff) ->lookupRepository(); if ($repository) { $this->repository = $repository; return $this->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_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_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_NOTHING, ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( self::ACTION_ADD_CC, self::ACTION_REMOVE_CC, self::ACTION_EMAIL, self::ACTION_FLAG, 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; default: throw new Exception("No rules to handle action '{$action}'."); } } return $result; } }