Example #1
0
 public static function loadAndApplyRules(HeraldAdapter $adapter)
 {
     $engine = new HeraldEngine();
     $rules = $engine->loadRulesForAdapter($adapter);
     $effects = $engine->applyRules($rules, $adapter);
     $engine->applyEffects($effects, $adapter, $rules);
     return $engine->getTranscript();
 }
Example #2
0
 public static function loadAndApplyRules(HeraldObjectAdapter $object)
 {
     $content_type = $object->getHeraldTypeName();
     $rules = HeraldRule::loadAllByContentTypeWithFullData($content_type, $object->getPHID());
     $engine = new HeraldEngine();
     $effects = $engine->applyRules($rules, $object);
     $engine->applyEffects($effects, $object, $rules);
     return $engine->getTranscript();
 }
 public function saveDiff(DifferentialDiff $diff)
 {
     $actor = $this->requireActor();
     // Generate a PHID first, so the transcript will point at the object if
     // we deicde to preserve it.
     $phid = $diff->generatePHID();
     $diff->setPHID($phid);
     $adapter = id(new HeraldDifferentialDiffAdapter())->setDiff($diff);
     $adapter->setContentSource($this->getContentSource());
     $adapter->setIsNewObject(true);
     $engine = new HeraldEngine();
     $rules = $engine->loadRulesForAdapter($adapter);
     $rules = mpull($rules, null, 'getID');
     $effects = $engine->applyRules($rules, $adapter);
     $blocking_effect = null;
     foreach ($effects as $effect) {
         if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) {
             $blocking_effect = $effect;
             break;
         }
     }
     if ($blocking_effect) {
         $rule = idx($rules, $effect->getRuleID());
         if ($rule && strlen($rule->getName())) {
             $rule_name = $rule->getName();
         } else {
             $rule_name = pht('Unnamed Herald Rule');
         }
         $message = $effect->getTarget();
         if (!strlen($message)) {
             $message = pht('(None.)');
         }
         throw new DifferentialDiffCreationRejectException(pht("Creation of this diff was rejected by Herald rule %s.\n" . "  Rule: %s\n" . "Reason: %s", 'H' . $effect->getRuleID(), $rule_name, $message));
     }
     $diff->save();
     // NOTE: We only save the transcript if we didn't block the diff.
     // Otherwise, we might save some of the diff's content in the transcript
     // table, which would defeat the purpose of allowing rules to block
     // storage of key material.
     $engine->applyEffects($effects, $adapter, $rules);
     $xscript = $engine->getTranscript();
 }
 private function applyHeraldRules(array $updates, HeraldAdapter $adapter_template, array $all_updates)
 {
     if (!$updates) {
         return;
     }
     $adapter_template->setHookEngine($this);
     $engine = new HeraldEngine();
     $rules = null;
     $blocking_effect = null;
     $blocked_update = null;
     $blocking_xscript = null;
     foreach ($updates as $update) {
         $adapter = id(clone $adapter_template)->setPushLog($update);
         if ($rules === null) {
             $rules = $engine->loadRulesForAdapter($adapter);
         }
         $effects = $engine->applyRules($rules, $adapter);
         $engine->applyEffects($effects, $adapter, $rules);
         $xscript = $engine->getTranscript();
         // Store any PHIDs we want to send email to for later.
         foreach ($adapter->getEmailPHIDs() as $email_phid) {
             $this->emailPHIDs[$email_phid] = $email_phid;
         }
         $block_action = DiffusionBlockHeraldAction::ACTIONCONST;
         if ($blocking_effect === null) {
             foreach ($effects as $effect) {
                 if ($effect->getAction() == $block_action) {
                     $blocking_effect = $effect;
                     $blocked_update = $update;
                     $blocking_xscript = $xscript;
                     break;
                 }
             }
         }
     }
     if ($blocking_effect) {
         $rule = $blocking_effect->getRule();
         $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_HERALD;
         $this->rejectDetails = $rule->getPHID();
         $message = $blocking_effect->getTarget();
         if (!strlen($message)) {
             $message = pht('(None.)');
         }
         $blocked_ref_name = coalesce($blocked_update->getRefName(), $blocked_update->getRefNewShort());
         $blocked_name = $blocked_update->getRefType() . '/' . $blocked_ref_name;
         throw new DiffusionCommitHookRejectException(pht("This push was rejected by Herald push rule %s.\n" . "    Change: %s\n" . "      Rule: %s\n" . "    Reason: %s\n" . "Transcript: %s", $rule->getMonogram(), $blocked_name, $rule->getName(), $message, PhabricatorEnv::getProductionURI('/herald/transcript/' . $blocking_xscript->getID() . '/')));
     }
 }
 public function processRequest()
 {
     $request = $this->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 = 'Required';
             $errors[] = 'An object name is required.';
         }
         if (!$errors) {
             $matches = null;
             $object = null;
             if (preg_match('/^D(\\d+)$/', $object_name, $matches)) {
                 $object = id(new DifferentialRevision())->load($matches[1]);
                 if (!$object) {
                     $e_name = 'Invalid';
                     $errors[] = 'No Differential Revision with that ID exists.';
                 }
             } else {
                 if (preg_match('/^r([A-Z]+)(\\w+)$/', $object_name, $matches)) {
                     $repo = id(new PhabricatorRepository())->loadOneWhere('callsign = %s', $matches[1]);
                     if (!$repo) {
                         $e_name = 'Invalid';
                         $errors[] = 'There is no repository with the callsign ' . $matches[1] . '.';
                     }
                     $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere('repositoryID = %d AND commitIdentifier = %s', $repo->getID(), $matches[2]);
                     if (!$commit) {
                         $e_name = 'Invalid';
                         $errors[] = 'There is no commit with that identifier.';
                     }
                     $object = $commit;
                 } else {
                     $e_name = 'Invalid';
                     $errors[] = 'This object name is not recognized.';
                 }
             }
             if (!$errors) {
                 if ($object instanceof DifferentialRevision) {
                     $adapter = new HeraldDifferentialRevisionAdapter($object, $object->loadActiveDiff());
                 } else {
                     if ($object instanceof PhabricatorRepositoryCommit) {
                         $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $object->getID());
                         $adapter = new HeraldCommitAdapter($repo, $object, $data);
                     } else {
                         throw new Exception("Can not build adapter for object!");
                     }
                 }
                 $rules = HeraldRule::loadAllByContentTypeWithFullData($adapter->getHeraldTypeName());
                 $engine = new HeraldEngine();
                 $effects = $engine->applyRules($rules, $adapter);
                 $dry_run = new HeraldDryRunAdapter();
                 $engine->applyEffects($effects, $dry_run);
                 $xscript = $engine->getTranscript();
                 return id(new AphrontRedirectResponse())->setURI('/herald/transcript/' . $xscript->getID() . '/');
             }
         }
     }
     if ($errors) {
         $error_view = new AphrontErrorView();
         $error_view->setTitle('Form Errors');
         $error_view->setErrors($errors);
     } else {
         $error_view = null;
     }
     $form = id(new AphrontFormView())->setUser($user)->appendChild('<p class="aphront-form-instructions">Enter an object to test rules ' . 'for, like a Diffusion commit (e.g., <tt>rX123</tt>) or a ' . 'Differential revision (e.g., <tt>D123</tt>). You will be shown the ' . 'results of a dry run on the object.</p>')->appendChild(id(new AphrontFormTextControl())->setLabel('Object Name')->setName('object_name')->setError($e_name)->setValue($object_name))->appendChild(id(new AphrontFormSubmitControl())->setValue('Test Rules'));
     $panel = new AphrontPanelView();
     $panel->setHeader('Test Herald Rules');
     $panel->setWidth(AphrontPanelView::WIDTH_FULL);
     $panel->appendChild($form);
     return $this->buildStandardPageResponse(array($error_view, $panel), array('title' => 'Test Console', 'tab' => 'test'));
 }
    public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit)
    {
        if ($repository->getDetail('herald-disabled')) {
            return;
        }
        $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID());
        $rules = HeraldRule::loadAllByContentTypeWithFullData(HeraldContentTypeConfig::CONTENT_TYPE_COMMIT);
        $adapter = new HeraldCommitAdapter($repository, $commit, $data);
        $engine = new HeraldEngine();
        $effects = $engine->applyRules($rules, $adapter);
        $engine->applyEffects($effects, $adapter);
        $email_phids = $adapter->getEmailPHIDs();
        if (!$email_phids) {
            return;
        }
        $xscript = $engine->getTranscript();
        $commit_name = $adapter->getHeraldName();
        $revision = $adapter->loadDifferentialRevision();
        $name = null;
        if ($revision) {
            $name = ' ' . $revision->getTitle();
        }
        $author_phid = $data->getCommitDetail('authorPHID');
        $reviewer_phid = $data->getCommitDetail('reviewerPHID');
        $phids = array_filter(array($author_phid, $reviewer_phid));
        $handles = array();
        if ($phids) {
            $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
        }
        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();
        $details = PhabricatorEnv::getProductionURI('/' . $commit_name);
        $differential = $revision ? PhabricatorEnv::getProductionURI('/D' . $revision->getID()) : 'No revision.';
        $files = $adapter->loadAffectedPaths();
        sort($files);
        $files = implode("\n  ", $files);
        $xscript_id = $xscript->getID();
        $manage_uri = PhabricatorEnv::getProductionURI('/herald/view/commits/');
        $why_uri = PhabricatorEnv::getProductionURI('/herald/transcript/' . $xscript_id . '/');
        $body = <<<EOBODY
