diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php index fe2e2f818b..a112650371 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php @@ -1,124 +1,125 @@ getRequest(); $viewer = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); // NOTE: We're inverting these here, because the storage is silly. $v_notify = !$repository->getHumanReadableDetail('herald-disabled'); $v_autoclose = !$repository->getHumanReadableDetail('disable-autoclose'); if ($request->isFormPost()) { $v_notify = $request->getBool('notify'); $v_autoclose = $request->getBool('autoclose'); $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_notify = PhabricatorRepositoryTransaction::TYPE_NOTIFY; $type_autoclose = PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE; $xactions[] = id(clone $template) ->setTransactionType($type_notify) ->setNewValue($v_notify); $xactions[] = id(clone $template) ->setTransactionType($type_autoclose) ->setNewValue($v_autoclose); id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($viewer) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } $content = array(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Edit Actions')); $title = pht('Edit Actions (%s)', $repository->getName()); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($repository) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions( pht( "Normally, Phabricator publishes notifications when it discovers ". "new commits. You can disable publishing for this repository by ". "turning off **Notify/Publish**. This will disable notifications, ". - "feed, and Herald for this repository.". + "feed, and Herald (including audits and build plans) for this ". + "repository.". "\n\n". "When Phabricator discovers a new commit, it can automatically ". "close associated revisions and tasks. If you don't want ". "Phabricator to close objects when it discovers new commits in ". "this repository, you can disable **Autoclose**.")) ->appendChild( id(new AphrontFormSelectControl()) ->setName('notify') ->setLabel(pht('Notify/Publish')) ->setValue((int)$v_notify) ->setOptions( array( 1 => pht('Enable Notifications, Feed and Herald'), 0 => pht('Disable Notifications, Feed and Herald'), ))) ->appendChild( id(new AphrontFormSelectControl()) ->setName('autoclose') ->setLabel(pht('Autoclose')) ->setValue((int)$v_autoclose) ->setOptions( array( 1 => pht('Enable Autoclose'), 0 => pht('Disable Autoclose'), ))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Actions')) ->addCancelButton($edit_uri)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php index 7c7ac2d0de..cdf4087196 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php @@ -1,475 +1,474 @@ applyHeraldRules($repository, $commit); $commit->writeImportStatusFlag( PhabricatorRepositoryCommit::IMPORTED_HERALD); return $result; } private function applyHeraldRules( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $commit->attachRepository($repository); // 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; } + if ($repository->getDetail('herald-disabled')) { + 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 = 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); // TODO: This should be integrated properly once we move to // ApplicationTransactions. $field_list = PhabricatorCustomField::getObjectFields( $commit, PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS); $field_list ->setViewer(PhabricatorUser::getOmnipotentUser()) ->readFieldsFromStorage($commit); foreach ($field_list->getFields() as $field) { try { $field->buildApplicationTransactionMailBody( new DifferentialTransaction(), // Bogus object to satisfy typehint. $body); } catch (Exception $ex) { // Log the exception and continue. phlog($ex); } } $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 = id(new PhabricatorObjectListQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->setAllowPartialResults(true) ->setAllowedTypes( array( PhabricatorPeoplePHIDTypeUser::TYPECONST, PhabricatorProjectPHIDTypeProject::TYPECONST, )) ->setObjectList($matches[1]) ->execute(); 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; } } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php index 71fa47fb30..e6b8630776 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php @@ -1,127 +1,131 @@ getDetail('herald-disabled')) { + return; + } + $affected_paths = PhabricatorOwnerPathQuery::loadAffectedPaths( $repository, $commit, PhabricatorUser::getOmnipotentUser()); $affected_packages = PhabricatorOwnersPackage::loadAffectedPackages( $repository, $affected_paths); if ($affected_packages) { $requests = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere( 'commitPHID = %s', $commit->getPHID()); $requests = mpull($requests, null, 'getAuditorPHID'); foreach ($affected_packages as $package) { $request = idx($requests, $package->getPHID()); if ($request) { // Don't update request if it exists already. continue; } if ($package->getAuditingEnabled()) { $reasons = $this->checkAuditReasons($commit, $package); if ($reasons) { $audit_status = PhabricatorAuditStatusConstants::AUDIT_REQUIRED; } else { $audit_status = PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED; } } else { $reasons = array(); $audit_status = PhabricatorAuditStatusConstants::NONE; } $relationship = new PhabricatorRepositoryAuditRequest(); $relationship->setAuditorPHID($package->getPHID()); $relationship->setCommitPHID($commit->getPHID()); $relationship->setAuditReasons($reasons); $relationship->setAuditStatus($audit_status); $relationship->save(); $requests[$package->getPHID()] = $relationship; } $commit->updateAuditStatus($requests); $commit->save(); } $commit->writeImportStatusFlag( PhabricatorRepositoryCommit::IMPORTED_OWNERS); if ($this->shouldQueueFollowupTasks()) { PhabricatorWorker::scheduleTask( 'PhabricatorRepositoryCommitHeraldWorker', array( 'commitID' => $commit->getID(), )); } } private function checkAuditReasons( PhabricatorRepositoryCommit $commit, PhabricatorOwnersPackage $package) { $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); $reasons = array(); if ($data->getCommitDetail('vsDiff')) { $reasons[] = "Changed After Revision Was Accepted"; } $commit_author_phid = $data->getCommitDetail('authorPHID'); if (!$commit_author_phid) { $reasons[] = "Commit Author Not Recognized"; } $revision_id = $data->getCommitDetail('differential.revisionID'); $revision_author_phid = null; $commit_reviewedby_phid = null; if ($revision_id) { // TODO: (T603) This is probably safe to use an omnipotent user on, // but check things more closely. $revision = id(new DifferentialRevision())->load($revision_id); if ($revision) { $revision_author_phid = $revision->getAuthorPHID(); $commit_reviewedby_phid = $data->getCommitDetail('reviewerPHID'); if ($revision_author_phid !== $commit_author_phid) { $reasons[] = "Author Not Matching with Revision"; } } else { $reasons[] = "Revision Not Found"; } } else { $reasons[] = "No Revision Specified"; } $owners_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs( array($package->getID())); if (!($commit_author_phid && in_array($commit_author_phid, $owners_phids) || $commit_reviewedby_phid && in_array($commit_reviewedby_phid, $owners_phids))) { $reasons[] = "Owners Not Involved"; } return $reasons; } }