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() . '/')));
     }
 }
 private function applyHeraldRules(PhabricatorLiskDAO $object, array $xactions)
 {
     $adapter = $this->buildHeraldAdapter($object, $xactions);
     $adapter->setContentSource($this->getContentSource());
     $adapter->setIsNewObject($this->getIsNewObject());
     $xscript = HeraldEngine::loadAndApplyRules($adapter);
     $this->setHeraldAdapter($adapter);
     $this->setHeraldTranscript($xscript);
     return array_merge($this->didApplyHeraldRules($object, $adapter, $xscript), $adapter->getQueuedTransactions());
 }
 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();
    }
 public function doesConditionMatch(HeraldEngine $engine, HeraldRule $rule, HeraldCondition $condition, $field_value)
 {
     $condition_type = $condition->getFieldCondition();
     $condition_value = $condition->getValue();
     switch ($condition_type) {
         case self::CONDITION_CONTAINS:
             // "Contains" can take an array of strings, as in "Any changed
             // filename" for diffs.
             foreach ((array) $field_value as $value) {
                 if (stripos($value, $condition_value) !== false) {
                     return true;
                 }
             }
             return false;
         case self::CONDITION_NOT_CONTAINS:
             return stripos($field_value, $condition_value) === false;
         case self::CONDITION_IS:
             return $field_value == $condition_value;
         case self::CONDITION_IS_NOT:
             return $field_value != $condition_value;
         case self::CONDITION_IS_ME:
             return $field_value == $rule->getAuthorPHID();
         case self::CONDITION_IS_NOT_ME:
             return $field_value != $rule->getAuthorPHID();
         case self::CONDITION_IS_ANY:
             if (!is_array($condition_value)) {
                 throw new HeraldInvalidConditionException(pht('Expected condition value to be an array.'));
             }
             $condition_value = array_fuse($condition_value);
             return isset($condition_value[$field_value]);
         case self::CONDITION_IS_NOT_ANY:
             if (!is_array($condition_value)) {
                 throw new HeraldInvalidConditionException(pht('Expected condition value to be an array.'));
             }
             $condition_value = array_fuse($condition_value);
             return !isset($condition_value[$field_value]);
         case self::CONDITION_INCLUDE_ALL:
             if (!is_array($field_value)) {
                 throw new HeraldInvalidConditionException(pht('Object produced non-array value!'));
             }
             if (!is_array($condition_value)) {
                 throw new HeraldInvalidConditionException(pht('Expected condition value to be an array.'));
             }
             $have = array_select_keys(array_fuse($field_value), $condition_value);
             return count($have) == count($condition_value);
         case self::CONDITION_INCLUDE_ANY:
             return (bool) array_select_keys(array_fuse($field_value), $condition_value);
         case self::CONDITION_INCLUDE_NONE:
             return !array_select_keys(array_fuse($field_value), $condition_value);
         case self::CONDITION_EXISTS:
         case self::CONDITION_IS_TRUE:
             return (bool) $field_value;
         case self::CONDITION_NOT_EXISTS:
         case self::CONDITION_IS_FALSE:
             return !$field_value;
         case self::CONDITION_UNCONDITIONALLY:
             return (bool) $field_value;
         case self::CONDITION_NEVER:
             return false;
         case self::CONDITION_REGEXP:
             foreach ((array) $field_value as $value) {
                 // We add the 'S' flag because we use the regexp multiple times.
                 // It shouldn't cause any troubles if the flag is already there
                 // - /.*/S is evaluated same as /.*/SS.
                 $result = @preg_match($condition_value . 'S', $value);
                 if ($result === false) {
                     throw new HeraldInvalidConditionException(pht('Regular expression is not valid!'));
                 }
                 if ($result) {
                     return true;
                 }
             }
             return false;
         case self::CONDITION_REGEXP_PAIR:
             // Match a JSON-encoded pair of regular expressions against a
             // dictionary. The first regexp must match the dictionary key, and the
             // second regexp must match the dictionary value. If any key/value pair
             // in the dictionary matches both regexps, the condition is satisfied.
             $regexp_pair = null;
             try {
                 $regexp_pair = phutil_json_decode($condition_value);
             } catch (PhutilJSONParserException $ex) {
                 throw new HeraldInvalidConditionException(pht('Regular expression pair is not valid JSON!'));
             }
             if (count($regexp_pair) != 2) {
                 throw new HeraldInvalidConditionException(pht('Regular expression pair is not a pair!'));
             }
             $key_regexp = array_shift($regexp_pair);
             $value_regexp = array_shift($regexp_pair);
             foreach ((array) $field_value as $key => $value) {
                 $key_matches = @preg_match($key_regexp, $key);
                 if ($key_matches === false) {
                     throw new HeraldInvalidConditionException(pht('First regular expression is invalid!'));
                 }
                 if ($key_matches) {
                     $value_matches = @preg_match($value_regexp, $value);
                     if ($value_matches === false) {
                         throw new HeraldInvalidConditionException(pht('Second regular expression is invalid!'));
                     }
                     if ($value_matches) {
                         return true;
                     }
                 }
             }
             return false;
         case self::CONDITION_RULE:
         case self::CONDITION_NOT_RULE:
             $rule = $engine->getRule($condition_value);
             if (!$rule) {
                 throw new HeraldInvalidConditionException(pht('Condition references a rule which does not exist!'));
             }
             $is_not = $condition_type == self::CONDITION_NOT_RULE;
             $result = $engine->doesRuleMatch($rule, $this);
             if ($is_not) {
                 $result = !$result;
             }
             return $result;
         case self::CONDITION_HAS_BIT:
             return ($condition_value & $field_value) === (int) $condition_value;
         case self::CONDITION_NOT_BIT:
             return ($condition_value & $field_value) !== (int) $condition_value;
         default:
             throw new HeraldInvalidConditionException(pht("Unknown condition '%s'.", $condition_type));
     }
 }
 private function applyHeraldRules(PhabricatorLiskDAO $object, array $xactions)
 {
     $adapter = $this->buildHeraldAdapter($object, $xactions)->setContentSource($this->getContentSource())->setIsNewObject($this->getIsNewObject())->setAppliedTransactions($xactions);
     if ($this->getApplicationEmail()) {
         $adapter->setApplicationEmail($this->getApplicationEmail());
     }
     $xscript = HeraldEngine::loadAndApplyRules($adapter);
     $this->setHeraldAdapter($adapter);
     $this->setHeraldTranscript($xscript);
     if ($adapter instanceof HarbormasterBuildableAdapterInterface) {
         HarbormasterBuildable::applyBuildPlans($adapter->getHarbormasterBuildablePHID(), $adapter->getHarbormasterContainerPHID(), $adapter->getQueuedHarbormasterBuildRequests());
     }
     return array_merge($this->didApplyHeraldRules($object, $adapter, $xscript), $adapter->getQueuedTransactions());
 }
 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();
     }
 }
 /**
  * We run Herald as part of transaction validation because Herald can
  * block diff creation for Differential diffs. Its important to do this
  * separately so no Herald logs are saved; these logs could expose
  * information the Herald rules are inteneded to block.
  */
 protected function validateTransaction(PhabricatorLiskDAO $object, $type, array $xactions)
 {
     $errors = parent::validateTransaction($object, $type, $xactions);
     foreach ($xactions as $xaction) {
         switch ($type) {
             case DifferentialDiffTransaction::TYPE_DIFF_CREATE:
                 $diff = clone $object;
                 $diff = $this->updateDiffFromDict($diff, $xaction->getNewValue());
                 $adapter = $this->buildHeraldAdapter($diff, $xactions);
                 $adapter->setContentSource($this->getContentSource());
                 $adapter->setIsNewObject($this->getIsNewObject());
                 $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 = $blocking_effect->getRule();
                     $message = $effect->getTarget();
                     if (!strlen($message)) {
                         $message = pht('(None.)');
                     }
                     $errors[] = new PhabricatorApplicationTransactionValidationError($type, pht('Rejected by Herald'), pht("Creation of this diff was rejected by Herald rule %s.\n" . "  Rule: %s\n" . "Reason: %s", $rule->getMonogram(), $rule->getName(), $message));
                 }
                 break;
         }
     }
     return $errors;
 }
 public function save()
 {
     $revision = $this->getRevision();
     $is_new = $this->isNewRevision();
     if ($is_new) {
         $this->initializeNewRevision($revision);
     }
     $revision->loadRelationships();
     $this->willWriteRevision();
     if ($this->reviewers === null) {
         $this->reviewers = $revision->getReviewers();
     }
     if ($this->cc === null) {
         $this->cc = $revision->getCCPHIDs();
     }
     $diff = $this->getDiff();
     if ($diff) {
         $revision->setLineCount($diff->getLineCount());
     }
     // Save the revision, to generate its ID and PHID if it is new. We need
     // the ID/PHID in order to record them in Herald transcripts, but don't
     // want to hold a transaction open while running Herald because it is
     // potentially somewhat slow. The downside is that we may end up with a
     // saved revision/diff pair without appropriate CCs. We could be better
     // about this -- for example:
     //
     //  - Herald can't affect reviewers, so we could compute them before
     //    opening the transaction and then save them in the transaction.
     //  - Herald doesn't *really* need PHIDs to compute its effects, we could
     //    run it before saving these objects and then hand over the PHIDs later.
     //
     // But this should address the problem of orphaned revisions, which is
     // currently the only problem we experience in practice.
     $revision->openTransaction();
     if ($diff) {
         $revision->setBranchName($diff->getBranch());
         $revision->setArcanistProjectPHID($diff->getArcanistProjectPHID());
     }
     $revision->save();
     if ($diff) {
         $diff->setRevisionID($revision->getID());
         $diff->save();
     }
     $revision->saveTransaction();
     // We're going to build up three dictionaries: $add, $rem, and $stable. The
     // $add dictionary has added reviewers/CCs. The $rem dictionary has
     // reviewers/CCs who have been removed, and the $stable array is
     // reviewers/CCs who haven't changed. We're going to send new reviewers/CCs
     // a different ("welcome") email than we send stable reviewers/CCs.
     $old = array('rev' => array_fill_keys($revision->getReviewers(), true), 'ccs' => array_fill_keys($revision->getCCPHIDs(), true));
     $xscript_header = null;
     $xscript_uri = null;
     $new = array('rev' => array_fill_keys($this->reviewers, true), 'ccs' => array_fill_keys($this->cc, true));
     $rem_ccs = array();
     $xscript_phid = null;
     if ($diff) {
         $adapter = new HeraldDifferentialRevisionAdapter($revision, $diff);
         $adapter->setExplicitCCs($new['ccs']);
         $adapter->setExplicitReviewers($new['rev']);
         $adapter->setForbiddenCCs($revision->getUnsubscribedPHIDs());
         $xscript = HeraldEngine::loadAndApplyRules($adapter);
         $xscript_uri = '/herald/transcript/' . $xscript->getID() . '/';
         $xscript_phid = $xscript->getPHID();
         $xscript_header = $xscript->getXHeraldRulesHeader();
         $xscript_header = HeraldTranscript::saveXHeraldRulesHeader($revision->getPHID(), $xscript_header);
         $sub = array('rev' => array(), 'ccs' => $adapter->getCCsAddedByHerald());
         $rem_ccs = $adapter->getCCsRemovedByHerald();
     } else {
         $sub = array('rev' => array(), 'ccs' => array());
     }
     // Remove any CCs which are prevented by Herald rules.
     $sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs);
     $new['ccs'] = array_diff_key($new['ccs'], $rem_ccs);
     $add = array();
     $rem = array();
     $stable = array();
     foreach (array('rev', 'ccs') as $key) {
         $add[$key] = array();
         if ($new[$key] !== null) {
             $add[$key] += array_diff_key($new[$key], $old[$key]);
         }
         $add[$key] += array_diff_key($sub[$key], $old[$key]);
         $combined = $sub[$key];
         if ($new[$key] !== null) {
             $combined += $new[$key];
         }
         $rem[$key] = array_diff_key($old[$key], $combined);
         $stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]);
     }
     self::alterReviewers($revision, $this->reviewers, array_keys($rem['rev']), array_keys($add['rev']), $this->actorPHID);
     // We want to attribute new CCs to a "reasonPHID", representing the reason
     // they were added. This is either a user (if some user explicitly CCs
     // them, or uses "Add CCs...") or a Herald transcript PHID, indicating that
     // they were added by a Herald rule.
     if ($add['ccs'] || $rem['ccs']) {
         $reasons = array();
         foreach ($add['ccs'] as $phid => $ignored) {
             if (empty($new['ccs'][$phid])) {
                 $reasons[$phid] = $xscript_phid;
             } else {
                 $reasons[$phid] = $this->actorPHID;
             }
         }
         foreach ($rem['ccs'] as $phid => $ignored) {
             if (empty($new['ccs'][$phid])) {
                 $reasons[$phid] = $this->actorPHID;
             } else {
                 $reasons[$phid] = $xscript_phid;
             }
         }
     } else {
         $reasons = $this->actorPHID;
     }
     self::alterCCs($revision, $this->cc, array_keys($rem['ccs']), array_keys($add['ccs']), $reasons);
     $this->updateAuxiliaryFields();
     // Add the author and users included from Herald rules to the relevant set
     // of users so they get a copy of the email.
     if (!$this->silentUpdate) {
         if ($is_new) {
             $add['rev'][$this->getActorPHID()] = true;
             if ($diff) {
                 $add['rev'] += $adapter->getEmailPHIDsAddedByHerald();
             }
         } else {
             $stable['rev'][$this->getActorPHID()] = true;
             if ($diff) {
                 $stable['rev'] += $adapter->getEmailPHIDsAddedByHerald();
             }
         }
     }
     $mail = array();
     $phids = array($this->getActorPHID());
     $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
     $actor_handle = $handles[$this->getActorPHID()];
     $changesets = null;
     $comment = null;
     if ($diff) {
         $changesets = $diff->loadChangesets();
         // TODO: This should probably be in DifferentialFeedbackEditor?
         if (!$is_new) {
             $comment = $this->createComment();
         }
         if ($comment) {
             $mail[] = id(new DifferentialNewDiffMail($revision, $actor_handle, $changesets))->setIsFirstMailAboutRevision($is_new)->setIsFirstMailToRecipients($is_new)->setComments($this->getComments())->setToPHIDs(array_keys($stable['rev']))->setCCPHIDs(array_keys($stable['ccs']));
         }
         // Save the changes we made above.
         $diff->setDescription(preg_replace('/\\n.*/s', '', $this->getComments()));
         $diff->save();
         $this->updateAffectedPathTable($revision, $diff, $changesets);
         $this->updateRevisionHashTable($revision, $diff);
         // An updated diff should require review, as long as it's not closed
         // or accepted. The "accepted" status is "sticky" to encourage courtesy
         // re-diffs after someone accepts with minor changes/suggestions.
         $status = $revision->getStatus();
         if ($status != ArcanistDifferentialRevisionStatus::CLOSED && $status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
             $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
         }
     } else {
         $diff = $revision->loadActiveDiff();
         if ($diff) {
             $changesets = $diff->loadChangesets();
         } else {
             $changesets = array();
         }
     }
     $revision->save();
     $this->didWriteRevision();
     $event_data = array('revision_id' => $revision->getID(), 'revision_phid' => $revision->getPHID(), 'revision_name' => $revision->getTitle(), 'revision_author_phid' => $revision->getAuthorPHID(), 'action' => $is_new ? DifferentialAction::ACTION_CREATE : DifferentialAction::ACTION_UPDATE, 'feedback_content' => $is_new ? phutil_utf8_shorten($revision->getSummary(), 140) : $this->getComments(), 'actor_phid' => $revision->getAuthorPHID());
     id(new PhabricatorTimelineEvent('difx', $event_data))->recordEvent();
     id(new PhabricatorFeedStoryPublisher())->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL)->setStoryData($event_data)->setStoryTime(time())->setStoryAuthorPHID($revision->getAuthorPHID())->setRelatedPHIDs(array($revision->getPHID(), $revision->getAuthorPHID()))->setPrimaryObjectPHID($revision->getPHID())->setSubscribedPHIDs(array_merge(array($revision->getAuthorPHID()), $revision->getReviewers(), $revision->getCCPHIDs()))->publish();
     //  TODO: Move this into a worker task thing.
     PhabricatorSearchDifferentialIndexer::indexRevision($revision);
     if ($this->silentUpdate) {
         return;
     }
     $revision->loadRelationships();
     if ($add['rev']) {
         $message = id(new DifferentialNewDiffMail($revision, $actor_handle, $changesets))->setIsFirstMailAboutRevision($is_new)->setIsFirstMailToRecipients(true)->setToPHIDs(array_keys($add['rev']));
         if ($is_new) {
             // The first time we send an email about a revision, put the CCs in
             // the "CC:" field of the same "Review Requested" email that reviewers
             // get, so you don't get two initial emails if you're on a list that
             // is CC'd.
             $message->setCCPHIDs(array_keys($add['ccs']));
         }
         $mail[] = $message;
     }
     // If we added CCs, we want to send them an email, but only if they were not
     // already a reviewer and were not added as one (in these cases, they got
     // a "NewDiff" mail, either in the past or just a moment ago). You can still
     // get two emails, but only if a revision is updated and you are added as a
     // reviewer at the same time a list you are on is added as a CC, which is
     // rare and reasonable.
     $implied_ccs = self::getImpliedCCs($revision);
     $implied_ccs = array_fill_keys($implied_ccs, true);
     $add['ccs'] = array_diff_key($add['ccs'], $implied_ccs);
     if (!$is_new && $add['ccs']) {
         $mail[] = id(new DifferentialCCWelcomeMail($revision, $actor_handle, $changesets))->setIsFirstMailToRecipients(true)->setToPHIDs(array_keys($add['ccs']));
     }
     foreach ($mail as $message) {
         $message->setHeraldTranscriptURI($xscript_uri);
         $message->setXHeraldRulesHeader($xscript_header);
         $message->send();
     }
 }
 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 save()
 {
     $revision = $this->getRevision();
     // TODO
     //    $revision->openTransaction();
     $is_new = $this->isNewRevision();
     if ($is_new) {
         // These fields aren't nullable; set them to sensible defaults if they
         // haven't been configured. We're just doing this so we can generate an
         // ID for the revision if we don't have one already.
         $revision->setLineCount(0);
         if ($revision->getStatus() === null) {
             $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
         }
         if ($revision->getTitle() === null) {
             $revision->setTitle('Untitled Revision');
         }
         if ($revision->getAuthorPHID() === null) {
             $revision->setAuthorPHID($this->getActorPHID());
         }
         if ($revision->getSummary() === null) {
             $revision->setSummary('');
         }
         if ($revision->getTestPlan() === null) {
             $revision->setTestPlan('');
         }
         $revision->save();
     }
     $revision->loadRelationships();
     $this->willWriteRevision();
     if ($this->reviewers === null) {
         $this->reviewers = $revision->getReviewers();
     }
     if ($this->cc === null) {
         $this->cc = $revision->getCCPHIDs();
     }
     // We're going to build up three dictionaries: $add, $rem, and $stable. The
     // $add dictionary has added reviewers/CCs. The $rem dictionary has
     // reviewers/CCs who have been removed, and the $stable array is
     // reviewers/CCs who haven't changed. We're going to send new reviewers/CCs
     // a different ("welcome") email than we send stable reviewers/CCs.
     $old = array('rev' => array_fill_keys($revision->getReviewers(), true), 'ccs' => array_fill_keys($revision->getCCPHIDs(), true));
     $diff = $this->getDiff();
     $xscript_header = null;
     $xscript_uri = null;
     $new = array('rev' => array_fill_keys($this->reviewers, true), 'ccs' => array_fill_keys($this->cc, true));
     $rem_ccs = array();
     if ($diff) {
         $diff->setRevisionID($revision->getID());
         $revision->setLineCount($diff->getLineCount());
         $adapter = new HeraldDifferentialRevisionAdapter($revision, $diff);
         $adapter->setExplicitCCs($new['ccs']);
         $adapter->setExplicitReviewers($new['rev']);
         $adapter->setForbiddenCCs($revision->getUnsubscribedPHIDs());
         $xscript = HeraldEngine::loadAndApplyRules($adapter);
         $xscript_uri = PhabricatorEnv::getProductionURI('/herald/transcript/' . $xscript->getID() . '/');
         $xscript_phid = $xscript->getPHID();
         $xscript_header = $xscript->getXHeraldRulesHeader();
         $xscript_header = HeraldTranscript::saveXHeraldRulesHeader($revision->getPHID(), $xscript_header);
         $sub = array('rev' => array(), 'ccs' => $adapter->getCCsAddedByHerald());
         $rem_ccs = $adapter->getCCsRemovedByHerald();
     } else {
         $sub = array('rev' => array(), 'ccs' => array());
     }
     // Remove any CCs which are prevented by Herald rules.
     $sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs);
     $new['ccs'] = array_diff_key($new['ccs'], $rem_ccs);
     $add = array();
     $rem = array();
     $stable = array();
     foreach (array('rev', 'ccs') as $key) {
         $add[$key] = array();
         if ($new[$key] !== null) {
             $add[$key] += array_diff_key($new[$key], $old[$key]);
         }
         $add[$key] += array_diff_key($sub[$key], $old[$key]);
         $combined = $sub[$key];
         if ($new[$key] !== null) {
             $combined += $new[$key];
         }
         $rem[$key] = array_diff_key($old[$key], $combined);
         $stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]);
     }
     self::alterReviewers($revision, $this->reviewers, array_keys($rem['rev']), array_keys($add['rev']), $this->actorPHID);
     /*
     
         // TODO: When Herald is brought over, run through this stuff to figure
         // out which adds are Herald's fault.
     
         // TODO: Still need to do this.
     
         if ($add['ccs'] || $rem['ccs']) {
           foreach (array_keys($add['ccs']) as $id) {
             if (empty($new['ccs'][$id])) {
               $reason_phid = 'TODO';//$xscript_phid;
             } else {
               $reason_phid = $this->getActorPHID();
             }
           }
           foreach (array_keys($rem['ccs']) as $id) {
             if (empty($new['ccs'][$id])) {
               $reason_phid = $this->getActorPHID();
             } else {
               $reason_phid = 'TODO';//$xscript_phid;
             }
           }
         }
     */
     self::alterCCs($revision, $this->cc, array_keys($rem['ccs']), array_keys($add['ccs']), $this->actorPHID);
     $this->updateAuxiliaryFields();
     // Add the author and users included from Herald rules to the relevant set
     // of users so they get a copy of the email.
     if (!$this->silentUpdate) {
         if ($is_new) {
             $add['rev'][$this->getActorPHID()] = true;
             if ($diff) {
                 $add['rev'] += $adapter->getEmailPHIDsAddedByHerald();
             }
         } else {
             $stable['rev'][$this->getActorPHID()] = true;
             if ($diff) {
                 $stable['rev'] += $adapter->getEmailPHIDsAddedByHerald();
             }
         }
     }
     $mail = array();
     $phids = array($this->getActorPHID());
     $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
     $actor_handle = $handles[$this->getActorPHID()];
     $changesets = null;
     $comment = null;
     if ($diff) {
         $changesets = $diff->loadChangesets();
         // TODO: This should probably be in DifferentialFeedbackEditor?
         if (!$is_new) {
             $comment = $this->createComment();
         }
         if ($comment) {
             $mail[] = id(new DifferentialNewDiffMail($revision, $actor_handle, $changesets))->setIsFirstMailAboutRevision($is_new)->setIsFirstMailToRecipients($is_new)->setComments($this->getComments())->setToPHIDs(array_keys($stable['rev']))->setCCPHIDs(array_keys($stable['ccs']));
         }
         // Save the changes we made above.
         $diff->setDescription(substr($this->getComments(), 0, 80));
         $diff->save();
         // An updated diff should require review, as long as it's not committed
         // or accepted. The "accepted" status is "sticky" to encourage courtesy
         // re-diffs after someone accepts with minor changes/suggestions.
         $status = $revision->getStatus();
         if ($status != DifferentialRevisionStatus::COMMITTED && $status != DifferentialRevisionStatus::ACCEPTED) {
             $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
         }
     } else {
         $diff = $revision->loadActiveDiff();
         if ($diff) {
             $changesets = $diff->loadChangesets();
         } else {
             $changesets = array();
         }
     }
     $revision->save();
     $this->didWriteRevision();
     $event_data = array('revision_id' => $revision->getID(), 'revision_phid' => $revision->getPHID(), 'revision_name' => $revision->getTitle(), 'revision_author_phid' => $revision->getAuthorPHID(), 'action' => $is_new ? DifferentialAction::ACTION_CREATE : DifferentialAction::ACTION_UPDATE, 'feedback_content' => $is_new ? phutil_utf8_shorten($revision->getSummary(), 140) : $this->getComments(), 'actor_phid' => $revision->getAuthorPHID());
     id(new PhabricatorTimelineEvent('difx', $event_data))->recordEvent();
     id(new PhabricatorFeedStoryPublisher())->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL)->setStoryData($event_data)->setStoryTime(time())->setStoryAuthorPHID($revision->getAuthorPHID())->setRelatedPHIDs(array($revision->getPHID(), $revision->getAuthorPHID()))->publish();
     // TODO
     //    $revision->saveTransaction();
     //  TODO: Move this into a worker task thing.
     PhabricatorSearchDifferentialIndexer::indexRevision($revision);
     if ($this->silentUpdate) {
         return;
     }
     $revision->loadRelationships();
     if ($add['rev']) {
         $message = id(new DifferentialNewDiffMail($revision, $actor_handle, $changesets))->setIsFirstMailAboutRevision($is_new)->setIsFirstMailToRecipients(true)->setToPHIDs(array_keys($add['rev']));
         if ($is_new) {
             // The first time we send an email about a revision, put the CCs in
             // the "CC:" field of the same "Review Requested" email that reviewers
             // get, so you don't get two initial emails if you're on a list that
             // is CC'd.
             $message->setCCPHIDs(array_keys($add['ccs']));
         }
         $mail[] = $message;
     }
     // If you were added as a reviewer and a CC, just give you the reviewer
     // email. We could go to greater lengths to prevent this, but there's
     // bunch of stuff with list subscriptions anyway. You can still get two
     // emails, but only if a revision is updated and you are added as a reviewer
     // at the same time a list you are on is added as a CC, which is rare and
     // reasonable.
     $add['ccs'] = array_diff_key($add['ccs'], $add['rev']);
     if (!$is_new && $add['ccs']) {
         $mail[] = id(new DifferentialCCWelcomeMail($revision, $actor_handle, $changesets))->setIsFirstMailToRecipients(true)->setToPHIDs(array_keys($add['ccs']));
     }
     foreach ($mail as $message) {
         $message->setHeraldTranscriptURI($xscript_uri);
         $message->setXHeraldRulesHeader($xscript_header);
         $message->send();
     }
 }
 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);
     }
 }
    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();
        }
    }