diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php
index f9524084a..a6ef866c9 100644
--- a/src/applications/audit/editor/PhabricatorAuditEditor.php
+++ b/src/applications/audit/editor/PhabricatorAuditEditor.php
@@ -1,993 +1,992 @@
 <?php
 
 final class PhabricatorAuditEditor
   extends PhabricatorApplicationTransactionEditor {
 
   const MAX_FILES_SHOWN_IN_EMAIL = 1000;
 
   private $auditReasonMap = array();
   private $affectedFiles;
   private $rawPatch;
   private $auditorPHIDs = array();
 
   private $didExpandInlineState;
 
   public function addAuditReason($phid, $reason) {
     if (!isset($this->auditReasonMap[$phid])) {
       $this->auditReasonMap[$phid] = array();
     }
     $this->auditReasonMap[$phid][] = $reason;
     return $this;
   }
 
   private function getAuditReasons($phid) {
     if (isset($this->auditReasonMap[$phid])) {
       return $this->auditReasonMap[$phid];
     }
     if ($this->getIsHeraldEditor()) {
       $name = 'herald';
     } else {
       $name = $this->getActor()->getUsername();
     }
     return array(pht('Added by %s.', $name));
   }
 
   public function setRawPatch($patch) {
     $this->rawPatch = $patch;
     return $this;
   }
 
   public function getRawPatch() {
     return $this->rawPatch;
   }
 
   public function getEditorApplicationClass() {
     return 'PhabricatorAuditApplication';
   }
 
   public function getEditorObjectsDescription() {
     return pht('Audits');
   }
 
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
     $types[] = PhabricatorTransactions::TYPE_COMMENT;
     $types[] = PhabricatorTransactions::TYPE_EDGE;
     $types[] = PhabricatorTransactions::TYPE_INLINESTATE;
 
     $types[] = PhabricatorAuditTransaction::TYPE_COMMIT;
 
     // TODO: These will get modernized eventually, but that can happen one
     // at a time later on.
     $types[] = PhabricatorAuditActionConstants::ACTION;
     $types[] = PhabricatorAuditActionConstants::INLINE;
     $types[] = PhabricatorAuditActionConstants::ADD_AUDITORS;
 
     return $types;
   }
 
   protected function transactionHasEffect(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorAuditActionConstants::INLINE:
         return $xaction->hasComment();
     }
 
     return parent::transactionHasEffect($object, $xaction);
   }
 
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorAuditActionConstants::ACTION:
       case PhabricatorAuditActionConstants::INLINE:
       case PhabricatorAuditTransaction::TYPE_COMMIT:
         return null;
       case PhabricatorAuditActionConstants::ADD_AUDITORS:
         // TODO: For now, just record the added PHIDs. Eventually, turn these
         // into real edge transactions, probably?
         return array();
     }
 
     return parent::getCustomTransactionOldValue($object, $xaction);
   }
 
   protected function getCustomTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorAuditActionConstants::ACTION:
       case PhabricatorAuditActionConstants::INLINE:
       case PhabricatorAuditActionConstants::ADD_AUDITORS:
       case PhabricatorAuditTransaction::TYPE_COMMIT:
         return $xaction->getNewValue();
     }
 
     return parent::getCustomTransactionNewValue($object, $xaction);
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorAuditActionConstants::ACTION:
       case PhabricatorAuditActionConstants::INLINE:
       case PhabricatorAuditActionConstants::ADD_AUDITORS:
       case PhabricatorAuditTransaction::TYPE_COMMIT:
         return;
     }
 
     return parent::applyCustomInternalTransaction($object, $xaction);
   }
 
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorAuditActionConstants::ACTION:
       case PhabricatorAuditTransaction::TYPE_COMMIT:
         return;
       case PhabricatorAuditActionConstants::INLINE:
         $reply = $xaction->getComment()->getReplyToComment();
         if ($reply && !$reply->getHasReplies()) {
           $reply->setHasReplies(1)->save();
         }
         return;
       case PhabricatorAuditActionConstants::ADD_AUDITORS:
         $new = $xaction->getNewValue();
         if (!is_array($new)) {
           $new = array();
         }
 
         $old = $xaction->getOldValue();
         if (!is_array($old)) {
           $old = array();
         }
 
         $add = array_diff_key($new, $old);
 
         $actor = $this->requireActor();
 
         $requests = $object->getAudits();
         $requests = mpull($requests, null, 'getAuditorPHID');
         foreach ($add as $phid) {
           if (isset($requests[$phid])) {
             $request = $requests[$phid];
 
             // Only update an existing request if the current status is not
             // an interesting status.
             if ($request->isInteresting()) {
               continue;
             }
           } else {
             $request = id(new PhabricatorRepositoryAuditRequest())
               ->setCommitPHID($object->getPHID())
               ->setAuditorPHID($phid);
           }
 
           if ($this->getIsHeraldEditor()) {
             $audit_requested = $xaction->getMetadataValue('auditStatus');
             $audit_reason_map = $xaction->getMetadataValue('auditReasonMap');
             $audit_reason = $audit_reason_map[$phid];
           } else {
             $audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED;
             $audit_reason = $this->getAuditReasons($phid);
           }
 
           $request
             ->setAuditStatus($audit_requested)
             ->setAuditReasons($audit_reason)
             ->save();
 
           $requests[$phid] = $request;
         }
 
         $object->attachAudits($requests);
         return;
     }
 
     return parent::applyCustomExternalTransaction($object, $xaction);
   }
 
   protected function applyBuiltinExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_INLINESTATE:
         $table = new PhabricatorAuditTransactionComment();
         $conn_w = $table->establishConnection('w');
         foreach ($xaction->getNewValue() as $phid => $state) {
           queryfx(
             $conn_w,
             'UPDATE %T SET fixedState = %s WHERE phid = %s',
             $table->getTableName(),
             $state,
             $phid);
         }
         break;
     }
 
     return parent::applyBuiltinExternalTransaction($object, $xaction);
   }
 
   protected function applyFinalEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     // Load auditors explicitly; we may not have them if the caller was a
     // generic piece of infrastructure.
 
     $commit = id(new DiffusionCommitQuery())
       ->setViewer($this->requireActor())
       ->withIDs(array($object->getID()))
       ->needAuditRequests(true)
       ->executeOne();
     if (!$commit) {
       throw new Exception(
         pht('Failed to load commit during transaction finalization!'));
     }
     $object->attachAudits($commit->getAudits());
 
     $status_concerned = PhabricatorAuditStatusConstants::CONCERNED;
     $status_closed = PhabricatorAuditStatusConstants::CLOSED;
     $status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
     $status_accepted = PhabricatorAuditStatusConstants::ACCEPTED;
     $status_concerned = PhabricatorAuditStatusConstants::CONCERNED;
 
     $actor_phid = $this->getActingAsPHID();
     $actor_is_author = ($object->getAuthorPHID()) &&
       ($actor_phid == $object->getAuthorPHID());
 
     $import_status_flag = null;
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         case PhabricatorAuditTransaction::TYPE_COMMIT:
           $import_status_flag = PhabricatorRepositoryCommit::IMPORTED_HERALD;
           break;
         case PhabricatorAuditActionConstants::ACTION:
           $new = $xaction->getNewValue();
           switch ($new) {
             case PhabricatorAuditActionConstants::CLOSE:
               // "Close" means wipe out all the concerns.
               $requests = $object->getAudits();
               foreach ($requests as $request) {
                 if ($request->getAuditStatus() == $status_concerned) {
                   $request
                     ->setAuditStatus($status_closed)
                     ->save();
                 }
               }
               break;
             case PhabricatorAuditActionConstants::RESIGN:
               $requests = $object->getAudits();
               $requests = mpull($requests, null, 'getAuditorPHID');
               $actor_request = idx($requests, $actor_phid);
 
               // If the actor doesn't currently have a relationship to the
               // commit, add one explicitly. For example, this allows members
               // of a project to resign from a commit and have it drop out of
               // their queue.
 
               if (!$actor_request) {
                 $actor_request = id(new PhabricatorRepositoryAuditRequest())
                   ->setCommitPHID($object->getPHID())
                   ->setAuditorPHID($actor_phid);
 
                 $requests[] = $actor_request;
                 $object->attachAudits($requests);
               }
 
               $actor_request
                 ->setAuditStatus($status_resigned)
                 ->save();
               break;
             case PhabricatorAuditActionConstants::ACCEPT:
             case PhabricatorAuditActionConstants::CONCERN:
               if ($new == PhabricatorAuditActionConstants::ACCEPT) {
                 $new_status = $status_accepted;
               } else {
                 $new_status = $status_concerned;
               }
 
               $requests = $object->getAudits();
               $requests = mpull($requests, null, 'getAuditorPHID');
 
               // Figure out which requests the actor has authority over: these
               // are user requests where they are the auditor, and packages
               // and projects they are a member of.
 
               if ($actor_is_author) {
                 // When modifying your own commits, you act only on behalf of
                 // yourself, not your packages/projects -- the idea being that
                 // you can't accept your own commits.
                 $authority_phids = array($actor_phid);
               } else {
                 $authority_phids =
                   PhabricatorAuditCommentEditor::loadAuditPHIDsForUser(
                     $this->requireActor());
               }
 
               $authority = array_select_keys(
                 $requests,
                 $authority_phids);
 
               if (!$authority) {
                 // If the actor has no authority over any existing requests,
                 // create a new request for them.
 
                 $actor_request = id(new PhabricatorRepositoryAuditRequest())
                   ->setCommitPHID($object->getPHID())
                   ->setAuditorPHID($actor_phid)
                   ->setAuditStatus($new_status)
                   ->save();
 
                 $requests[$actor_phid] = $actor_request;
                 $object->attachAudits($requests);
               } else {
                 // Otherwise, update the audit status of the existing requests.
                 foreach ($authority as $request) {
                   $request
                     ->setAuditStatus($new_status)
                     ->save();
                 }
               }
               break;
 
           }
           break;
       }
     }
 
     $requests = $object->getAudits();
     $object->updateAuditStatus($requests);
     $object->save();
 
     if ($import_status_flag) {
       $object->writeImportStatusFlag($import_status_flag);
     }
 
     // Collect auditor PHIDs for building mail.
     $this->auditorPHIDs = mpull($object->getAudits(), 'getAuditorPHID');
 
     return $xactions;
   }
 
   protected function expandTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $xactions = parent::expandTransaction($object, $xaction);
     switch ($xaction->getTransactionType()) {
       case PhabricatorAuditTransaction::TYPE_COMMIT:
         $request = $this->createAuditRequestTransactionFromCommitMessage(
           $object);
         if ($request) {
           $xactions[] = $request;
           $this->setUnmentionablePHIDMap($request->getNewValue());
         }
         break;
       default:
         break;
     }
 
     if (!$this->didExpandInlineState) {
       switch ($xaction->getTransactionType()) {
         case PhabricatorTransactions::TYPE_COMMENT:
         case PhabricatorAuditActionConstants::ACTION:
           $this->didExpandInlineState = true;
 
           $actor_phid = $this->getActingAsPHID();
           $actor_is_author = ($object->getAuthorPHID() == $actor_phid);
           if (!$actor_is_author) {
             break;
           }
 
           $state_map = PhabricatorTransactions::getInlineStateMap();
 
           $inlines = id(new DiffusionDiffInlineCommentQuery())
             ->setViewer($this->getActor())
             ->withCommitPHIDs(array($object->getPHID()))
             ->withFixedStates(array_keys($state_map))
             ->execute();
 
           if (!$inlines) {
             break;
           }
 
           $old_value = mpull($inlines, 'getFixedState', 'getPHID');
           $new_value = array();
           foreach ($old_value as $key => $state) {
             $new_value[$key] = $state_map[$state];
           }
 
           $xactions[] = id(new PhabricatorAuditTransaction())
             ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
             ->setIgnoreOnNoEffect(true)
             ->setOldValue($old_value)
             ->setNewValue($new_value);
           break;
       }
     }
 
     return $xactions;
   }
 
   private function createAuditRequestTransactionFromCommitMessage(
     PhabricatorRepositoryCommit $commit) {
 
     $data = $commit->getCommitData();
     $message = $data->getCommitMessage();
 
     $matches = null;
     if (!preg_match('/^Auditors?:\s*(.*)$/im', $message, $matches)) {
       return array();
     }
 
     $phids = id(new PhabricatorObjectListQuery())
       ->setViewer($this->getActor())
       ->setAllowPartialResults(true)
       ->setAllowedTypes(
         array(
           PhabricatorPeopleUserPHIDType::TYPECONST,
           PhabricatorProjectProjectPHIDType::TYPECONST,
         ))
       ->setObjectList($matches[1])
       ->execute();
 
     if (!$phids) {
       return array();
     }
 
     foreach ($phids as $phid) {
       $this->addAuditReason($phid, pht('Requested by Author'));
     }
     return id(new PhabricatorAuditTransaction())
       ->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS)
       ->setNewValue(array_fuse($phids));
   }
 
   protected function sortTransactions(array $xactions) {
     $xactions = parent::sortTransactions($xactions);
 
     $head = array();
     $tail = array();
 
     foreach ($xactions as $xaction) {
       $type = $xaction->getTransactionType();
       if ($type == PhabricatorAuditActionConstants::INLINE) {
         $tail[] = $xaction;
       } else {
         $head[] = $xaction;
       }
     }
 
     return array_values(array_merge($head, $tail));
   }
 
   protected function validateTransaction(
     PhabricatorLiskDAO $object,
     $type,
     array $xactions) {
 
     $errors = parent::validateTransaction($object, $type, $xactions);
 
     foreach ($xactions as $xaction) {
       switch ($type) {
         case PhabricatorAuditActionConstants::ACTION:
           $error = $this->validateAuditAction(
             $object,
             $type,
             $xaction,
             $xaction->getNewValue());
           if ($error) {
             $errors[] = new PhabricatorApplicationTransactionValidationError(
               $type,
               pht('Invalid'),
               $error,
               $xaction);
           }
           break;
       }
     }
 
     return $errors;
   }
 
   private function validateAuditAction(
     PhabricatorLiskDAO $object,
     $type,
     PhabricatorAuditTransaction $xaction,
     $action) {
 
     $can_author_close_key = 'audit.can-author-close-audit';
     $can_author_close = PhabricatorEnv::getEnvConfig($can_author_close_key);
 
     $actor_is_author = ($object->getAuthorPHID()) &&
       ($object->getAuthorPHID() == $this->getActingAsPHID());
 
     switch ($action) {
       case PhabricatorAuditActionConstants::CLOSE:
         if (!$actor_is_author) {
           return pht(
             'You can not close this audit because you are not the author '.
             'of the commit.');
         }
 
         if (!$can_author_close) {
           return pht(
             'You can not close this audit because "%s" is disabled in '.
             'the Phabricator configuration.',
             $can_author_close_key);
         }
 
         break;
     }
 
     return null;
   }
 
 
   protected function supportsSearch() {
     return true;
   }
 
   protected function expandCustomRemarkupBlockTransactions(
     PhabricatorLiskDAO $object,
     array $xactions,
     array $changes,
     PhutilMarkupEngine $engine) {
 
     // we are only really trying to find unmentionable phids here...
     // don't bother with this outside initial commit (i.e. create)
     // transaction
     $is_commit = false;
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         case PhabricatorAuditTransaction::TYPE_COMMIT:
           $is_commit = true;
           break;
       }
     }
 
     // "result" is always an array....
     $result = array();
     if (!$is_commit) {
       return $result;
     }
 
     $flat_blocks = mpull($changes, 'getNewValue');
     $huge_block = implode("\n\n", $flat_blocks);
     $phid_map = array();
     $phid_map[] = $this->getUnmentionablePHIDMap();
     $monograms = array();
 
     $task_refs = id(new ManiphestCustomFieldStatusParser())
       ->parseCorpus($huge_block);
     foreach ($task_refs as $match) {
       foreach ($match['monograms'] as $monogram) {
         $monograms[] = $monogram;
       }
     }
 
     $rev_refs = id(new DifferentialCustomFieldDependsOnParser())
       ->parseCorpus($huge_block);
     foreach ($rev_refs as $match) {
       foreach ($match['monograms'] as $monogram) {
         $monograms[] = $monogram;
       }
     }
 
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($this->getActor())
       ->withNames($monograms)
       ->execute();
     $phid_map[] = mpull($objects, 'getPHID', 'getPHID');
     $phid_map = array_mergev($phid_map);
     $this->setUnmentionablePHIDMap($phid_map);
 
     return $result;
   }
 
   protected function buildReplyHandler(PhabricatorLiskDAO $object) {
     $reply_handler = new PhabricatorAuditReplyHandler();
     $reply_handler->setMailReceiver($object);
     return $reply_handler;
   }
 
   protected function getMailSubjectPrefix() {
     return PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix');
   }
 
   protected function getMailThreadID(PhabricatorLiskDAO $object) {
     // For backward compatibility, use this legacy thread ID.
     return 'diffusion-audit-'.$object->getPHID();
   }
 
   protected function buildMailTemplate(PhabricatorLiskDAO $object) {
     $identifier = $object->getCommitIdentifier();
     $repository = $object->getRepository();
     $monogram = $repository->getMonogram();
 
     $summary = $object->getSummary();
     $name = $repository->formatCommitName($identifier);
 
     $subject = "{$name}: {$summary}";
     $thread_topic = "Commit {$monogram}{$identifier}";
 
     $template = id(new PhabricatorMetaMTAMail())
       ->setSubject($subject)
       ->addHeader('Thread-Topic', $thread_topic);
 
     $this->attachPatch(
       $template,
       $object);
 
     return $template;
   }
 
   protected function getMailTo(PhabricatorLiskDAO $object) {
     $phids = array();
 
     if ($object->getAuthorPHID()) {
       $phids[] = $object->getAuthorPHID();
     }
 
     $status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
     foreach ($object->getAudits() as $audit) {
       if (!$audit->isInteresting()) {
         // Don't send mail to uninteresting auditors, like packages which
         // own this code but which audits have not triggered for.
         continue;
       }
 
       if ($audit->getAuditStatus() != $status_resigned) {
         $phids[] = $audit->getAuditorPHID();
       }
     }
 
     $phids[] = $this->getActingAsPHID();
 
     return $phids;
   }
 
   protected function buildMailBody(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $body = parent::buildMailBody($object, $xactions);
 
     $type_inline = PhabricatorAuditActionConstants::INLINE;
     $type_push = PhabricatorAuditTransaction::TYPE_COMMIT;
 
     $is_commit = false;
     $inlines = array();
     foreach ($xactions as $xaction) {
       if ($xaction->getTransactionType() == $type_inline) {
         $inlines[] = $xaction;
       }
       if ($xaction->getTransactionType() == $type_push) {
         $is_commit = true;
       }
     }
 
     if ($inlines) {
       $body->addTextSection(
         pht('INLINE COMMENTS'),
         $this->renderInlineCommentsForMail($object, $inlines));
     }
 
     if ($is_commit) {
       $data = $object->getCommitData();
       $body->addTextSection(pht('AFFECTED FILES'), $this->affectedFiles);
       $this->inlinePatch(
         $body,
         $object);
     }
 
     $data = $object->getCommitData();
 
     $user_phids = array();
 
     $author_phid = $object->getAuthorPHID();
     if ($author_phid) {
       $user_phids[$author_phid][] = pht('Author');
     }
 
     $committer_phid = $data->getCommitDetail('committerPHID');
     if ($committer_phid && ($committer_phid != $author_phid)) {
       $user_phids[$committer_phid][] = pht('Committer');
     }
 
     foreach ($this->auditorPHIDs as $auditor_phid) {
       $user_phids[$auditor_phid][] = pht('Auditor');
     }
 
     // TODO: It would be nice to show pusher here too, but that information
     // is a little tricky to get at right now.
 
     if ($user_phids) {
       $handle_phids = array_keys($user_phids);
       $handles = id(new PhabricatorHandleQuery())
         ->setViewer($this->requireActor())
         ->withPHIDs($handle_phids)
         ->execute();
 
       $user_info = array();
       foreach ($user_phids as $phid => $roles) {
         $user_info[] = pht(
           '%s (%s)',
           $handles[$phid]->getName(),
           implode(', ', $roles));
       }
 
       $body->addTextSection(
         pht('USERS'),
         implode("\n", $user_info));
     }
 
     $monogram = $object->getRepository()->formatCommitName(
       $object->getCommitIdentifier());
 
     $body->addLinkSection(
       pht('COMMIT'),
       PhabricatorEnv::getProductionURI('/'.$monogram));
 
     return $body;
   }
 
   private function attachPatch(
     PhabricatorMetaMTAMail $template,
     PhabricatorRepositoryCommit $commit) {
 
     if (!$this->getRawPatch()) {
       return;
     }
 
     $attach_key = 'metamta.diffusion.attach-patches';
     $attach_patches = PhabricatorEnv::getEnvConfig($attach_key);
     if (!$attach_patches) {
       return;
     }
 
     $repository = $commit->getRepository();
     $encoding = $repository->getDetail('encoding', 'UTF-8');
 
     $raw_patch = $this->getRawPatch();
     $commit_name = $repository->formatCommitName(
       $commit->getCommitIdentifier());
 
     $template->addAttachment(
       new PhabricatorMetaMTAAttachment(
         $raw_patch,
         $commit_name.'.patch',
         'text/x-patch; charset='.$encoding));
   }
 
   private function inlinePatch(
     PhabricatorMetaMTAMailBody $body,
     PhabricatorRepositoryCommit $commit) {
 
     if (!$this->getRawPatch()) {
         return;
     }
 
     $inline_key = 'metamta.diffusion.inline-patches';
     $inline_patches = PhabricatorEnv::getEnvConfig($inline_key);
     if (!$inline_patches) {
       return;
     }
 
     $repository = $commit->getRepository();
     $raw_patch = $this->getRawPatch();
     $result = null;
     $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.
       $encoding = $repository->getDetail('encoding', 'UTF-8');
       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";
     }
     $body->addRawSection($result);
   }
 
   private function renderInlineCommentsForMail(
     PhabricatorLiskDAO $object,
     array $inline_xactions) {
 
     $inlines = mpull($inline_xactions, 'getComment');
 
     $block = array();
 
     $path_map = id(new DiffusionPathQuery())
       ->withPathIDs(mpull($inlines, 'getPathID'))
       ->execute();
     $path_map = ipull($path_map, 'path', 'id');
 
     foreach ($inlines as $inline) {
       $path = idx($path_map, $inline->getPathID());
       if ($path === null) {
         continue;
       }
 
       $start = $inline->getLineNumber();
       $len   = $inline->getLineLength();
       if ($len) {
         $range = $start.'-'.($start + $len);
       } else {
         $range = $start;
       }
 
       $content = $inline->getContent();
       $block[] = "{$path}:{$range} {$content}";
     }
 
     return implode("\n", $block);
   }
 
   public function getMailTagsMap() {
     return array(
       PhabricatorAuditTransaction::MAILTAG_COMMIT =>
         pht('A commit is created.'),
       PhabricatorAuditTransaction::MAILTAG_ACTION_CONCERN =>
         pht('A commit has a concerned raised against it.'),
       PhabricatorAuditTransaction::MAILTAG_ACTION_ACCEPT =>
         pht('A commit is accepted.'),
       PhabricatorAuditTransaction::MAILTAG_ACTION_RESIGN =>
         pht('A commit has an auditor resign.'),
       PhabricatorAuditTransaction::MAILTAG_ACTION_CLOSE =>
         pht('A commit is closed.'),
       PhabricatorAuditTransaction::MAILTAG_ADD_AUDITORS =>
         pht('A commit has auditors added.'),
       PhabricatorAuditTransaction::MAILTAG_ADD_CCS =>
         pht("A commit's subscribers change."),
       PhabricatorAuditTransaction::MAILTAG_PROJECTS =>
         pht("A commit's projects change."),
       PhabricatorAuditTransaction::MAILTAG_COMMENT =>
         pht('Someone comments on a commit.'),
       PhabricatorAuditTransaction::MAILTAG_OTHER =>
         pht('Other commit activity not listed above occurs.'),
     );
   }
 
   protected function shouldApplyHeraldRules(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         case PhabricatorAuditTransaction::TYPE_COMMIT:
           $repository = $object->getRepository();
           if (!$repository->shouldPublish()) {
             return false;
           }
           return true;
         default:
           break;
       }
     }
     return parent::shouldApplyHeraldRules($object, $xactions);
   }
 
   protected function buildHeraldAdapter(
     PhabricatorLiskDAO $object,
     array $xactions) {
-
     return id(new HeraldCommitAdapter())
-      ->setCommit($object);
+      ->setObject($object);
   }
 
   protected function didApplyHeraldRules(
     PhabricatorLiskDAO $object,
     HeraldAdapter $adapter,
     HeraldTranscript $transcript) {
 
     $limit = self::MAX_FILES_SHOWN_IN_EMAIL;
     $files = $adapter->loadAffectedPaths();
     sort($files);
     if (count($files) > $limit) {
       array_splice($files, $limit);
       $files[] = pht(
         '(This commit affected more than %d files. Only %d are shown here '.
         'and additional ones are truncated.)',
         $limit,
         $limit);
     }
     $this->affectedFiles = implode("\n", $files);
 
     return array();
   }
 
   private function isCommitMostlyImported(PhabricatorLiskDAO $object) {
     $has_message = PhabricatorRepositoryCommit::IMPORTED_MESSAGE;
     $has_changes = PhabricatorRepositoryCommit::IMPORTED_CHANGE;
 
     // Don't publish feed stories or email about events which occur during
     // import. In particular, this affects tasks being attached when they are
     // closed by "Fixes Txxxx" in a commit message. See T5851.
 
     $mask = ($has_message | $has_changes);
 
     return $object->isPartiallyImported($mask);
   }
 
 
   private function shouldPublishRepositoryActivity(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     // not every code path loads the repository so tread carefully
     // TODO: They should, and then we should simplify this.
     $repository = $object->getRepository($assert_attached = false);
     if ($repository != PhabricatorLiskDAO::ATTACHABLE) {
       if (!$repository->shouldPublish()) {
         return false;
       }
     }
 
     return $this->isCommitMostlyImported($object);
   }
 
   protected function shouldSendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $this->shouldPublishRepositoryActivity($object, $xactions);
   }
 
   protected function shouldEnableMentions(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $this->shouldPublishRepositoryActivity($object, $xactions);
   }
 
   protected function shouldPublishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $this->shouldPublishRepositoryActivity($object, $xactions);
   }
 
   protected function getCustomWorkerState() {
     return array(
       'rawPatch' => $this->rawPatch,
       'affectedFiles' => $this->affectedFiles,
       'auditorPHIDs' => $this->auditorPHIDs,
     );
   }
 
   protected function getCustomWorkerStateEncoding() {
     return array(
       'rawPatch' => self::STORAGE_ENCODING_BINARY,
     );
   }
 
   protected function loadCustomWorkerState(array $state) {
     $this->rawPatch = idx($state, 'rawPatch');
     $this->affectedFiles = idx($state, 'affectedFiles');
     $this->auditorPHIDs = idx($state, 'auditorPHIDs');
     return $this;
   }
 
   protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
     return id(new DiffusionCommitQuery())
       ->setViewer($this->requireActor())
       ->withIDs(array($object->getID()))
       ->needAuditRequests(true)
       ->needCommitData(true)
       ->executeOne();
   }
 
 }