DESCRIPTION
{$description}

DETAILS
  {$details}

DIFFERENTIAL REVISION
  {$differential}

AFFECTED FILES
  {$files}

MANAGE HERALD COMMIT RULES
  {$manage_uri}

WHY DID I GET THIS EMAIL?
  {$why_uri}

EOBODY;
        $subject = "[Herald/Commit] {$commit_name} ({$who}){$name}";
        $mailer = new PhabricatorMetaMTAMail();
        $mailer->setRelatedPHID($commit->getPHID());
        $mailer->addTos($email_phids);
        $mailer->setSubject($subject);
        $mailer->setBody($body);
        $mailer->addHeader('X-Herald-Rules', $xscript->getXHeraldRulesHeader());
        if ($author_phid) {
            $mailer->setFrom($author_phid);
        }
        $mailer->saveAndSend();
    }
 /**
  * @task mail
  */
 private function runHeraldMailRules(array $messages)
 {
     foreach ($messages as $message) {
         $engine = new HeraldEngine();
         $adapter = id(new PhabricatorMailOutboundMailHeraldAdapter())->setObject($message);
         $rules = $engine->loadRulesForAdapter($adapter);
         $effects = $engine->applyRules($rules, $adapter);
         $engine->applyEffects($effects, $adapter, $rules);
     }
 }
 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);
     $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.';
     $limit = self::MAX_FILES_SHOWN_IN_EMAIL;
     $files = $adapter->loadAffectedPaths();
     sort($files);
     if (count($files) > $limit) {
         array_splice($files, $limit);
         $files[] = '(This commit affected more than ' . $limit . ' files. ' . 'Only ' . $limit . ' are shown here and additional ones are truncated.)';
     }
     $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(), $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();
     }
 }
 public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit)
 {
     $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID());
     if (!$data) {
         // TODO: Permanent failure.
         return;
     }
     $rules = HeraldRule::loadAllByContentTypeWithFullData(HeraldContentTypeConfig::CONTENT_TYPE_COMMIT, $commit->getPHID());
     $adapter = new HeraldCommitAdapter($repository, $commit, $data);
     $engine = new HeraldEngine();
     $effects = $engine->applyRules($rules, $adapter);
     $engine->applyEffects($effects, $adapter, $rules);
     $audit_phids = $adapter->getAuditMap();
     if ($audit_phids) {
         $this->createAudits($commit, $audit_phids, $rules);
     }
     $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, $herald_targets));
     if (!$email_phids) {
         return;
     }
     $xscript = $engine->getTranscript();
     $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 PhabricatorObjectHandleData($phids))->loadHandles();
     $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();
     $manage_uri = '/herald/view/commits/';
     $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($manage_uri, $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);
     }
     $mails = $reply_handler->multiplexMail($template, id(new PhabricatorObjectHandleData($email_phids))->loadHandles(), array());
     foreach ($mails as $mail) {
         $mail->saveAndSend();
     }
 }
    public function parseCommit(PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit)
    {
        $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere('commitID = %d', $commit->getID());
        if (!$data) {
            // TODO: Permanent failure.
            return;
        }
        $rules = HeraldRule::loadAllByContentTypeWithFullData(HeraldContentTypeConfig::CONTENT_TYPE_COMMIT, $commit->getPHID());
        $adapter = new HeraldCommitAdapter($repository, $commit, $data);
        $engine = new HeraldEngine();
        $effects = $engine->applyRules($rules, $adapter);
        $engine->applyEffects($effects, $adapter, $rules);
        $audit_phids = $adapter->getAuditMap();
        if ($audit_phids) {
            $this->createAudits($commit, $audit_phids, $rules);
        }
        $this->createAuditsFromCommitMessage($commit, $data);
        $email_phids = $adapter->getEmailPHIDs();
        if (!$email_phids) {
            return;
        }
        if ($repository->getDetail('herald-disabled')) {
            // This just means "disable email"; audits are (mostly) idempotent.
            return;
        }
        $xscript = $engine->getTranscript();
        $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 PhabricatorObjectHandleData($phids))->loadHandles();
        $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();
        $manage_uri = PhabricatorEnv::getProductionURI('/herald/view/commits/');
        $why_uri = PhabricatorEnv::getProductionURI('/herald/transcript/' . $xscript_id . '/');
        $reply_handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit($commit);
        $reply_instructions = $reply_handler->getReplyHandlerInstructions();
        if ($reply_instructions) {
            $reply_instructions = "\n" . "REPLY HANDLER ACTIONS\n" . "  " . $reply_instructions . "\n";
        }
        $body = <<<EOBODY
DESCRIPTION
{$description}

DETAILS
  {$commit_uri}

DIFFERENTIAL REVISION
  {$differential}

AFFECTED FILES
  {$files}
{$reply_instructions}
MANAGE HERALD COMMIT RULES
  {$manage_uri}

WHY DID I GET THIS EMAIL?
  {$why_uri}

EOBODY;
        $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix');
        $subject = trim("{$prefix} {$commit_name}: {$name}");
        $vary_subject = trim("{$prefix} [Commit] {$commit_name}: {$name}");
        $threading = PhabricatorAuditCommentEditor::getMailThreading($commit->getPHID());
        list($thread_id, $thread_topic) = $threading;
        $template = new PhabricatorMetaMTAMail();
        $template->setRelatedPHID($commit->getPHID());
        $template->setSubject($subject);
        $template->setVarySubject($subject);
        $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);
        }
        $mails = $reply_handler->multiplexMail($template, id(new PhabricatorObjectHandleData($email_phids))->loadHandles(), array());
        foreach ($mails as $mail) {
            $mail->saveAndSend();
        }
    }