diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php index f3a0ef53ab..1755cb7701 100644 --- a/src/applications/diffusion/herald/HeraldCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php @@ -1,372 +1,375 @@ setViewer($viewer) ->withPHIDs(array($object->getPHID())) ->needCommitData(true) ->executeOne(); if (!$object) { throw new Exception( pht( 'Failed to reload commit ("%s") to fetch commit data.', $object->getPHID())); } return id(clone $this) ->setObject($object); } protected function initializeNewAdapter() { $this->commit = $this->newObject(); } public function setObject($object) { $this->commit = $object; return $this; } public function getObject() { return $this->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() { $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $repository_phid = $this->getRepository()->getPHID(); $commit_phid = $this->getObject()->getPHID(); $phids = array(); $phids[] = $commit_phid; $phids[] = $repository_phid; // NOTE: This is projects for the repository, not for the commit. When // Herald evaluates, commits normally can not have any project tags yet. $repository_project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $repository_phid, $project_type); foreach ($repository_project_phids as $phid) { $phids[] = $phid; } $phids = array_unique($phids); $phids = array_values($phids); return $phids; } public function explainValidTriggerObjects() { return pht('This rule can trigger for **repositories** and **projects**.'); } public function getHeraldName() { return $this->commit->getMonogram(); } public function loadAffectedPaths() { $viewer = $this->getViewer(); if ($this->affectedPaths === null) { $result = PhabricatorOwnerPathQuery::loadAffectedPaths( $this->getRepository(), $this->commit, $viewer); $this->affectedPaths = $result; } return $this->affectedPaths; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $this->getRepository(), $this->loadAffectedPaths()); $this->affectedPackages = $packages; } return $this->affectedPackages; } public function loadAuditNeededPackages() { 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); $this->auditNeededPackages = $requests; } return $this->auditNeededPackages; } public function loadDifferentialRevision() { - $viewer = $this->getViewer(); - if ($this->affectedRevision === null) { - $this->affectedRevision = false; - - $commit = $this->getObject(); - $data = $commit->getCommitData(); - - $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($viewer) - ->needReviewers(true) - ->executeOne(); - if ($revision) { - $this->affectedRevision = $revision; - } + $viewer = $this->getViewer(); + + // NOTE: The viewer here is omnipotent, which means that Herald discloses + // some information users do not normally have access to when rules load + // the revision related to a commit. See D20468. + + // A user who wants to learn about "Dxyz" can write a Herald rule which + // uses all the "Related revision..." fields, then push a commit which + // contains "Differential Revision: Dxyz" in the message to make Herald + // evaluate the commit with "Dxyz" as the related revision. + + // At time of writing, this commit will link to the revision and the + // transcript for the commit will disclose some information about the + // revision (like reviewers, subscribers, and build status) which the + // commit author could not otherwise see. + + // For now, we just accept this. The disclosures are relatively + // uninteresting and you have to jump through a lot of hoops (and leave + // a lot of evidence) to get this information. + + $revision = DiffusionCommitRevisionQuery::loadRevisionForCommit( + $viewer, + $this->getObject()); + if ($revision) { + $this->affectedRevision = $revision; + } else { + $this->affectedRevision = false; } } return $this->affectedRevision; } public static function getEnormousByteLimit() { return 256 * 1024 * 1024; // 256MB. See T13142 and T13143. } public static function getEnormousTimeLimit() { return 60 * 15; // 15 Minutes } private function loadCommitDiff() { $viewer = $this->getViewer(); $byte_limit = self::getEnormousByteLimit(); $time_limit = self::getEnormousTimeLimit(); $diff_info = $this->callConduit( 'diffusion.rawdiffquery', array( 'commit' => $this->commit->getCommitIdentifier(), 'timeout' => $time_limit, 'byteLimit' => $byte_limit, 'linesOfContext' => 0, )); if ($diff_info['tooHuge']) { throw new Exception( pht( 'The raw text of this change is enormous (larger than %s byte(s)). '. 'Herald can not process it.', new PhutilNumber($byte_limit))); } if ($diff_info['tooSlow']) { throw new Exception( pht( 'The raw text of this change took too long to process (longer '. 'than %s second(s)). Herald can not process it.', new PhutilNumber($time_limit))); } $file_phid = $diff_info['filePHID']; $diff_file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$diff_file) { throw new Exception( pht( 'Failed to load diff ("%s") for this change.', $file_phid)); } $raw = $diff_file->loadFileData(); $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw); $diff = DifferentialDiff::newEphemeralFromRawChanges( $changes); return $diff; } public function isDiffEnormous() { $this->loadDiffContent('*'); return ($this->commitDiff instanceof Exception); } public function loadDiffContent($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(pht("Unknown content selection '%s'!", $type)); } } $result[$change->getFilename()] = implode("\n", $lines); } return $result; } public function loadIsMergeCommit() { $parents = $this->callConduit( 'diffusion.commitparentsquery', array( 'commit' => $this->getObject()->getCommitIdentifier(), )); return (count($parents) > 1); } private function callConduit($method, array $params) { $viewer = $this->getViewer(); $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $viewer, 'repository' => $this->getRepository(), 'commit' => $this->commit->getCommitIdentifier(), )); return DiffusionQuery::callConduitWithDiffusionRequest( $viewer, $drequest, $method, $params); } private function getRepository() { return $this->getObject()->getRepository(); } /* -( HarbormasterBuildableAdapterInterface )------------------------------ */ public function getHarbormasterBuildablePHID() { return $this->getObject()->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getObject()->getRepository()->getPHID(); } public function getQueuedHarbormasterBuildRequests() { return $this->buildRequests; } public function queueHarbormasterBuildRequest( HarbormasterBuildRequest $request) { $this->buildRequests[] = $request; } } diff --git a/src/applications/diffusion/query/DiffusionCommitRevisionQuery.php b/src/applications/diffusion/query/DiffusionCommitRevisionQuery.php index a73db18a59..68a3ff3beb 100644 --- a/src/applications/diffusion/query/DiffusionCommitRevisionQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitRevisionQuery.php @@ -1,49 +1,68 @@ withSourcePHIDs($commit_phids) ->withEdgeTypes( array( DiffusionCommitHasRevisionEdgeType::EDGECONST, )); $edge_query->execute(); $revision_phids = $edge_query->getDestinationPHIDs(); if (!$revision_phids) { return array(); } $revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withPHIDs($revision_phids) ->execute(); $revisions = mpull($revisions, null, 'getPHID'); $map = array(); foreach ($commit_phids as $commit_phid) { $revision_phids = $edge_query->getDestinationPHIDs( array( $commit_phid, )); $map[$commit_phid] = array_select_keys($revisions, $revision_phids); } return $map; } + public static function loadRevisionForCommit( + PhabricatorUser $viewer, + PhabricatorRepositoryCommit $commit) { + + $data = $commit->getCommitData(); + + $revision_id = $data->getCommitDetail('differential.revisionID'); + if (!$revision_id) { + return null; + } + + return id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->withIDs(array($revision_id)) + ->needReviewers(true) + ->executeOne(); + } + + } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php index 447355551c..9394ba6b71 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php @@ -1,378 +1,372 @@ shouldSkipImportStep()) { $this->publishCommit($repository, $commit); $commit->writeImportStatusFlag($this->getImportStepFlag()); } // This is the last task in the sequence, so we don't need to queue any // followup workers. } private function publishCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $publisher = $repository->newPublisher(); if (!$publisher->shouldPublishCommit($commit)) { return; } $commit_phid = $commit->getPHID(); // Reload the commit to get the commit data, identities, and any // outstanding audit requests. $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withPHIDs(array($commit_phid)) ->needCommitData(true) ->needIdentities(true) ->needAuditRequests(true) ->executeOne(); if (!$commit) { throw new PhabricatorWorkerPermanentFailureException( pht( 'Failed to reload commit "%s".', $commit_phid)); } $xactions = array( $this->newAuditTransactions($commit), $this->newPublishTransactions($commit), ); $xactions = array_mergev($xactions); $acting_phid = $this->getPublishAsPHID($commit); $content_source = $this->newContentSource(); $editor = $commit->getApplicationTransactionEditor() ->setActor($viewer) ->setActingAsPHID($acting_phid) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setContentSource($content_source); try { $raw_patch = $this->loadRawPatchText($repository, $commit); } catch (Exception $ex) { $raw_patch = pht('Unable to generate patch: %s', $ex->getMessage()); } $editor->setRawPatch($raw_patch); $editor->applyTransactions($commit, $xactions); } private function getPublishAsPHID(PhabricatorRepositoryCommit $commit) { if ($commit->hasCommitterIdentity()) { return $commit->getCommitterIdentity()->getIdentityDisplayPHID(); } if ($commit->hasAuthorIdentity()) { return $commit->getAuthorIdentity()->getIdentityDisplayPHID(); } return id(new PhabricatorDiffusionApplication())->getPHID(); } private function newPublishTransactions(PhabricatorRepositoryCommit $commit) { $data = $commit->getCommitData(); $xactions = array(); $xactions[] = $commit->getApplicationTransactionTemplate() ->setTransactionType(PhabricatorAuditTransaction::TYPE_COMMIT) ->setDateCreated($commit->getEpoch()) ->setNewValue( array( 'description' => $data->getCommitMessage(), 'summary' => $data->getSummary(), 'authorName' => $data->getAuthorName(), 'authorPHID' => $commit->getAuthorPHID(), 'committerName' => $data->getCommitDetail('committer'), 'committerPHID' => $data->getCommitDetail('committerPHID'), )); return $xactions; } private function newAuditTransactions(PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = $commit->getRepository(); $affected_paths = PhabricatorOwnerPathQuery::loadAffectedPaths( $repository, $commit, PhabricatorUser::getOmnipotentUser()); $affected_packages = PhabricatorOwnersPackage::loadAffectedPackages( $repository, $affected_paths); $commit->writeOwnersEdges(mpull($affected_packages, 'getPHID')); if (!$affected_packages) { return array(); } $data = $commit->getCommitData(); $author_phid = $data->getCommitDetail('authorPHID'); - $revision_id = $data->getCommitDetail('differential.revisionID'); - if ($revision_id) { - $revision = id(new DifferentialRevisionQuery()) - ->setViewer($viewer) - ->withIDs(array($revision_id)) - ->needReviewers(true) - ->executeOne(); - } else { - $revision = null; - } + + $revision = DiffusionCommitRevisionQuery::loadRevisionForCommit( + $viewer, + $commit); $requests = $commit->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); $auditor_phids = array(); foreach ($affected_packages as $package) { $request = idx($requests, $package->getPHID()); if ($request) { // Don't update request if it exists already. continue; } $should_audit = $this->shouldTriggerAudit( $commit, $package, $author_phid, $revision); if (!$should_audit) { continue; } $auditor_phids[] = $package->getPHID(); } // If none of the packages are triggering audits, we're all done. if (!$auditor_phids) { return array(); } $audit_type = DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE; $xactions = array(); $xactions[] = $commit->getApplicationTransactionTemplate() ->setTransactionType($audit_type) ->setNewValue( array( '+' => array_fuse($auditor_phids), )); return $xactions; } private function shouldTriggerAudit( PhabricatorRepositoryCommit $commit, PhabricatorOwnersPackage $package, $author_phid, $revision) { $audit_uninvolved = false; $audit_unreviewed = false; $rule = $package->newAuditingRule(); switch ($rule->getKey()) { case PhabricatorOwnersAuditRule::AUDITING_NONE: return false; case PhabricatorOwnersAuditRule::AUDITING_ALL: return true; case PhabricatorOwnersAuditRule::AUDITING_NO_OWNER: $audit_uninvolved = true; break; case PhabricatorOwnersAuditRule::AUDITING_UNREVIEWED: $audit_unreviewed = true; break; case PhabricatorOwnersAuditRule::AUDITING_NO_OWNER_AND_UNREVIEWED: $audit_uninvolved = true; $audit_unreviewed = true; break; } // If auditing is configured to trigger on unreviewed changes, check if // the revision was "Accepted" when it landed. If not, trigger an audit. if ($audit_unreviewed) { $commit_unreviewed = true; if ($revision) { $was_accepted = DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED; if ($revision->isPublished()) { if ($revision->getProperty($was_accepted)) { $commit_unreviewed = false; } } } if ($commit_unreviewed) { return true; } } // If auditing is configured to trigger on changes with no involved owner, // check for an owner. If we don't find one, trigger an audit. if ($audit_uninvolved) { $owner_involved = $this->isOwnerInvolved( $commit, $package, $author_phid, $revision); if (!$owner_involved) { return true; } } // We can't find any reason to trigger an audit for this commit. return false; } private function isOwnerInvolved( PhabricatorRepositoryCommit $commit, PhabricatorOwnersPackage $package, $author_phid, $revision) { $owner_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs( array( $package->getID(), )); $owner_phids = array_fuse($owner_phids); // For the purposes of deciding whether the owners were involved in the // revision or not, consider a review by the package itself to count as // involvement. This can happen when human reviewers force-accept on // behalf of packages they don't own but have authority over. $owner_phids[$package->getPHID()] = $package->getPHID(); // If the commit author is identifiable and a package owner, they're // involved. if ($author_phid) { if (isset($owner_phids[$author_phid])) { return true; } } // Otherwise, we need to find an owner as a reviewer. // If we don't have a revision, this is hopeless: no owners are involved. if (!$revision) { return true; } $accepted_statuses = array( DifferentialReviewerStatus::STATUS_ACCEPTED, DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER, ); $accepted_statuses = array_fuse($accepted_statuses); $found_accept = false; foreach ($revision->getReviewers() as $reviewer) { $reviewer_phid = $reviewer->getReviewerPHID(); // If this reviewer isn't a package owner or the package itself, // just ignore them. if (empty($owner_phids[$reviewer_phid])) { continue; } // If this reviewer accepted the revision and owns the package (or is // the package), we've found an involved owner. if (isset($accepted_statuses[$reviewer->getReviewerStatus()])) { $found_accept = true; break; } } if ($found_accept) { return true; } return false; } private function loadRawPatchText( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $identifier = $commit->getCommitIdentifier(); $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $viewer, 'repository' => $repository, )); $time_key = 'metamta.diffusion.time-limit'; $byte_key = 'metamta.diffusion.byte-limit'; $time_limit = PhabricatorEnv::getEnvConfig($time_key); $byte_limit = PhabricatorEnv::getEnvConfig($byte_key); $diff_info = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, $drequest, 'diffusion.rawdiffquery', array( 'commit' => $identifier, 'linesOfContext' => 3, 'timeout' => $time_limit, 'byteLimit' => $byte_limit, )); if ($diff_info['tooSlow']) { throw new Exception( pht( 'Patch generation took longer than configured limit ("%s") of '. '%s second(s).', $time_key, new PhutilNumber($time_limit))); } if ($diff_info['tooHuge']) { $pretty_limit = phutil_format_bytes($byte_limit); throw new Exception( pht( 'Patch size exceeds configured byte size limit ("%s") of %s.', $byte_key, $pretty_limit)); } $file_phid = $diff_info['filePHID']; $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file ("%s") returned by "%s".', $file_phid, 'diffusion.rawdiffquery')); } return $file->loadFileData(); } }