diff --git a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php
index 2aed146cb..53fd62fe2 100644
--- a/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php
+++ b/src/applications/differential/herald/HeraldDifferentialRevisionAdapter.php
@@ -1,166 +1,166 @@
 <?php
 
 final class HeraldDifferentialRevisionAdapter
   extends HeraldDifferentialAdapter
   implements HarbormasterBuildableAdapterInterface {
 
   protected $revision;
 
   protected $affectedPackages;
   protected $changesets;
   private $haveHunks;
 
   private $buildRequests = array();
 
   public function getAdapterApplicationClass() {
     return 'PhabricatorDifferentialApplication';
   }
 
   protected function newObject() {
     return new DifferentialRevision();
   }
 
   public function isTestAdapterForObject($object) {
     return ($object instanceof DifferentialRevision);
   }
 
   public function getAdapterTestDescription() {
     return pht(
       'Test rules which run when a revision is created or updated.');
   }
 
-  public function newTestAdapter($object) {
+  public function newTestAdapter(PhabricatorUser $viewer, $object) {
     return self::newLegacyAdapter(
       $object,
       $object->loadActiveDiff());
   }
 
   protected function initializeNewAdapter() {
     $this->revision = $this->newObject();
   }
 
   public function getObject() {
     return $this->revision;
   }
 
   public function getAdapterContentType() {
     return 'differential';
   }
 
   public function getAdapterContentName() {
     return pht('Differential Revisions');
   }
 
   public function getAdapterContentDescription() {
     return pht(
       "React to revisions being created or updated.\n".
       "Revision rules can send email, flag revisions, add reviewers, ".
       "and run build plans.");
   }
 
   public function supportsRuleType($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return true;
       case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
       default:
         return false;
     }
   }
 
   public function getRepetitionOptions() {
     return array(
       HeraldRepetitionPolicyConfig::EVERY,
       HeraldRepetitionPolicyConfig::FIRST,
     );
   }
 
   public static function newLegacyAdapter(
     DifferentialRevision $revision,
     DifferentialDiff $diff) {
     $object = new HeraldDifferentialRevisionAdapter();
 
     // Reload the revision to pick up relationship information.
     $revision = id(new DifferentialRevisionQuery())
       ->withIDs(array($revision->getID()))
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->needRelationships(true)
       ->needReviewerStatus(true)
       ->executeOne();
 
     $object->revision = $revision;
     $object->setDiff($diff);
 
     return $object;
   }
 
   public function getHeraldName() {
     return $this->revision->getTitle();
   }
 
   protected function loadChangesets() {
     if ($this->changesets === null) {
       $this->changesets = $this->getDiff()->loadChangesets();
     }
     return $this->changesets;
   }
 
   protected function loadChangesetsWithHunks() {
     $changesets = $this->loadChangesets();
 
     if ($changesets && !$this->haveHunks) {
       $this->haveHunks = true;
 
       id(new DifferentialHunkQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withChangesets($changesets)
         ->needAttachToChangesets(true)
         ->execute();
     }
 
     return $changesets;
   }
 
   public function loadAffectedPackages() {
     if ($this->affectedPackages === null) {
       $this->affectedPackages = array();
 
       $repository = $this->loadRepository();
       if ($repository) {
         $packages = PhabricatorOwnersPackage::loadAffectedPackages(
           $repository,
           $this->loadAffectedPaths());
         $this->affectedPackages = $packages;
       }
     }
     return $this->affectedPackages;
   }
 
   public function loadReviewers() {
     $reviewers = $this->getObject()->getReviewerStatus();
     return mpull($reviewers, 'getReviewerPHID');
   }
 
 
 /* -(  HarbormasterBuildableAdapterInterface  )------------------------------ */
 
 
   public function getHarbormasterBuildablePHID() {
     return $this->getDiff()->getPHID();
   }
 
   public function getHarbormasterContainerPHID() {
     return $this->getObject()->getPHID();
   }
 
   public function getQueuedHarbormasterBuildRequests() {
     return $this->buildRequests;
   }
 
   public function queueHarbormasterBuildRequest(
     HarbormasterBuildRequest $request) {
     $this->buildRequests[] = $request;
   }
 
 }
diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php
index 759b8afa6..7b4d26b77 100644
--- a/src/applications/diffusion/herald/HeraldCommitAdapter.php
+++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php
@@ -1,361 +1,353 @@
 <?php
 
 final class HeraldCommitAdapter
   extends HeraldAdapter
   implements HarbormasterBuildableAdapterInterface {
 
   protected $diff;
   protected $revision;
 
   protected $commit;
-  protected $commitData;
   private $commitDiff;
 
   protected $affectedPaths;
   protected $affectedRevision;
   protected $affectedPackages;
   protected $auditNeededPackages;
 
   private $buildRequests = array();
 
   public function getAdapterApplicationClass() {
     return 'PhabricatorDiffusionApplication';
   }
 
   protected function newObject() {
     return new PhabricatorRepositoryCommit();
   }
 
   public function isTestAdapterForObject($object) {
     return ($object instanceof PhabricatorRepositoryCommit);
   }
 
   public function getAdapterTestDescription() {
     return pht(
       'Test rules which run after a commit is discovered and imported.');
   }
 
+  public function newTestAdapter(PhabricatorUser $viewer, $object) {
+    $object = id(new DiffusionCommitQuery())
+      ->setViewer($viewer)
+      ->withPHIDs(array($object->getPHID()))
+      ->needCommitData(true)
+      ->executeOne();
+    if (!$object) {
+      throw new Exception(
+        pht(
+          'Failed to reload commit ("%s") to fetch commit data.',
+          $object->getPHID()));
+    }
+
+    return id(clone $this)
+      ->setObject($object);
+  }
+
   protected function initializeNewAdapter() {
     $this->commit = $this->newObject();
   }
 
   public function setObject($object) {
     $this->commit = $object;
 
     return $this;
   }
 
   public function getObject() {
     return $this->commit;
   }
 
   public function getAdapterContentType() {
     return 'commit';
   }
 
   public function getAdapterContentName() {
     return pht('Commits');
   }
 
   public function getAdapterContentDescription() {
     return pht(
       "React to new commits appearing in tracked repositories.\n".
       "Commit rules can send email, flag commits, trigger audits, ".
       "and run build plans.");
   }
 
   public function supportsRuleType($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
       case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
         return true;
       default:
         return false;
     }
   }
 
   public function canTriggerOnObject($object) {
     if ($object instanceof PhabricatorRepository) {
       return true;
     }
     if ($object instanceof PhabricatorProject) {
       return true;
     }
     return false;
   }
 
   public function getTriggerObjectPHIDs() {
     $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
 
     return array_merge(
       array(
         $this->getRepository()->getPHID(),
         $this->getPHID(),
       ),
       $this->loadEdgePHIDs($project_type));
   }
 
   public function explainValidTriggerObjects() {
     return pht('This rule can trigger for **repositories** and **projects**.');
   }
 
-  public function setCommit(PhabricatorRepositoryCommit $commit) {
-    $viewer = PhabricatorUser::getOmnipotentUser();
-
-    $repository = id(new PhabricatorRepositoryQuery())
-      ->setViewer($viewer)
-      ->withIDs(array($commit->getRepositoryID()))
-      ->executeOne();
-    if (!$repository) {
-      throw new Exception(pht('Unable to load repository!'));
-    }
-
-    $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
-      'commitID = %d',
-      $commit->getID());
-    if (!$data) {
-      throw new Exception(pht('Unable to load commit data!'));
-    }
-
-    $this->commit = clone $commit;
-    $this->commit->attachRepository($repository);
-    $this->commit->attachCommitData($data);
-
-    $this->commitData = $data;
-
-    return $this;
-  }
-
   public function getHeraldName() {
     return $this->commit->getMonogram();
   }
 
   public function loadAffectedPaths() {
     if ($this->affectedPaths === null) {
       $result = PhabricatorOwnerPathQuery::loadAffectedPaths(
         $this->getRepository(),
         $this->commit,
         PhabricatorUser::getOmnipotentUser());
       $this->affectedPaths = $result;
     }
     return $this->affectedPaths;
   }
 
   public function loadAffectedPackages() {
     if ($this->affectedPackages === null) {
       $packages = PhabricatorOwnersPackage::loadAffectedPackages(
         $this->getRepository(),
         $this->loadAffectedPaths());
       $this->affectedPackages = $packages;
     }
     return $this->affectedPackages;
   }
 
   public function loadAuditNeededPackages() {
     if ($this->auditNeededPackages === null) {
       $status_arr = array(
         PhabricatorAuditStatusConstants::AUDIT_REQUIRED,
         PhabricatorAuditStatusConstants::CONCERNED,
       );
       $requests = id(new PhabricatorRepositoryAuditRequest())
           ->loadAllWhere(
         'commitPHID = %s AND auditStatus IN (%Ls)',
         $this->commit->getPHID(),
         $status_arr);
       $this->auditNeededPackages = $requests;
     }
     return $this->auditNeededPackages;
   }
 
   public function loadDifferentialRevision() {
     if ($this->affectedRevision === null) {
       $this->affectedRevision = false;
-      $data = $this->commitData;
+
+      $commit = $this->getObject();
+      $data = $commit->getCommitData();
+
       $revision_id = $data->getCommitDetail('differential.revisionID');
       if ($revision_id) {
         // NOTE: The Herald rule owner might not actually have access to
         // the revision, and can control which revision a commit is
         // associated with by putting text in the commit message. However,
         // the rules they can write against revisions don't actually expose
         // anything interesting, so it seems reasonable to load unconditionally
         // here.
 
         $revision = id(new DifferentialRevisionQuery())
           ->withIDs(array($revision_id))
           ->setViewer(PhabricatorUser::getOmnipotentUser())
           ->needRelationships(true)
           ->needReviewerStatus(true)
           ->executeOne();
         if ($revision) {
           $this->affectedRevision = $revision;
         }
       }
     }
     return $this->affectedRevision;
   }
 
   public static function getEnormousByteLimit() {
     return 1024 * 1024 * 1024; // 1GB
   }
 
   public static function getEnormousTimeLimit() {
     return 60 * 15; // 15 Minutes
   }
 
   private function loadCommitDiff() {
     $viewer = PhabricatorUser::getOmnipotentUser();
 
     $byte_limit = self::getEnormousByteLimit();
     $time_limit = self::getEnormousTimeLimit();
 
     $diff_info = $this->callConduit(
       'diffusion.rawdiffquery',
       array(
         'commit' => $this->commit->getCommitIdentifier(),
         'timeout' => $time_limit,
         'byteLimit' => $byte_limit,
         'linesOfContext' => 0,
       ));
 
     if ($diff_info['tooHuge']) {
       throw new Exception(
         pht(
           'The raw text of this change is enormous (larger than %s byte(s)). '.
           'Herald can not process it.',
           new PhutilNumber($byte_limit)));
     }
 
     if ($diff_info['tooSlow']) {
       throw new Exception(
         pht(
           'The raw text of this change took too long to process (longer '.
           'than %s second(s)). Herald can not process it.',
           new PhutilNumber($time_limit)));
     }
 
     $file_phid = $diff_info['filePHID'];
     $diff_file = id(new PhabricatorFileQuery())
       ->setViewer($viewer)
       ->withPHIDs(array($file_phid))
       ->executeOne();
     if (!$diff_file) {
       throw new Exception(
         pht(
           'Failed to load diff ("%s") for this change.',
           $file_phid));
     }
 
     $raw = $diff_file->loadFileData();
 
     $parser = new ArcanistDiffParser();
     $changes = $parser->parseDiff($raw);
 
     $diff = DifferentialDiff::newEphemeralFromRawChanges(
       $changes);
     return $diff;
   }
 
   public function isDiffEnormous() {
     $this->loadDiffContent('*');
     return ($this->commitDiff instanceof Exception);
   }
 
   public function loadDiffContent($type) {
     if ($this->commitDiff === null) {
       try {
         $this->commitDiff = $this->loadCommitDiff();
       } catch (Exception $ex) {
         $this->commitDiff = $ex;
         phlog($ex);
       }
     }
 
     if ($this->commitDiff instanceof Exception) {
       $ex = $this->commitDiff;
       $ex_class = get_class($ex);
       $ex_message = pht('Failed to load changes: %s', $ex->getMessage());
 
       return array(
         '<'.$ex_class.'>' => $ex_message,
       );
     }
 
     $changes = $this->commitDiff->getChangesets();
 
     $result = array();
     foreach ($changes as $change) {
       $lines = array();
       foreach ($change->getHunks() as $hunk) {
         switch ($type) {
           case '-':
             $lines[] = $hunk->makeOldFile();
             break;
           case '+':
             $lines[] = $hunk->makeNewFile();
             break;
           case '*':
             $lines[] = $hunk->makeChanges();
             break;
           default:
             throw new Exception(pht("Unknown content selection '%s'!", $type));
         }
       }
       $result[$change->getFilename()] = implode("\n", $lines);
     }
 
     return $result;
   }
 
   public function loadIsMergeCommit() {
     $parents = $this->callConduit(
       'diffusion.commitparentsquery',
       array(
         'commit' => $this->getObject()->getCommitIdentifier(),
       ));
 
     return (count($parents) > 1);
   }
 
   private function callConduit($method, array $params) {
     $viewer = PhabricatorUser::getOmnipotentUser();
 
     $drequest = DiffusionRequest::newFromDictionary(
       array(
         'user' => $viewer,
         'repository' => $this->getRepository(),
         'commit' => $this->commit->getCommitIdentifier(),
       ));
 
     return DiffusionQuery::callConduitWithDiffusionRequest(
       $viewer,
       $drequest,
       $method,
       $params);
   }
 
   private function getRepository() {
     return $this->getObject()->getRepository();
   }
 
 /* -(  HarbormasterBuildableAdapterInterface  )------------------------------ */
 
 
   public function getHarbormasterBuildablePHID() {
     return $this->getObject()->getPHID();
   }
 
   public function getHarbormasterContainerPHID() {
     return $this->getObject()->getRepository()->getPHID();
   }
 
   public function getQueuedHarbormasterBuildRequests() {
     return $this->buildRequests;
   }
 
   public function queueHarbormasterBuildRequest(
     HarbormasterBuildRequest $request) {
     $this->buildRequests[] = $request;
   }
 
 }
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index ac227819f..78ce86294 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,1119 +1,1119 @@
 <?php
 
 abstract class HeraldAdapter extends Phobject {
 
   const CONDITION_CONTAINS        = 'contains';
   const CONDITION_NOT_CONTAINS    = '!contains';
   const CONDITION_IS              = 'is';
   const CONDITION_IS_NOT          = '!is';
   const CONDITION_IS_ANY          = 'isany';
   const CONDITION_IS_NOT_ANY      = '!isany';
   const CONDITION_INCLUDE_ALL     = 'all';
   const CONDITION_INCLUDE_ANY     = 'any';
   const CONDITION_INCLUDE_NONE    = 'none';
   const CONDITION_IS_ME           = 'me';
   const CONDITION_IS_NOT_ME       = '!me';
   const CONDITION_REGEXP          = 'regexp';
   const CONDITION_NOT_REGEXP      = '!regexp';
   const CONDITION_RULE            = 'conditions';
   const CONDITION_NOT_RULE        = '!conditions';
   const CONDITION_EXISTS          = 'exists';
   const CONDITION_NOT_EXISTS      = '!exists';
   const CONDITION_UNCONDITIONALLY = 'unconditionally';
   const CONDITION_NEVER           = 'never';
   const CONDITION_REGEXP_PAIR     = 'regexp-pair';
   const CONDITION_HAS_BIT         = 'bit';
   const CONDITION_NOT_BIT         = '!bit';
   const CONDITION_IS_TRUE         = 'true';
   const CONDITION_IS_FALSE        = 'false';
 
   private $contentSource;
   private $isNewObject;
   private $applicationEmail;
   private $appliedTransactions = array();
   private $queuedTransactions = array();
   private $emailPHIDs = array();
   private $forcedEmailPHIDs = array();
   private $fieldMap;
   private $actionMap;
   private $edgeCache = array();
 
   public function getEmailPHIDs() {
     return array_values($this->emailPHIDs);
   }
 
   public function getForcedEmailPHIDs() {
     return array_values($this->forcedEmailPHIDs);
   }
 
   public function addEmailPHID($phid, $force) {
     $this->emailPHIDs[$phid] = $phid;
     if ($force) {
       $this->forcedEmailPHIDs[$phid] = $phid;
     }
     return $this;
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
   public function getContentSource() {
     return $this->contentSource;
   }
 
   public function getIsNewObject() {
     if (is_bool($this->isNewObject)) {
       return $this->isNewObject;
     }
 
     throw new Exception(
       pht(
         'You must %s to a boolean first!',
         'setIsNewObject()'));
   }
   public function setIsNewObject($new) {
     $this->isNewObject = (bool)$new;
     return $this;
   }
 
   public function supportsApplicationEmail() {
     return false;
   }
 
   public function setApplicationEmail(
     PhabricatorMetaMTAApplicationEmail $email) {
     $this->applicationEmail = $email;
     return $this;
   }
 
   public function getApplicationEmail() {
     return $this->applicationEmail;
   }
 
   public function getPHID() {
     return $this->getObject()->getPHID();
   }
 
   abstract public function getHeraldName();
 
   public function getHeraldField($field_key) {
     return $this->requireFieldImplementation($field_key)
       ->getHeraldFieldValue($this->getObject());
   }
 
   public function applyHeraldEffects(array $effects) {
     assert_instances_of($effects, 'HeraldEffect');
 
     $result = array();
     foreach ($effects as $effect) {
       $result[] = $this->applyStandardEffect($effect);
     }
 
     return $result;
   }
 
   public function isAvailableToUser(PhabricatorUser $viewer) {
     $applications = id(new PhabricatorApplicationQuery())
       ->setViewer($viewer)
       ->withInstalled(true)
       ->withClasses(array($this->getAdapterApplicationClass()))
       ->execute();
 
     return !empty($applications);
   }
 
 
   /**
    * Set the list of transactions which just took effect.
    *
    * These transactions are set by @{class:PhabricatorApplicationEditor}
    * automatically, before it invokes Herald.
    *
    * @param list<PhabricatorApplicationTransaction> List of transactions.
    * @return this
    */
   final public function setAppliedTransactions(array $xactions) {
     assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
     $this->appliedTransactions = $xactions;
     return $this;
   }
 
 
   /**
    * Get a list of transactions which just took effect.
    *
    * When an object is edited normally, transactions are applied and then
    * Herald executes. You can call this method to examine the transactions
    * if you want to react to them.
    *
    * @return list<PhabricatorApplicationTransaction> List of transactions.
    */
   final public function getAppliedTransactions() {
     return $this->appliedTransactions;
   }
 
   public function queueTransaction($transaction) {
     $this->queuedTransactions[] = $transaction;
   }
 
   public function getQueuedTransactions() {
     return $this->queuedTransactions;
   }
 
   public function newTransaction() {
     $object = $this->newObject();
 
     if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
       throw new Exception(
         pht(
           'Unable to build a new transaction for adapter object; it does '.
           'not implement "%s".',
           'PhabricatorApplicationTransactionInterface'));
     }
 
     return $object->getApplicationTransactionTemplate();
   }
 
 
   /**
    * NOTE: You generally should not override this; it exists to support legacy
    * adapters which had hard-coded content types.
    */
   public function getAdapterContentType() {
     return get_class($this);
   }
 
   abstract public function getAdapterContentName();
   abstract public function getAdapterContentDescription();
   abstract public function getAdapterApplicationClass();
   abstract public function getObject();
 
   /**
    * Return a new characteristic object for this adapter.
    *
    * The adapter will use this object to test for interfaces, generate
    * transactions, and interact with custom fields.
    *
    * Adapters must return an object from this method to enable custom
    * field rules and various implicit actions.
    *
    * Normally, you'll return an empty version of the adapted object:
    *
    *   return new ApplicationObject();
    *
    * @return null|object Template object.
    */
   protected function newObject() {
     return null;
   }
 
   public function supportsRuleType($rule_type) {
     return false;
   }
 
   public function canTriggerOnObject($object) {
     return false;
   }
 
   public function isTestAdapterForObject($object) {
     return false;
   }
 
   public function canCreateTestAdapterForObject($object) {
     return $this->isTestAdapterForObject($object);
   }
 
-  public function newTestAdapter($object) {
+  public function newTestAdapter(PhabricatorUser $viewer, $object) {
     return id(clone $this)
       ->setObject($object);
   }
 
   public function getAdapterTestDescription() {
     return null;
   }
 
   public function explainValidTriggerObjects() {
     return pht('This adapter can not trigger on objects.');
   }
 
   public function getTriggerObjectPHIDs() {
     return array($this->getPHID());
   }
 
   public function getAdapterSortKey() {
     return sprintf(
       '%08d%s',
       $this->getAdapterSortOrder(),
       $this->getAdapterContentName());
   }
 
   public function getAdapterSortOrder() {
     return 1000;
   }
 
 
 /* -(  Fields  )------------------------------------------------------------- */
 
   private function getFieldImplementationMap() {
     if ($this->fieldMap === null) {
       // We can't use PhutilClassMapQuery here because field expansion
       // depends on the adapter and object.
 
       $object = $this->getObject();
 
       $map = array();
       $all = HeraldField::getAllFields();
       foreach ($all as $key => $field) {
         $field = id(clone $field)->setAdapter($this);
 
         if (!$field->supportsObject($object)) {
           continue;
         }
         $subfields = $field->getFieldsForObject($object);
         foreach ($subfields as $subkey => $subfield) {
           if (isset($map[$subkey])) {
             throw new Exception(
               pht(
                 'Two HeraldFields (of classes "%s" and "%s") have the same '.
                 'field key ("%s") after expansion for an object of class '.
                 '"%s" inside adapter "%s". Each field must have a unique '.
                 'field key.',
                 get_class($subfield),
                 get_class($map[$subkey]),
                 $subkey,
                 get_class($object),
                 get_class($this)));
           }
 
           $subfield = id(clone $subfield)->setAdapter($this);
 
           $map[$subkey] = $subfield;
         }
       }
       $this->fieldMap = $map;
     }
 
     return $this->fieldMap;
   }
 
   private function getFieldImplementation($key) {
     return idx($this->getFieldImplementationMap(), $key);
   }
 
   public function getFields() {
     return array_keys($this->getFieldImplementationMap());
   }
 
   public function getFieldNameMap() {
     return mpull($this->getFieldImplementationMap(), 'getHeraldFieldName');
   }
 
   public function getFieldGroupKey($field_key) {
     $field = $this->getFieldImplementation($field_key);
 
     if (!$field) {
       return null;
     }
 
     return $field->getFieldGroupKey();
   }
 
 
 /* -(  Conditions  )--------------------------------------------------------- */
 
 
   public function getConditionNameMap() {
     return array(
       self::CONDITION_CONTAINS        => pht('contains'),
       self::CONDITION_NOT_CONTAINS    => pht('does not contain'),
       self::CONDITION_IS              => pht('is'),
       self::CONDITION_IS_NOT          => pht('is not'),
       self::CONDITION_IS_ANY          => pht('is any of'),
       self::CONDITION_IS_TRUE         => pht('is true'),
       self::CONDITION_IS_FALSE        => pht('is false'),
       self::CONDITION_IS_NOT_ANY      => pht('is not any of'),
       self::CONDITION_INCLUDE_ALL     => pht('include all of'),
       self::CONDITION_INCLUDE_ANY     => pht('include any of'),
       self::CONDITION_INCLUDE_NONE    => pht('do not include'),
       self::CONDITION_IS_ME           => pht('is myself'),
       self::CONDITION_IS_NOT_ME       => pht('is not myself'),
       self::CONDITION_REGEXP          => pht('matches regexp'),
       self::CONDITION_NOT_REGEXP      => pht('does not match regexp'),
       self::CONDITION_RULE            => pht('matches:'),
       self::CONDITION_NOT_RULE        => pht('does not match:'),
       self::CONDITION_EXISTS          => pht('exists'),
       self::CONDITION_NOT_EXISTS      => pht('does not exist'),
       self::CONDITION_UNCONDITIONALLY => '',  // don't show anything!
       self::CONDITION_NEVER           => '',  // don't show anything!
       self::CONDITION_REGEXP_PAIR     => pht('matches regexp pair'),
       self::CONDITION_HAS_BIT         => pht('has bit'),
       self::CONDITION_NOT_BIT         => pht('lacks bit'),
     );
   }
 
   public function getConditionsForField($field) {
     return $this->requireFieldImplementation($field)
       ->getHeraldFieldConditions();
   }
 
   private function requireFieldImplementation($field_key) {
     $field = $this->getFieldImplementation($field_key);
 
     if (!$field) {
       throw new Exception(
         pht(
           'No field with key "%s" is available to Herald adapter "%s".',
           $field_key,
           get_class($this)));
     }
 
     return $field;
   }
 
   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:
       case self::CONDITION_NOT_CONTAINS:
         // "Contains and "does not contain" can take an array of strings, as in
         // "Any changed filename" for diffs.
 
         $result_if_match = ($condition_type == self::CONDITION_CONTAINS);
 
         foreach ((array)$field_value as $value) {
           if (stripos($value, $condition_value) !== false) {
             return $result_if_match;
           }
         }
         return !$result_if_match;
       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:
       case self::CONDITION_NOT_REGEXP:
         $result_if_match = ($condition_type == 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 $result_if_match;
           }
         }
         return !$result_if_match;
       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));
     }
   }
 
   public function willSaveCondition(HeraldCondition $condition) {
     $condition_type = $condition->getFieldCondition();
     $condition_value = $condition->getValue();
 
     switch ($condition_type) {
       case self::CONDITION_REGEXP:
       case self::CONDITION_NOT_REGEXP:
         $ok = @preg_match($condition_value, '');
         if ($ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression "%s" is not valid. Regular expressions '.
               'must have enclosing characters (e.g. "@/path/to/file@", not '.
               '"/path/to/file") and be syntactically correct.',
               $condition_value));
         }
         break;
       case self::CONDITION_REGEXP_PAIR:
         $json = null;
         try {
           $json = phutil_json_decode($condition_value);
         } catch (PhutilJSONParserException $ex) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression pair "%s" is not valid JSON. Enter a '.
               'valid JSON array with two elements.',
               $condition_value));
         }
 
         if (count($json) != 2) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression pair "%s" must have exactly two '.
               'elements.',
               $condition_value));
         }
 
         $key_regexp = array_shift($json);
         $val_regexp = array_shift($json);
 
         $key_ok = @preg_match($key_regexp, '');
         if ($key_ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The first regexp in the regexp pair, "%s", is not a valid '.
               'regexp.',
               $key_regexp));
         }
 
         $val_ok = @preg_match($val_regexp, '');
         if ($val_ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The second regexp in the regexp pair, "%s", is not a valid '.
               'regexp.',
               $val_regexp));
         }
         break;
       case self::CONDITION_CONTAINS:
       case self::CONDITION_NOT_CONTAINS:
       case self::CONDITION_IS:
       case self::CONDITION_IS_NOT:
       case self::CONDITION_IS_ANY:
       case self::CONDITION_IS_NOT_ANY:
       case self::CONDITION_INCLUDE_ALL:
       case self::CONDITION_INCLUDE_ANY:
       case self::CONDITION_INCLUDE_NONE:
       case self::CONDITION_IS_ME:
       case self::CONDITION_IS_NOT_ME:
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
       case self::CONDITION_EXISTS:
       case self::CONDITION_NOT_EXISTS:
       case self::CONDITION_UNCONDITIONALLY:
       case self::CONDITION_NEVER:
       case self::CONDITION_HAS_BIT:
       case self::CONDITION_NOT_BIT:
       case self::CONDITION_IS_TRUE:
       case self::CONDITION_IS_FALSE:
         // No explicit validation for these types, although there probably
         // should be in some cases.
         break;
       default:
         throw new HeraldInvalidConditionException(
           pht(
             'Unknown condition "%s"!',
             $condition_type));
     }
   }
 
 
 /* -(  Actions  )------------------------------------------------------------ */
 
   private function getActionImplementationMap() {
     if ($this->actionMap === null) {
       // We can't use PhutilClassMapQuery here because action expansion
       // depends on the adapter and object.
 
       $object = $this->getObject();
 
       $map = array();
       $all = HeraldAction::getAllActions();
       foreach ($all as $key => $action) {
         $action = id(clone $action)->setAdapter($this);
 
         if (!$action->supportsObject($object)) {
           continue;
         }
 
         $subactions = $action->getActionsForObject($object);
         foreach ($subactions as $subkey => $subaction) {
           if (isset($map[$subkey])) {
             throw new Exception(
               pht(
                 'Two HeraldActions (of classes "%s" and "%s") have the same '.
                 'action key ("%s") after expansion for an object of class '.
                 '"%s" inside adapter "%s". Each action must have a unique '.
                 'action key.',
                 get_class($subaction),
                 get_class($map[$subkey]),
                 $subkey,
                 get_class($object),
                 get_class($this)));
           }
 
           $subaction = id(clone $subaction)->setAdapter($this);
 
           $map[$subkey] = $subaction;
         }
       }
       $this->actionMap = $map;
     }
 
     return $this->actionMap;
   }
 
   private function requireActionImplementation($action_key) {
     $action = $this->getActionImplementation($action_key);
 
     if (!$action) {
       throw new Exception(
         pht(
           'No action with key "%s" is available to Herald adapter "%s".',
           $action_key,
           get_class($this)));
     }
 
     return $action;
   }
 
   private function getActionsForRuleType($rule_type) {
     $actions = $this->getActionImplementationMap();
 
     foreach ($actions as $key => $action) {
       if (!$action->supportsRuleType($rule_type)) {
         unset($actions[$key]);
       }
     }
 
     return $actions;
   }
 
   public function getActionImplementation($key) {
     return idx($this->getActionImplementationMap(), $key);
   }
 
   public function getActionKeys() {
     return array_keys($this->getActionImplementationMap());
   }
 
   public function getActionGroupKey($action_key) {
     $action = $this->getActionImplementation($action_key);
     if (!$action) {
       return null;
     }
 
     return $action->getActionGroupKey();
   }
 
   public function getActions($rule_type) {
     $actions = array();
     foreach ($this->getActionsForRuleType($rule_type) as $key => $action) {
       $actions[] = $key;
     }
 
     return $actions;
   }
 
   public function getActionNameMap($rule_type) {
     $map = array();
     foreach ($this->getActionsForRuleType($rule_type) as $key => $action) {
       $map[$key] = $action->getHeraldActionName();
     }
 
     return $map;
   }
 
   public function willSaveAction(
     HeraldRule $rule,
     HeraldActionRecord $action) {
 
     $impl = $this->requireActionImplementation($action->getAction());
     $target = $action->getTarget();
     $target = $impl->willSaveActionValue($target);
 
     $action->setTarget($target);
   }
 
 
 
 /* -(  Values  )------------------------------------------------------------- */
 
 
   public function getValueTypeForFieldAndCondition($field, $condition) {
     return $this->requireFieldImplementation($field)
       ->getHeraldFieldValueType($condition);
   }
 
   public function getValueTypeForAction($action, $rule_type) {
     $impl = $this->requireActionImplementation($action);
     return $impl->getHeraldActionValueType();
   }
 
   private function buildTokenizerFieldValue(
     PhabricatorTypeaheadDatasource $datasource) {
 
     $key = 'action.'.get_class($datasource);
 
     return id(new HeraldTokenizerFieldValue())
       ->setKey($key)
       ->setDatasource($datasource);
   }
 
 /* -(  Repetition  )--------------------------------------------------------- */
 
 
   public function getRepetitionOptions() {
     return array(
       HeraldRepetitionPolicyConfig::EVERY,
     );
   }
 
   protected function initializeNewAdapter() {
     $this->setObject($this->newObject());
     return $this;
   }
 
   /**
    * Does this adapter's event fire only once?
    *
    * Single use adapters (like pre-commit and diff adapters) only fire once,
    * so fields like "Is new object" don't make sense to apply to their content.
    *
    * @return bool
    */
   public function isSingleEventAdapter() {
     return false;
   }
 
   public static function getAllAdapters() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getAdapterContentType')
       ->setSortMethod('getAdapterSortKey')
       ->execute();
   }
 
   public static function getAdapterForContentType($content_type) {
     $adapters = self::getAllAdapters();
 
     foreach ($adapters as $adapter) {
       if ($adapter->getAdapterContentType() == $content_type) {
         $adapter = id(clone $adapter);
         $adapter->initializeNewAdapter();
         return $adapter;
       }
     }
 
     throw new Exception(
       pht(
         'No adapter exists for Herald content type "%s".',
         $content_type));
   }
 
   public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
     $map = array();
 
     $adapters = self::getAllAdapters();
     foreach ($adapters as $adapter) {
       if (!$adapter->isAvailableToUser($viewer)) {
         continue;
       }
       $type = $adapter->getAdapterContentType();
       $name = $adapter->getAdapterContentName();
       $map[$type] = $name;
     }
 
     return $map;
   }
 
   public function getEditorValueForCondition(
     PhabricatorUser $viewer,
     HeraldCondition $condition) {
 
     $field = $this->requireFieldImplementation($condition->getFieldName());
 
     return $field->getEditorValue(
       $viewer,
       $condition->getFieldCondition(),
       $condition->getValue());
   }
 
   public function getEditorValueForAction(
     PhabricatorUser $viewer,
     HeraldActionRecord $action_record) {
 
     $action = $this->requireActionImplementation($action_record->getAction());
 
     return $action->getEditorValue(
       $viewer,
       $action_record->getTarget());
   }
 
   public function renderRuleAsText(
     HeraldRule $rule,
     PhabricatorHandleList $handles,
     PhabricatorUser $viewer) {
 
     require_celerity_resource('herald-css');
 
     $icon = id(new PHUIIconView())
       ->setIcon('fa-chevron-circle-right lightgreytext')
       ->addClass('herald-list-icon');
 
     if ($rule->getMustMatchAll()) {
       $match_text = pht('When all of these conditions are met:');
     } else {
       $match_text = pht('When any of these conditions are met:');
     }
 
     $match_title = phutil_tag(
       'p',
       array(
         'class' => 'herald-list-description',
       ),
       $match_text);
 
     $match_list = array();
     foreach ($rule->getConditions() as $condition) {
       $match_list[] = phutil_tag(
         'div',
         array(
           'class' => 'herald-list-item',
         ),
         array(
           $icon,
           $this->renderConditionAsText($condition, $handles, $viewer),
         ));
     }
 
     $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
       HeraldRepetitionPolicyConfig::EVERY);
 
     if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
       $action_text =
         pht('Take these actions every time this rule matches:');
     } else {
       $action_text =
         pht('Take these actions the first time this rule matches:');
     }
 
     $action_title = phutil_tag(
       'p',
       array(
         'class' => 'herald-list-description',
       ),
       $action_text);
 
     $action_list = array();
     foreach ($rule->getActions() as $action) {
       $action_list[] = phutil_tag(
         'div',
         array(
           'class' => 'herald-list-item',
         ),
         array(
           $icon,
           $this->renderActionAsText($viewer, $action, $handles),
         ));
     }
 
     return array(
       $match_title,
       $match_list,
       $action_title,
       $action_list,
     );
   }
 
   private function renderConditionAsText(
     HeraldCondition $condition,
     PhabricatorHandleList $handles,
     PhabricatorUser $viewer) {
 
     $field_type = $condition->getFieldName();
     $field = $this->getFieldImplementation($field_type);
 
     if (!$field) {
       return pht('Unknown Field: "%s"', $field_type);
     }
 
     $field_name = $field->getHeraldFieldName();
 
     $condition_type = $condition->getFieldCondition();
     $condition_name = idx($this->getConditionNameMap(), $condition_type);
 
     $value = $this->renderConditionValueAsText($condition, $handles, $viewer);
 
     return array(
       $field_name,
       ' ',
       $condition_name,
       ' ',
       $value,
     );
   }
 
   private function renderActionAsText(
     PhabricatorUser $viewer,
     HeraldActionRecord $action,
     PhabricatorHandleList $handles) {
 
     $impl = $this->getActionImplementation($action->getAction());
     if ($impl) {
       $impl->setViewer($viewer);
 
       $value = $action->getTarget();
       return $impl->renderActionDescription($value);
     }
 
     $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
 
     $action_type = $action->getAction();
 
     $default = pht('(Unknown Action "%s") equals', $action_type);
 
     $action_name = idx(
       $this->getActionNameMap($rule_global),
       $action_type,
       $default);
 
     $target = $this->renderActionTargetAsText($action, $handles);
 
     return hsprintf('    %s %s', $action_name, $target);
   }
 
   private function renderConditionValueAsText(
     HeraldCondition $condition,
     PhabricatorHandleList $handles,
     PhabricatorUser $viewer) {
 
     $field = $this->requireFieldImplementation($condition->getFieldName());
 
     return $field->renderConditionValue(
       $viewer,
       $condition->getFieldCondition(),
       $condition->getValue());
   }
 
   private function renderActionTargetAsText(
     HeraldActionRecord $action,
     PhabricatorHandleList $handles) {
 
     // TODO: This should be driven through HeraldAction.
 
     $target = $action->getTarget();
     if (!is_array($target)) {
       $target = array($target);
     }
     foreach ($target as $index => $val) {
       switch ($action->getAction()) {
         default:
           $handle = $handles->getHandleIfExists($val);
           if ($handle) {
             $target[$index] = $handle->renderLink();
           }
           break;
       }
     }
     $target = phutil_implode_html(', ', $target);
     return $target;
   }
 
   /**
    * Given a @{class:HeraldRule}, this function extracts all the phids that
    * we'll want to load as handles later.
    *
    * This function performs a somewhat hacky approach to figuring out what
    * is and is not a phid - try to get the phid type and if the type is
    * *not* unknown assume its a valid phid.
    *
    * Don't try this at home. Use more strongly typed data at home.
    *
    * Think of the children.
    */
   public static function getHandlePHIDs(HeraldRule $rule) {
     $phids = array($rule->getAuthorPHID());
     foreach ($rule->getConditions() as $condition) {
       $value = $condition->getValue();
       if (!is_array($value)) {
         $value = array($value);
       }
       foreach ($value as $val) {
         if (phid_get_type($val) !=
             PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
           $phids[] = $val;
         }
       }
     }
 
     foreach ($rule->getActions() as $action) {
       $target = $action->getTarget();
       if (!is_array($target)) {
         $target = array($target);
       }
       foreach ($target as $val) {
         if (phid_get_type($val) !=
             PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
           $phids[] = $val;
         }
       }
     }
 
     if ($rule->isObjectRule()) {
       $phids[] = $rule->getTriggerObjectPHID();
     }
 
     return $phids;
   }
 
 /* -(  Applying Effects  )--------------------------------------------------- */
 
 
   /**
    * @task apply
    */
   protected function applyStandardEffect(HeraldEffect $effect) {
     $action = $effect->getAction();
     $rule_type = $effect->getRule()->getRuleType();
 
     $impl = $this->getActionImplementation($action);
     if (!$impl) {
       return new HeraldApplyTranscript(
         $effect,
         false,
         array(
           array(
             HeraldAction::DO_STANDARD_INVALID_ACTION,
             $action,
           ),
         ));
     }
 
     if (!$impl->supportsRuleType($rule_type)) {
       return new HeraldApplyTranscript(
         $effect,
         false,
         array(
           array(
             HeraldAction::DO_STANDARD_WRONG_RULE_TYPE,
             $rule_type,
           ),
         ));
     }
 
     $impl->applyEffect($this->getObject(), $effect);
     return $impl->getApplyTranscript($effect);
   }
 
   public function loadEdgePHIDs($type) {
     if (!isset($this->edgeCache[$type])) {
       $phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $this->getObject()->getPHID(),
         $type);
 
       $this->edgeCache[$type] = array_fuse($phids);
     }
     return $this->edgeCache[$type];
   }
 
 }
diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php
index fae92864f..21bedcd84 100644
--- a/src/applications/herald/controller/HeraldTestConsoleController.php
+++ b/src/applications/herald/controller/HeraldTestConsoleController.php
@@ -1,216 +1,218 @@
 <?php
 
 final class HeraldTestConsoleController extends HeraldController {
 
   private $testObject;
   private $testAdapter;
 
   public function setTestObject($test_object) {
     $this->testObject = $test_object;
     return $this;
   }
 
   public function getTestObject() {
     return $this->testObject;
   }
 
   public function setTestAdapter(HeraldAdapter $test_adapter) {
     $this->testAdapter = $test_adapter;
     return $this;
   }
 
   public function getTestAdapter() {
     return $this->testAdapter;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getViewer();
 
     $response = $this->loadTestObject($request);
     if ($response) {
       return $response;
     }
 
     $response = $this->loadAdapter($request);
     if ($response) {
       return $response;
     }
 
     $object = $this->getTestObject();
     $adapter = $this->getTestAdapter();
 
     $adapter->setIsNewObject(false);
 
     $rules = id(new HeraldRuleQuery())
       ->setViewer($viewer)
       ->withContentTypes(array($adapter->getAdapterContentType()))
       ->withDisabled(false)
       ->needConditionsAndActions(true)
       ->needAppliedToPHIDs(array($object->getPHID()))
       ->needValidateAuthors(true)
       ->execute();
 
     $engine = id(new HeraldEngine())
       ->setDryRun(true);
 
     $effects = $engine->applyRules($rules, $adapter);
     $engine->applyEffects($effects, $adapter, $rules);
 
     $xscript = $engine->getTranscript();
 
     return id(new AphrontRedirectResponse())
       ->setURI('/herald/transcript/'.$xscript->getID().'/');
   }
 
   private function loadTestObject(AphrontRequest $request) {
     $viewer = $this->getViewer();
 
     $e_name = true;
     $v_name = null;
     $errors = array();
 
     if ($request->isFormPost()) {
       $v_name = trim($request->getStr('object_name'));
       if (!$v_name) {
         $e_name = pht('Required');
         $errors[] = pht('An object name is required.');
       }
 
       if (!$errors) {
         $object = id(new PhabricatorObjectQuery())
           ->setViewer($viewer)
           ->withNames(array($v_name))
           ->executeOne();
 
         if (!$object) {
           $e_name = pht('Invalid');
           $errors[] = pht('No object exists with that name.');
         }
       }
 
       if (!$errors) {
         $this->setTestObject($object);
         return null;
       }
     }
 
     $form = id(new AphrontFormView())
       ->setUser($viewer)
       ->appendRemarkupInstructions(
         pht(
         'Enter an object to test rules for, like a Diffusion commit (e.g., '.
         '`rX123`) or a Differential revision (e.g., `D123`). You will be '.
         'shown the results of a dry run on the object.'))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel(pht('Object Name'))
           ->setName('object_name')
           ->setError($e_name)
           ->setValue($v_name))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue(pht('Continue')));
 
     return $this->buildTestConsoleResponse($form, $errors);
   }
 
   private function loadAdapter(AphrontRequest $request) {
     $viewer = $this->getViewer();
     $object = $this->getTestObject();
 
     $adapter_key = $request->getStr('adapter');
 
     $adapters = HeraldAdapter::getAllAdapters();
 
     $can_select = array();
     $display_adapters = array();
     foreach ($adapters as $key => $adapter) {
       if (!$adapter->isTestAdapterForObject($object)) {
         continue;
       }
 
       if (!$adapter->isAvailableToUser($viewer)) {
         continue;
       }
 
       $display_adapters[$key] = $adapter;
 
       if ($adapter->canCreateTestAdapterForObject($object)) {
         $can_select[$key] = $adapter;
       }
     }
 
     if ($request->isFormPost() && $adapter_key) {
       if (isset($can_select[$adapter_key])) {
-        $adapter = $can_select[$adapter_key]->newTestAdapter($object);
+        $adapter = $can_select[$adapter_key]->newTestAdapter(
+          $viewer,
+          $object);
         $this->setTestAdapter($adapter);
         return null;
       }
     }
 
     $form = id(new AphrontFormView())
       ->addHiddenInput('object_name', $request->getStr('object_name'))
       ->setViewer($viewer);
 
     $cancel_uri = $this->getApplicationURI();
 
     if (!$display_adapters) {
       $form
         ->appendRemarkupInstructions(
           pht('//There are no available Herald events for this object.//'))
         ->appendControl(
           id(new AphrontFormSubmitControl())
             ->addCancelButton($cancel_uri));
     } else {
       $adapter_control = id(new AphrontFormRadioButtonControl())
         ->setLabel(pht('Event'))
         ->setName('adapter')
         ->setValue(head_key($can_select));
 
       foreach ($display_adapters as $adapter_key => $adapter) {
         $is_disabled = empty($can_select[$adapter_key]);
 
         $adapter_control->addButton(
           $adapter_key,
           $adapter->getAdapterContentName(),
           $adapter->getAdapterTestDescription(),
           null,
           $is_disabled);
       }
 
       $form
         ->appendControl($adapter_control)
         ->appendControl(
           id(new AphrontFormSubmitControl())
             ->setValue(pht('Run Test')));
     }
 
     return $this->buildTestConsoleResponse($form, array());
   }
 
   private function buildTestConsoleResponse($form, array $errors) {
     $box = id(new PHUIObjectBoxView())
       ->setFormErrors($errors)
       ->setForm($form);
 
     $crumbs = id($this->buildApplicationCrumbs())
       ->addTextCrumb(pht('Test Console'))
       ->setBorder(true);
 
     $title = pht('Test Console');
 
     $header = id(new PHUIHeaderView())
       ->setHeader($title)
       ->setHeaderIcon('fa-desktop');
 
     $view = id(new PHUITwoColumnView())
       ->setHeader($header)
       ->setFooter($box);
 
     return $this->newPage()
       ->setTitle($title)
       ->setCrumbs($crumbs)
       ->appendChild($view);
   }
 
 }