diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php
index 79d9e93af..a9b28c2db 100644
--- a/src/applications/audit/editor/PhabricatorAuditEditor.php
+++ b/src/applications/audit/editor/PhabricatorAuditEditor.php
@@ -1,473 +1,486 @@
 <?php
 
 final class PhabricatorAuditEditor
   extends PhabricatorApplicationTransactionEditor {
 
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
     $types[] = PhabricatorTransactions::TYPE_COMMENT;
     $types[] = PhabricatorTransactions::TYPE_EDGE;
 
     // 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:
         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:
         return $xaction->getNewValue();
     }
 
     return parent::getCustomTransactionNewValue($object, $xaction);
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
       case PhabricatorTransactions::TYPE_EDGE:
       case PhabricatorAuditActionConstants::ACTION:
       case PhabricatorAuditActionConstants::INLINE:
       case PhabricatorAuditActionConstants::ADD_AUDITORS:
         return;
     }
 
     return parent::applyCustomInternalTransaction($object, $xaction);
   }
 
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
       case PhabricatorTransactions::TYPE_EDGE:
       case PhabricatorAuditActionConstants::ACTION:
       case PhabricatorAuditActionConstants::INLINE:
         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])) {
             continue;
           }
 
           $audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED;
           $requests[] = id (new PhabricatorRepositoryAuditRequest())
             ->setCommitPHID($object->getPHID())
             ->setAuditorPHID($phid)
             ->setAuditStatus($audit_requested)
             ->setAuditReasons(
               array(
                 'Added by '.$actor->getUsername(),
               ))
             ->save();
         }
 
         $object->attachAudits($requests);
         return;
     }
 
     return parent::applyCustomExternalTransaction($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->requireActor()->getPHID();
+    $actor_phid = $this->getActingAsPHID();
     $actor_is_author = ($object->getAuthorPHID()) &&
       ($actor_phid == $object->getAuthorPHID());
 
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         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();
 
     return $xactions;
   }
 
   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->requireActor()->getPHID());
+      ($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 shouldSendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
-    return true;
+    return $this->isCommitMostlyImported($object);
   }
 
   protected function buildReplyHandler(PhabricatorLiskDAO $object) {
     $reply_handler = PhabricatorEnv::newObjectFromConfig(
       'metamta.diffusion.reply-handler');
     $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}";
 
     return id(new PhabricatorMetaMTAMail())
       ->setSubject($subject)
       ->addHeader('Thread-Topic', $thread_topic);
   }
 
   protected function getMailTo(PhabricatorLiskDAO $object) {
     $phids = array();
     if ($object->getAuthorPHID()) {
       $phids[] = $object->getAuthorPHID();
     }
 
     $status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
     foreach ($object->getAudits() as $audit) {
       if ($audit->getAuditStatus() != $status_resigned) {
         $phids[] = $audit->getAuditorPHID();
       }
     }
 
     return $phids;
   }
 
   protected function buildMailBody(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $body = parent::buildMailBody($object, $xactions);
 
     $type_inline = PhabricatorAuditActionConstants::INLINE;
 
     $inlines = array();
     foreach ($xactions as $xaction) {
       if ($xaction->getTransactionType() == $type_inline) {
         $inlines[] = $xaction;
       }
     }
 
     if ($inlines) {
       $body->addTextSection(
         pht('INLINE COMMENTS'),
         $this->renderInlineCommentsForMail($object, $inlines));
     }
 
     $monogram = $object->getRepository()->formatCommitName(
       $object->getCommitIdentifier());
 
     $body->addTextSection(
       pht('COMMIT'),
       PhabricatorEnv::getProductionURI('/'.$monogram));
 
     return $body;
   }
 
   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);
   }
 
   protected function shouldPublishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions) {
-    return true;
+    return $this->isCommitMostlyImported($object);
+  }
+
+  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);
   }
 
 }
diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php
index 0fb2c5317..8bfc106bf 100644
--- a/src/applications/differential/editor/DifferentialTransactionEditor.php
+++ b/src/applications/differential/editor/DifferentialTransactionEditor.php
@@ -1,1749 +1,1748 @@
 <?php
 
 final class DifferentialTransactionEditor
   extends PhabricatorApplicationTransactionEditor {
 
   private $heraldEmailPHIDs;
   private $changedPriorToCommitURI;
   private $isCloseByCommit;
 
   public function getDiffUpdateTransaction(array $xactions) {
     $type_update = DifferentialTransaction::TYPE_UPDATE;
 
     foreach ($xactions as $xaction) {
       if ($xaction->getTransactionType() == $type_update) {
         return $xaction;
       }
     }
 
     return null;
   }
 
   public function setIsCloseByCommit($is_close_by_commit) {
     $this->isCloseByCommit = $is_close_by_commit;
     return $this;
   }
 
   public function getIsCloseByCommit() {
     return $this->isCloseByCommit;
   }
 
   public function setChangedPriorToCommitURI($uri) {
     $this->changedPriorToCommitURI = $uri;
     return $this;
   }
 
   public function getChangedPriorToCommitURI() {
     return $this->changedPriorToCommitURI;
   }
 
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
     $types[] = PhabricatorTransactions::TYPE_COMMENT;
     $types[] = PhabricatorTransactions::TYPE_EDGE;
     $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
     $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
 
     $types[] = DifferentialTransaction::TYPE_ACTION;
     $types[] = DifferentialTransaction::TYPE_INLINE;
     $types[] = DifferentialTransaction::TYPE_STATUS;
     $types[] = DifferentialTransaction::TYPE_UPDATE;
 
     return $types;
   }
 
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return $object->getViewPolicy();
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return $object->getEditPolicy();
       case DifferentialTransaction::TYPE_ACTION:
         return null;
       case DifferentialTransaction::TYPE_INLINE:
         return null;
       case DifferentialTransaction::TYPE_UPDATE:
         if ($this->getIsNewObject()) {
           return null;
         } else {
           return $object->getActiveDiff()->getPHID();
         }
     }
 
     return parent::getCustomTransactionOldValue($object, $xaction);
   }
 
   protected function getCustomTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case DifferentialTransaction::TYPE_ACTION:
       case DifferentialTransaction::TYPE_UPDATE:
         return $xaction->getNewValue();
       case DifferentialTransaction::TYPE_INLINE:
         return null;
     }
 
     return parent::getCustomTransactionNewValue($object, $xaction);
   }
 
   protected function transactionHasEffect(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
+    $actor_phid = $this->getActingAsPHID();
+
     switch ($xaction->getTransactionType()) {
       case DifferentialTransaction::TYPE_INLINE:
         return $xaction->hasComment();
       case DifferentialTransaction::TYPE_ACTION:
         $status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
         $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED;
         $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
         $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
         $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED;
 
         $action_type = $xaction->getNewValue();
         switch ($action_type) {
           case DifferentialAction::ACTION_ACCEPT:
           case DifferentialAction::ACTION_REJECT:
             if ($action_type == DifferentialAction::ACTION_ACCEPT) {
               $new_status = DifferentialReviewerStatus::STATUS_ACCEPTED;
             } else {
               $new_status = DifferentialReviewerStatus::STATUS_REJECTED;
             }
 
             $actor = $this->getActor();
-            $actor_phid = $actor->getPHID();
 
             // These transactions can cause effects in two ways: by altering the
             // status of an existing reviewer; or by adding the actor as a new
             // reviewer.
 
             $will_add_reviewer = true;
             foreach ($object->getReviewerStatus() as $reviewer) {
               if ($reviewer->hasAuthority($actor)) {
                 if ($reviewer->getStatus() != $new_status) {
                   return true;
                 }
               }
               if ($reviewer->getReviewerPHID() == $actor_phid) {
                 $will_add_reviwer = false;
               }
             }
 
             return $will_add_reviewer;
           case DifferentialAction::ACTION_CLOSE:
             return ($object->getStatus() != $status_closed);
           case DifferentialAction::ACTION_ABANDON:
             return ($object->getStatus() != $status_abandoned);
           case DifferentialAction::ACTION_RECLAIM:
             return ($object->getStatus() == $status_abandoned);
           case DifferentialAction::ACTION_REOPEN:
             return ($object->getStatus() == $status_closed);
           case DifferentialAction::ACTION_RETHINK:
             return ($object->getStatus() != $status_plan);
           case DifferentialAction::ACTION_REQUEST:
             return ($object->getStatus() != $status_review);
           case DifferentialAction::ACTION_RESIGN:
-            $actor_phid = $this->getActor()->getPHID();
             foreach ($object->getReviewerStatus() as $reviewer) {
               if ($reviewer->getReviewerPHID() == $actor_phid) {
                 return true;
               }
             }
             return false;
           case DifferentialAction::ACTION_CLAIM:
-            $actor_phid = $this->getActor()->getPHID();
             return ($actor_phid != $object->getAuthorPHID());
         }
     }
 
     return parent::transactionHasEffect($object, $xaction);
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
     $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
     $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED;
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         $object->setViewPolicy($xaction->getNewValue());
         return;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         $object->setEditPolicy($xaction->getNewValue());
         return;
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
       case PhabricatorTransactions::TYPE_COMMENT:
       case DifferentialTransaction::TYPE_INLINE:
         return;
       case PhabricatorTransactions::TYPE_EDGE:
         return;
       case DifferentialTransaction::TYPE_UPDATE:
         if (!$this->getIsCloseByCommit() &&
             (($object->getStatus() == $status_revision) ||
              ($object->getStatus() == $status_plan))) {
           $object->setStatus($status_review);
         }
 
         $diff = $this->requireDiff($xaction->getNewValue());
 
         $object->setLineCount($diff->getLineCount());
         $object->setRepositoryPHID($diff->getRepositoryPHID());
         $object->setArcanistProjectPHID($diff->getArcanistProjectPHID());
         $object->attachActiveDiff($diff);
 
         // TODO: Update the `diffPHID` once we add that.
         return;
       case DifferentialTransaction::TYPE_ACTION:
         switch ($xaction->getNewValue()) {
           case DifferentialAction::ACTION_RESIGN:
           case DifferentialAction::ACTION_ACCEPT:
           case DifferentialAction::ACTION_REJECT:
             // These have no direct effects, and affect review status only
             // indirectly by altering reviewers with TYPE_EDGE transactions.
             return;
           case DifferentialAction::ACTION_ABANDON:
             $object->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED);
             return;
           case DifferentialAction::ACTION_RETHINK:
             $object->setStatus($status_plan);
             return;
           case DifferentialAction::ACTION_RECLAIM:
             $object->setStatus($status_review);
             return;
           case DifferentialAction::ACTION_REOPEN:
             $object->setStatus($status_review);
             return;
           case DifferentialAction::ACTION_REQUEST:
             $object->setStatus($status_review);
             return;
           case DifferentialAction::ACTION_CLOSE:
             $object->setStatus(ArcanistDifferentialRevisionStatus::CLOSED);
             return;
           case DifferentialAction::ACTION_CLAIM:
-            $object->setAuthorPHID($this->getActor()->getPHID());
+            $object->setAuthorPHID($this->getActingAsPHID());
             return;
         }
         break;
     }
 
     return parent::applyCustomInternalTransaction($object, $xaction);
   }
 
   protected function expandTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $results = parent::expandTransaction($object, $xaction);
 
     $actor = $this->getActor();
-    $actor_phid = $actor->getPHID();
+    $actor_phid = $this->getActingAsPHID();
     $type_edge = PhabricatorTransactions::TYPE_EDGE;
 
     $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED;
 
     $edge_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER;
     $edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST;
 
     $is_sticky_accept = PhabricatorEnv::getEnvConfig(
       'differential.sticky-accept');
 
     $downgrade_rejects = false;
     $downgrade_accepts = false;
     if ($this->getIsCloseByCommit()) {
       // Never downgrade reviewers when we're closing a revision after a
       // commit.
     } else {
       switch ($xaction->getTransactionType()) {
         case DifferentialTransaction::TYPE_UPDATE:
           $downgrade_rejects = true;
           if (!$is_sticky_accept) {
             // If "sticky accept" is disabled, also downgrade the accepts.
             $downgrade_accepts = true;
           }
           break;
         case DifferentialTransaction::TYPE_ACTION:
           switch ($xaction->getNewValue()) {
             case DifferentialAction::ACTION_REQUEST:
               $downgrade_rejects = true;
               if ((!$is_sticky_accept) ||
                   ($object->getStatus() != $status_plan)) {
                 // If the old state isn't "changes planned", downgrade the
                 // accepts. This exception allows an accepted revision to
                 // go through Plan Changes -> Request Review to return to
                 // "accepted" if the author didn't update the revision.
                 $downgrade_accepts = true;
               }
               break;
           }
           break;
       }
     }
 
     $new_accept = DifferentialReviewerStatus::STATUS_ACCEPTED;
     $new_reject = DifferentialReviewerStatus::STATUS_REJECTED;
     $old_accept = DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER;
     $old_reject = DifferentialReviewerStatus::STATUS_REJECTED_OLDER;
 
     if ($downgrade_rejects || $downgrade_accepts) {
       // When a revision is updated, change all "reject" to "rejected older
       // revision". This means we won't immediately push the update back into
       // "needs review", but outstanding rejects will still block it from
       // moving to "accepted".
 
       // We also do this for "Request Review", even though the diff is not
       // updated directly. Essentially, this acts like an update which doesn't
       // actually change the diff text.
 
       $edits = array();
       foreach ($object->getReviewerStatus() as $reviewer) {
         if ($downgrade_rejects) {
           if ($reviewer->getStatus() == $new_reject) {
             $edits[$reviewer->getReviewerPHID()] = array(
               'data' => array(
                 'status' => $old_reject,
               ),
             );
           }
         }
 
         if ($downgrade_accepts) {
           if ($reviewer->getStatus() == $new_accept) {
             $edits[$reviewer->getReviewerPHID()] = array(
               'data' => array(
                 'status' => $old_accept,
               ),
             );
           }
         }
       }
 
       if ($edits) {
         $results[] = id(new DifferentialTransaction())
           ->setTransactionType($type_edge)
           ->setMetadataValue('edge:type', $edge_reviewer)
           ->setIgnoreOnNoEffect(true)
           ->setNewValue(array('+' => $edits));
       }
     }
 
     switch ($xaction->getTransactionType()) {
       case DifferentialTransaction::TYPE_UPDATE:
         if ($this->getIsCloseByCommit()) {
           // Don't bother with any of this if this update is a side effect of
           // commit detection.
           break;
         }
 
         // When a revision is updated and the diff comes from a branch named
         // "T123" or similar, automatically associate the commit with the
         // task that the branch names.
 
         $maniphest = 'PhabricatorManiphestApplication';
         if (PhabricatorApplication::isClassInstalled($maniphest)) {
           $diff = $this->requireDiff($xaction->getNewValue());
           $branch = $diff->getBranch();
 
           // No "$", to allow for branches like T123_demo.
           $match = null;
           if (preg_match('/^T(\d+)/i', $branch, $match)) {
             $task_id = $match[1];
             $tasks = id(new ManiphestTaskQuery())
               ->setViewer($this->getActor())
               ->withIDs(array($task_id))
               ->execute();
             if ($tasks) {
               $task = head($tasks);
               $task_phid = $task->getPHID();
 
               $results[] = id(new DifferentialTransaction())
                 ->setTransactionType($type_edge)
                 ->setMetadataValue('edge:type', $edge_ref_task)
                 ->setIgnoreOnNoEffect(true)
                 ->setNewValue(array('+' => array($task_phid => $task_phid)));
             }
           }
         }
         break;
 
       case PhabricatorTransactions::TYPE_COMMENT:
         // When a user leaves a comment, upgrade their reviewer status from
         // "added" to "commented" if they're also a reviewer. We may further
         // upgrade this based on other actions in the transaction group.
 
         $status_added = DifferentialReviewerStatus::STATUS_ADDED;
         $status_commented = DifferentialReviewerStatus::STATUS_COMMENTED;
 
         $data = array(
           'status' => $status_commented,
         );
 
         $edits = array();
         foreach ($object->getReviewerStatus() as $reviewer) {
           if ($reviewer->getReviewerPHID() == $actor_phid) {
             if ($reviewer->getStatus() == $status_added) {
               $edits[$actor_phid] = array(
                 'data' => $data,
               );
             }
           }
         }
 
         if ($edits) {
           $results[] = id(new DifferentialTransaction())
             ->setTransactionType($type_edge)
             ->setMetadataValue('edge:type', $edge_reviewer)
             ->setIgnoreOnNoEffect(true)
             ->setNewValue(array('+' => $edits));
         }
         break;
 
       case DifferentialTransaction::TYPE_ACTION:
         $action_type = $xaction->getNewValue();
 
         switch ($action_type) {
           case DifferentialAction::ACTION_ACCEPT:
           case DifferentialAction::ACTION_REJECT:
             if ($action_type == DifferentialAction::ACTION_ACCEPT) {
               $data = array(
                 'status' => DifferentialReviewerStatus::STATUS_ACCEPTED,
               );
             } else {
               $data = array(
                 'status' => DifferentialReviewerStatus::STATUS_REJECTED,
               );
             }
 
             $edits = array();
 
             foreach ($object->getReviewerStatus() as $reviewer) {
               if ($reviewer->hasAuthority($actor)) {
                 $edits[$reviewer->getReviewerPHID()] = array(
                   'data' => $data,
                 );
               }
             }
 
             // Also either update or add the actor themselves as a reviewer.
             $edits[$actor_phid] = array(
               'data' => $data,
             );
 
             $results[] = id(new DifferentialTransaction())
               ->setTransactionType($type_edge)
               ->setMetadataValue('edge:type', $edge_reviewer)
               ->setIgnoreOnNoEffect(true)
               ->setNewValue(array('+' => $edits));
             break;
 
           case DifferentialAction::ACTION_CLAIM:
             // If the user is commandeering, add the previous owner as a
             // reviewer and remove the actor.
 
             $edits = array(
               '-' => array(
                 $actor_phid => $actor_phid,
               ),
             );
 
             $owner_phid = $object->getAuthorPHID();
             if ($owner_phid) {
               $reviewer = new DifferentialReviewer(
                 $owner_phid,
                 array(
                   'status' => DifferentialReviewerStatus::STATUS_ADDED,
                 ));
 
               $edits['+'] = array(
                 $owner_phid => array(
                   'data' => $reviewer->getEdgeData(),
                 ),
               );
             }
 
             // NOTE: We're setting setIsCommandeerSideEffect() on this because
             // normally you can't add a revision's author as a reviewer, but
             // this action swaps them after validation executes.
 
             $results[] = id(new DifferentialTransaction())
               ->setTransactionType($type_edge)
               ->setMetadataValue('edge:type', $edge_reviewer)
               ->setIgnoreOnNoEffect(true)
               ->setIsCommandeerSideEffect(true)
               ->setNewValue($edits);
 
             break;
           case DifferentialAction::ACTION_RESIGN:
             // If the user is resigning, add a separate reviewer edit
             // transaction which removes them as a reviewer.
 
             $results[] = id(new DifferentialTransaction())
               ->setTransactionType($type_edge)
               ->setMetadataValue('edge:type', $edge_reviewer)
               ->setIgnoreOnNoEffect(true)
               ->setNewValue(
                 array(
                   '-' => array(
                     $actor_phid => $actor_phid,
                   ),
                 ));
 
             break;
         }
       break;
     }
 
     return $results;
   }
 
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return;
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
       case PhabricatorTransactions::TYPE_EDGE:
       case PhabricatorTransactions::TYPE_COMMENT:
       case DifferentialTransaction::TYPE_ACTION:
       case DifferentialTransaction::TYPE_INLINE:
         return;
       case DifferentialTransaction::TYPE_UPDATE:
         // Now that we're inside the transaction, do a final check.
         $diff = $this->requireDiff($xaction->getNewValue());
 
         // TODO: It would be slightly cleaner to just revalidate this
         // transaction somehow using the same validation code, but that's
         // not easy to do at the moment.
 
         $revision_id = $diff->getRevisionID();
         if ($revision_id && ($revision_id != $object->getID())) {
           throw new Exception(
             pht(
               'Diff is already attached to another revision. You lost '.
               'a race?'));
         }
 
         $diff->setRevisionID($object->getID());
         $diff->save();
         return;
     }
 
     return parent::applyCustomExternalTransaction($object, $xaction);
   }
 
   protected function mergeEdgeData($type, array $u, array $v) {
     $result = parent::mergeEdgeData($type, $u, $v);
 
     switch ($type) {
       case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER:
         // When the same reviewer has their status updated by multiple
         // transactions, we want the strongest status to win. An example of
         // this is when a user adds a comment and also accepts a revision which
         // they are a reviewer on. The comment creates a "commented" status,
         // while the accept creates an "accepted" status. Since accept is
         // stronger, it should win and persist.
 
         $u_status = idx($u, 'status');
         $v_status = idx($v, 'status');
         $u_str = DifferentialReviewerStatus::getStatusStrength($u_status);
         $v_str = DifferentialReviewerStatus::getStatusStrength($v_status);
         if ($u_str > $v_str) {
           $result['status'] = $u_status;
         } else {
           $result['status'] = $v_status;
         }
         break;
     }
 
     return $result;
   }
 
   protected function applyFinalEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     // Load the most up-to-date version of the revision and its reviewers,
     // so we don't need to try to deduce the state of reviewers by examining
     // all the changes made by the transactions. Then, update the reviewers
     // on the object to make sure we're acting on the current reviewer set
     // (and, for example, sending mail to the right people).
 
     $new_revision = id(new DifferentialRevisionQuery())
       ->setViewer($this->getActor())
       ->needReviewerStatus(true)
       ->needActiveDiffs(true)
       ->withIDs(array($object->getID()))
       ->executeOne();
     if (!$new_revision) {
       throw new Exception(
         pht('Failed to load revision from transaction finalization.'));
     }
 
     $object->attachReviewerStatus($new_revision->getReviewerStatus());
     $object->attachActiveDiff($new_revision->getActiveDiff());
     $object->attachRepository($new_revision->getRepository());
 
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         case DifferentialTransaction::TYPE_UPDATE:
           $diff = $this->requireDiff($xaction->getNewValue(), true);
 
           // Update these denormalized index tables when we attach a new
           // diff to a revision.
 
           $this->updateRevisionHashTable($object, $diff);
           $this->updateAffectedPathTable($object, $diff);
           break;
       }
     }
 
     $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
     $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
     $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
 
     $old_status = $object->getStatus();
     switch ($old_status) {
       case $status_accepted:
       case $status_revision:
       case $status_review:
         // Try to move a revision to "accepted". We look for:
         //
         //   - at least one accepting reviewer who is a user; and
         //   - no rejects; and
         //   - no rejects of older diffs; and
         //   - no blocking reviewers.
 
         $has_accepting_user = false;
         $has_rejecting_reviewer = false;
         $has_rejecting_older_reviewer = false;
         $has_blocking_reviewer = false;
         foreach ($object->getReviewerStatus() as $reviewer) {
           $reviewer_status = $reviewer->getStatus();
           switch ($reviewer_status) {
             case DifferentialReviewerStatus::STATUS_REJECTED:
               $has_rejecting_reviewer = true;
               break;
             case DifferentialReviewerStatus::STATUS_REJECTED_OLDER:
               $has_rejecting_older_reviewer = true;
               break;
             case DifferentialReviewerStatus::STATUS_BLOCKING:
               $has_blocking_reviewer = true;
               break;
             case DifferentialReviewerStatus::STATUS_ACCEPTED:
               if ($reviewer->isUser()) {
                 $has_accepting_user = true;
               }
               break;
           }
         }
 
         $new_status = null;
         if ($has_accepting_user &&
             !$has_rejecting_reviewer &&
             !$has_rejecting_older_reviewer &&
             !$has_blocking_reviewer) {
           $new_status = $status_accepted;
         } else if ($has_rejecting_reviewer) {
           // This isn't accepted, and there's at least one rejecting reviewer,
           // so the revision needs changes. This usually happens after a
           // "reject".
           $new_status = $status_revision;
         } else if ($old_status == $status_accepted) {
           // This revision was accepted, but it no longer satisfies the
           // conditions for acceptance. This usually happens after an accepting
           // reviewer resigns or is removed.
           $new_status = $status_review;
         }
 
         if ($new_status !== null && ($new_status != $old_status)) {
           $xaction = id(new DifferentialTransaction())
             ->setTransactionType(DifferentialTransaction::TYPE_STATUS)
             ->setOldValue($old_status)
             ->setNewValue($new_status);
 
           $xaction = $this->populateTransaction($object, $xaction)->save();
 
           $xactions[] = $xaction;
 
           $object->setStatus($new_status)->save();
         }
         break;
       default:
         // Revisions can't transition out of other statuses (like closed or
         // abandoned) as a side effect of reviewer status changes.
         break;
     }
 
     return $xactions;
   }
 
   protected function validateTransaction(
     PhabricatorLiskDAO $object,
     $type,
     array $xactions) {
 
     $errors = parent::validateTransaction($object, $type, $xactions);
 
     $config_self_accept_key = 'differential.allow-self-accept';
     $allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);
 
     foreach ($xactions as $xaction) {
       switch ($type) {
         case PhabricatorTransactions::TYPE_EDGE:
           switch ($xaction->getMetadataValue('edge:type')) {
             case PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER:
 
               // Prevent the author from becoming a reviewer.
 
               // NOTE: This is pretty gross, but this restriction is unusual.
               // If we end up with too much more of this, we should try to clean
               // this up -- maybe by moving validation to after transactions
               // are adjusted (so we can just examine the final value) or adding
               // a second phase there?
 
               $author_phid = $object->getAuthorPHID();
               $new = $xaction->getNewValue();
 
               $add = idx($new, '+', array());
               $eq = idx($new, '=', array());
               $phids = array_keys($add + $eq);
 
               foreach ($phids as $phid) {
                 if (($phid == $author_phid) &&
                     !$allow_self_accept &&
                     !$xaction->getIsCommandeerSideEffect()) {
                   $errors[] =
                     new PhabricatorApplicationTransactionValidationError(
                       $type,
                       pht('Invalid'),
                       pht('The author of a revision can not be a reviewer.'),
                       $xaction);
                 }
               }
               break;
           }
           break;
         case DifferentialTransaction::TYPE_UPDATE:
           $diff = $this->loadDiff($xaction->getNewValue());
           if (!$diff) {
             $errors[] = new PhabricatorApplicationTransactionValidationError(
               $type,
               pht('Invalid'),
               pht('The specified diff does not exist.'),
               $xaction);
           } else if (($diff->getRevisionID()) &&
             ($diff->getRevisionID() != $object->getID())) {
             $errors[] = new PhabricatorApplicationTransactionValidationError(
               $type,
               pht('Invalid'),
               pht(
                 'You can not update this revision to the specified diff, '.
                 'because the diff is already attached to another revision.'),
               $xaction);
           }
           break;
         case DifferentialTransaction::TYPE_ACTION:
           $error = $this->validateDifferentialAction(
             $object,
             $type,
             $xaction,
             $xaction->getNewValue());
           if ($error) {
             $errors[] = new PhabricatorApplicationTransactionValidationError(
               $type,
               pht('Invalid'),
               $error,
               $xaction);
           }
           break;
       }
     }
 
     return $errors;
   }
 
   private function validateDifferentialAction(
     DifferentialRevision $revision,
     $type,
     DifferentialTransaction $xaction,
     $action) {
 
     $author_phid = $revision->getAuthorPHID();
-    $actor_phid = $this->getActor()->getPHID();
+    $actor_phid = $this->getActingAsPHID();
     $actor_is_author = ($author_phid == $actor_phid);
 
     $config_abandon_key = 'differential.always-allow-abandon';
     $always_allow_abandon = PhabricatorEnv::getEnvConfig($config_abandon_key);
 
     $config_close_key = 'differential.always-allow-close';
     $always_allow_close = PhabricatorEnv::getEnvConfig($config_close_key);
 
     $config_reopen_key = 'differential.allow-reopen';
     $allow_reopen = PhabricatorEnv::getEnvConfig($config_reopen_key);
 
     $config_self_accept_key = 'differential.allow-self-accept';
     $allow_self_accept = PhabricatorEnv::getEnvConfig($config_self_accept_key);
 
     $revision_status = $revision->getStatus();
 
     $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
     $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED;
     $status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
 
     switch ($action) {
       case DifferentialAction::ACTION_ACCEPT:
         if ($actor_is_author && !$allow_self_accept) {
           return pht(
             'You can not accept this revision because you are the owner.');
         }
 
         if ($revision_status == $status_abandoned) {
           return pht(
             'You can not accept this revision because it has been '.
             'abandoned.');
         }
 
         if ($revision_status == $status_closed) {
           return pht(
             'You can not accept this revision because it has already been '.
             'closed.');
         }
 
         // TODO: It would be nice to make this generic at some point.
         $signatures = DifferentialRequiredSignaturesField::loadForRevision(
           $revision);
         foreach ($signatures as $phid => $signed) {
           if (!$signed) {
             return pht(
               'You can not accept this revision because the author has '.
               'not signed all of the required legal documents.');
           }
         }
 
         break;
 
       case DifferentialAction::ACTION_REJECT:
         if ($actor_is_author) {
           return pht(
             'You can not request changes to your own revision.');
         }
 
         if ($revision_status == $status_abandoned) {
           return pht(
             'You can not request changes to this revision because it has been '.
             'abandoned.');
         }
 
         if ($revision_status == $status_closed) {
           return pht(
             'You can not request changes to this revision because it has '.
             'already been closed.');
         }
         break;
 
       case DifferentialAction::ACTION_RESIGN:
         // You can always resign from a revision if you're a reviewer. If you
         // aren't, this is a no-op rather than invalid.
         break;
 
       case DifferentialAction::ACTION_CLAIM:
         // You can claim a revision if you're not the owner. If you are, this
         // is a no-op rather than invalid.
 
         if ($revision_status == $status_closed) {
           return pht(
             'You can not commandeer this revision because it has already been '.
             'closed.');
         }
         break;
 
       case DifferentialAction::ACTION_ABANDON:
         if (!$actor_is_author && !$always_allow_abandon) {
           return pht(
             'You can not abandon this revision because you do not own it. '.
             'You can only abandon revisions you own.');
         }
 
         if ($revision_status == $status_closed) {
           return pht(
             'You can not abandon this revision because it has already been '.
             'closed.');
         }
 
         // NOTE: Abandons of already-abandoned revisions are treated as no-op
         // instead of invalid. Other abandons are OK.
 
         break;
 
       case DifferentialAction::ACTION_RECLAIM:
         if (!$actor_is_author) {
           return pht(
             'You can not reclaim this revision because you do not own '.
             'it. You can only reclaim revisions you own.');
         }
 
         if ($revision_status == $status_closed) {
           return pht(
             'You can not reclaim this revision because it has already been '.
             'closed.');
         }
 
         // NOTE: Reclaims of other non-abandoned revisions are treated as no-op
         // instead of invalid.
 
         break;
 
       case DifferentialAction::ACTION_REOPEN:
         if (!$allow_reopen) {
           return pht(
             'The reopen action is not enabled on this Phabricator install. '.
             'Adjust your configuration to enable it.');
         }
 
         // NOTE: If the revision is not closed, this is caught as a no-op
         // instead of an invalid transaction.
 
         break;
 
       case DifferentialAction::ACTION_RETHINK:
         if (!$actor_is_author) {
           return pht(
             'You can not plan changes to this revision because you do not '.
             'own it. To plan changes to a revision, you must be its owner.');
         }
 
         switch ($revision_status) {
           case ArcanistDifferentialRevisionStatus::ACCEPTED:
           case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
           case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
             // These are OK.
             break;
           case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
             // Let this through, it's a no-op.
             break;
           case ArcanistDifferentialRevisionStatus::ABANDONED:
             return pht(
               'You can not plan changes to this revision because it has '.
               'been abandoned.');
           case ArcanistDifferentialRevisionStatus::CLOSED:
             return pht(
               'You can not plan changes to this revision because it has '.
               'already been closed.');
           default:
             throw new Exception(
               pht(
                 'Encountered unexpected revision status ("%s") when '.
                 'validating "%s" action.',
                 $revision_status,
                 $action));
         }
         break;
 
       case DifferentialAction::ACTION_REQUEST:
         if (!$actor_is_author) {
           return pht(
             'You can not request review of this revision because you do '.
             'not own it. To request review of a revision, you must be its '.
             'owner.');
         }
 
         switch ($revision_status) {
           case ArcanistDifferentialRevisionStatus::ACCEPTED:
           case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
           case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
             // These are OK.
             break;
           case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
             // This will be caught as "no effect" later on.
             break;
           case ArcanistDifferentialRevisionStatus::ABANDONED:
             return pht(
               'You can not request review of this revision because it has '.
               'been abandoned. Instead, reclaim it.');
           case ArcanistDifferentialRevisionStatus::CLOSED:
             return pht(
               'You can not request review of this revision because it has '.
               'already been closed.');
           default:
             throw new Exception(
               pht(
                 'Encountered unexpected revision status ("%s") when '.
                 'validating "%s" action.',
                 $revision_status,
                 $action));
         }
         break;
 
       case DifferentialAction::ACTION_CLOSE:
         // We force revisions closed when we discover a corresponding commit.
         // In this case, revisions are allowed to transition to closed from
         // any state. This is an automated action taken by the daemons.
 
         if (!$this->getIsCloseByCommit()) {
           if (!$actor_is_author && !$always_allow_close) {
             return pht(
               'You can not close this revision because you do not own it. To '.
               'close a revision, you must be its owner.');
           }
 
           if ($revision_status != $status_accepted) {
             return pht(
               'You can not close this revision because it has not been '.
               'accepted. You can only close accepted revisions.');
           }
         }
         break;
     }
 
     return null;
   }
 
   protected function sortTransactions(array $xactions) {
     $xactions = parent::sortTransactions($xactions);
 
     $head = array();
     $tail = array();
 
     foreach ($xactions as $xaction) {
       $type = $xaction->getTransactionType();
       if ($type == DifferentialTransaction::TYPE_INLINE) {
         $tail[] = $xaction;
       } else {
         $head[] = $xaction;
       }
     }
 
     return array_values(array_merge($head, $tail));
   }
 
   protected function requireCapabilities(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
     }
 
     return parent::requireCapabilities($object, $xaction);
   }
 
   protected function shouldPublishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return true;
   }
 
   protected function shouldSendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return true;
   }
 
   protected function getMailTo(PhabricatorLiskDAO $object) {
     $phids = array();
     $phids[] = $object->getAuthorPHID();
     foreach ($object->getReviewerStatus() as $reviewer) {
       $phids[] = $reviewer->getReviewerPHID();
     }
     return $phids;
   }
 
   protected function getMailCC(PhabricatorLiskDAO $object) {
     $phids = parent::getMailCC($object);
 
     if ($this->heraldEmailPHIDs) {
       foreach ($this->heraldEmailPHIDs as $phid) {
         $phids[] = $phid;
       }
     }
 
     return $phids;
   }
 
   protected function getMailAction(
     PhabricatorLiskDAO $object,
     array $xactions) {
     $action = parent::getMailAction($object, $xactions);
 
     $strongest = $this->getStrongestAction($object, $xactions);
     switch ($strongest->getTransactionType()) {
       case DifferentialTransaction::TYPE_UPDATE:
         $count = new PhutilNumber($object->getLineCount());
         $action = pht('%s, %s line(s)', $action, $count);
         break;
     }
 
     return $action;
   }
 
   protected function getMailSubjectPrefix() {
     return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
   }
 
   protected function getMailThreadID(PhabricatorLiskDAO $object) {
     // This is nonstandard, but retains threading with older messages.
     $phid = $object->getPHID();
     return "differential-rev-{$phid}-req";
   }
 
   protected function buildReplyHandler(PhabricatorLiskDAO $object) {
     return id(new DifferentialReplyHandler())
       ->setMailReceiver($object);
   }
 
   protected function buildMailTemplate(PhabricatorLiskDAO $object) {
     $id = $object->getID();
     $title = $object->getTitle();
 
     $original_title = $object->getOriginalTitle();
 
     $subject = "D{$id}: {$title}";
     $thread_topic = "D{$id}: {$original_title}";
 
     return id(new PhabricatorMetaMTAMail())
       ->setSubject($subject)
       ->addHeader('Thread-Topic', $thread_topic);
   }
 
   protected function buildMailBody(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $body = parent::buildMailBody($object, $xactions);
 
     $type_inline = DifferentialTransaction::TYPE_INLINE;
 
     $inlines = array();
     foreach ($xactions as $xaction) {
       if ($xaction->getTransactionType() == $type_inline) {
         $inlines[] = $xaction;
       }
     }
 
     $changed_uri = $this->getChangedPriorToCommitURI();
     if ($changed_uri) {
       $body->addTextSection(
         pht('CHANGED PRIOR TO COMMIT'),
         $changed_uri);
     }
 
     if ($inlines) {
       $body->addTextSection(
         pht('INLINE COMMENTS'),
         $this->renderInlineCommentsForMail($object, $inlines));
     }
 
     $body->addTextSection(
       pht('REVISION DETAIL'),
       PhabricatorEnv::getProductionURI('/D'.$object->getID()));
 
     $update_xaction = null;
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         case DifferentialTransaction::TYPE_UPDATE:
           $update_xaction = $xaction;
           break;
       }
     }
 
     if ($update_xaction) {
       $diff = $this->requireDiff($update_xaction->getNewValue(), true);
 
       $body->addTextSection(
         pht('AFFECTED FILES'),
         $this->renderAffectedFilesForMail($diff));
 
       $config_key_inline = 'metamta.differential.inline-patches';
       $config_inline = PhabricatorEnv::getEnvConfig($config_key_inline);
 
       $config_key_attach = 'metamta.differential.attach-patches';
       $config_attach = PhabricatorEnv::getEnvConfig($config_key_attach);
 
       if ($config_inline || $config_attach) {
         $patch = $this->renderPatchForMail($diff);
         $lines = count(phutil_split_lines($patch));
 
         if ($config_inline && ($lines <= $config_inline)) {
           $body->addTextSection(
             pht('CHANGE DETAILS'),
             $patch);
         }
 
         if ($config_attach) {
           $name = pht('D%s.%s.patch', $object->getID(), $diff->getID());
           $mime_type = 'text/x-patch; charset=utf-8';
           $body->addAttachment(
             new PhabricatorMetaMTAAttachment($patch, $name, $mime_type));
         }
       }
     }
 
     return $body;
   }
 
   protected function supportsSearch() {
     return true;
   }
 
   protected function extractFilePHIDsFromCustomTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {}
 
     return parent::extractFilePHIDsFromCustomTransaction($object, $xaction);
   }
 
   protected function expandCustomRemarkupBlockTransactions(
     PhabricatorLiskDAO $object,
     array $xactions,
     $blocks,
     PhutilMarkupEngine $engine) {
 
     $flat_blocks = array_mergev($blocks);
     $huge_block = implode("\n\n", $flat_blocks);
 
     $task_map = array();
     $task_refs = id(new ManiphestCustomFieldStatusParser())
       ->parseCorpus($huge_block);
     foreach ($task_refs as $match) {
       foreach ($match['monograms'] as $monogram) {
         $task_id = (int)trim($monogram, 'tT');
         $task_map[$task_id] = true;
       }
     }
 
     $rev_map = array();
     $rev_refs = id(new DifferentialCustomFieldDependsOnParser())
       ->parseCorpus($huge_block);
     foreach ($rev_refs as $match) {
       foreach ($match['monograms'] as $monogram) {
         $rev_id = (int)trim($monogram, 'dD');
         $rev_map[$rev_id] = true;
       }
     }
 
     $edges = array();
 
     if ($task_map) {
       $tasks = id(new ManiphestTaskQuery())
         ->setViewer($this->getActor())
         ->withIDs(array_keys($task_map))
         ->execute();
 
       if ($tasks) {
         $edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST;
         $edges[$edge_related] = mpull($tasks, 'getPHID', 'getPHID');
       }
     }
 
     if ($rev_map) {
       $revs = id(new DifferentialRevisionQuery())
         ->setViewer($this->getActor())
         ->withIDs(array_keys($rev_map))
         ->execute();
       $rev_phids = mpull($revs, 'getPHID', 'getPHID');
 
       // NOTE: Skip any write attempts if a user cleverly implies a revision
       // depends upon itself.
       unset($rev_phids[$object->getPHID()]);
 
       if ($revs) {
         $edge_depends = PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV;
         $edges[$edge_depends] = $rev_phids;
       }
     }
 
     $result = array();
     foreach ($edges as $type => $specs) {
       $result[] = id(new DifferentialTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
         ->setMetadataValue('edge:type', $type)
         ->setNewValue(array('+' => $specs));
     }
 
     return $result;
   }
 
   private function renderInlineCommentsForMail(
     PhabricatorLiskDAO $object,
     array $inlines) {
 
     $context_key = 'metamta.differential.unified-comment-context';
     $show_context = PhabricatorEnv::getEnvConfig($context_key);
 
     $changeset_ids = array();
     foreach ($inlines as $inline) {
       $id = $inline->getComment()->getChangesetID();
       $changeset_ids[$id] = $id;
     }
 
     $changesets = id(new DifferentialChangesetQuery())
       ->setViewer($this->getActor())
       ->withIDs($changeset_ids)
       ->needHunks(true)
       ->execute();
 
     $inline_groups = DifferentialTransactionComment::sortAndGroupInlines(
       $inlines,
       $changesets);
 
     if ($show_context) {
       $hunk_parser = new DifferentialHunkParser();
     }
 
     $result = array();
     foreach ($inline_groups as $changeset_id => $group) {
       $changeset = idx($changesets, $changeset_id);
       if (!$changeset) {
         continue;
       }
 
       foreach ($group as $inline) {
         $comment = $inline->getComment();
         $file = $changeset->getFilename();
         $start = $comment->getLineNumber();
         $len = $comment->getLineLength();
         if ($len) {
           $range = $start.'-'.($start + $len);
         } else {
           $range = $start;
         }
 
         $inline_content = $comment->getContent();
 
         if (!$show_context) {
           $result[] = "{$file}:{$range} {$inline_content}";
         } else {
           $result[] = '================';
           $result[] = 'Comment at: '.$file.':'.$range;
           $result[] = $hunk_parser->makeContextDiff(
             $changeset->getHunks(),
             $comment->getIsNewFile(),
             $comment->getLineNumber(),
             $comment->getLineLength(),
             1);
           $result[] = '----------------';
 
           $result[] = $inline_content;
           $result[] = null;
         }
       }
     }
 
     return implode("\n", $result);
   }
 
   private function loadDiff($phid, $need_changesets = false) {
     $query = id(new DifferentialDiffQuery())
       ->withPHIDs(array($phid))
       ->setViewer($this->getActor());
 
     if ($need_changesets) {
       $query->needChangesets(true);
     }
 
     return $query->executeOne();
   }
 
   private function requireDiff($phid, $need_changesets = false) {
     $diff = $this->loadDiff($phid, $need_changesets);
     if (!$diff) {
       throw new Exception(pht('Diff "%s" does not exist!', $phid));
     }
 
     return $diff;
   }
 
 /* -(  Herald Integration  )------------------------------------------------- */
 
   protected function shouldApplyHeraldRules(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     if ($this->getIsNewObject()) {
       return true;
     }
 
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         case DifferentialTransaction::TYPE_UPDATE:
           if (!$this->getIsCloseByCommit()) {
             return true;
           }
           break;
         case DifferentialTransaction::TYPE_ACTION:
           switch ($xaction->getNewValue()) {
             case DifferentialAction::ACTION_CLAIM:
               // When users commandeer revisions, we may need to trigger
               // signatures or author-based rules.
               return true;
           }
           break;
       }
     }
 
     return parent::shouldApplyHeraldRules($object, $xactions);
   }
 
   protected function buildHeraldAdapter(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $unsubscribed_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
       $object->getPHID(),
       PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER);
 
     $subscribed_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID(
       $object->getPHID());
 
     $revision = id(new DifferentialRevisionQuery())
       ->setViewer($this->getActor())
       ->withPHIDs(array($object->getPHID()))
       ->needActiveDiffs(true)
       ->needReviewerStatus(true)
       ->executeOne();
     if (!$revision) {
       throw new Exception(
         pht(
           'Failed to load revision for Herald adapter construction!'));
     }
 
     $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter(
       $revision,
       $revision->getActiveDiff());
 
     $reviewers = $revision->getReviewerStatus();
     $reviewer_phids = mpull($reviewers, 'getReviewerPHID');
 
     $adapter->setExplicitCCs($subscribed_phids);
     $adapter->setExplicitReviewers($reviewer_phids);
     $adapter->setForbiddenCCs($unsubscribed_phids);
 
     $adapter->setIsNewObject($this->getIsNewObject());
 
     return $adapter;
   }
 
   protected function didApplyHeraldRules(
     PhabricatorLiskDAO $object,
     HeraldAdapter $adapter,
     HeraldTranscript $transcript) {
 
     $xactions = array();
 
     // Build a transaction to adjust CCs.
     $ccs = array(
       '+' => array_keys($adapter->getCCsAddedByHerald()),
       '-' => array_keys($adapter->getCCsRemovedByHerald()),
     );
     $value = array();
     foreach ($ccs as $type => $phids) {
       foreach ($phids as $phid) {
         $value[$type][$phid] = $phid;
       }
     }
 
     if ($value) {
       $xactions[] = id(new DifferentialTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
         ->setNewValue($value);
     }
 
     // Build a transaction to adjust reviewers.
     $reviewers = array(
       DifferentialReviewerStatus::STATUS_ADDED =>
         array_keys($adapter->getReviewersAddedByHerald()),
       DifferentialReviewerStatus::STATUS_BLOCKING =>
         array_keys($adapter->getBlockingReviewersAddedByHerald()),
     );
 
     $old_reviewers = $object->getReviewerStatus();
     $old_reviewers = mpull($old_reviewers, null, 'getReviewerPHID');
 
     $value = array();
     foreach ($reviewers as $status => $phids) {
       foreach ($phids as $phid) {
         if ($phid == $object->getAuthorPHID()) {
           // Don't try to add the revision's author as a reviewer, since this
           // isn't valid and doesn't make sense.
           continue;
         }
 
         // If the target is already a reviewer, don't try to change anything
         // if their current status is at least as strong as the new status.
         // For example, don't downgrade an "Accepted" to a "Blocking Reviewer".
         $old_reviewer = idx($old_reviewers, $phid);
         if ($old_reviewer) {
           $old_status = $old_reviewer->getStatus();
 
           $old_strength = DifferentialReviewerStatus::getStatusStrength(
             $old_status);
           $new_strength = DifferentialReviewerStatus::getStatusStrength(
             $status);
 
           if ($new_strength <= $old_strength) {
             continue;
           }
         }
 
         $value['+'][$phid] = array(
           'data' => array(
             'status' => $status,
           ),
         );
       }
     }
 
     if ($value) {
       $edge_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER;
 
       $xactions[] = id(new DifferentialTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
         ->setMetadataValue('edge:type', $edge_reviewer)
         ->setNewValue($value);
     }
 
     // Require legalpad document signatures.
     $legal_phids = $adapter->getRequiredSignatureDocumentPHIDs();
     if ($legal_phids) {
       // We only require signatures of documents which have not already
       // been signed. In general, this reduces the amount of churn that
       // signature rules cause.
 
       $signatures = id(new LegalpadDocumentSignatureQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withDocumentPHIDs($legal_phids)
         ->withSignerPHIDs(array($object->getAuthorPHID()))
         ->execute();
       $signed_phids = mpull($signatures, 'getDocumentPHID');
       $legal_phids = array_diff($legal_phids, $signed_phids);
 
       // If we still have something to trigger, add the edges.
       if ($legal_phids) {
         $edge_legal = PhabricatorEdgeConfig::TYPE_OBJECT_NEEDS_SIGNATURE;
         $xactions[] = id(new DifferentialTransaction())
           ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
           ->setMetadataValue('edge:type', $edge_legal)
           ->setNewValue(
             array(
               '+' => array_fuse($legal_phids),
             ));
       }
     }
 
     // Save extra email PHIDs for later.
     $email_phids = $adapter->getEmailPHIDsAddedByHerald();
     $this->heraldEmailPHIDs = array_keys($email_phids);
 
     // Apply build plans.
     HarbormasterBuildable::applyBuildPlans(
       $adapter->getDiff()->getPHID(),
       $adapter->getPHID(),
       $adapter->getBuildPlans());
 
     return $xactions;
   }
 
   /**
    * Update the table which links Differential revisions to paths they affect,
    * so Diffusion can efficiently find pending revisions for a given file.
    */
   private function updateAffectedPathTable(
     DifferentialRevision $revision,
     DifferentialDiff $diff) {
 
     $repository = $revision->getRepository();
     if (!$repository) {
       // The repository where the code lives is untracked.
       return;
     }
 
     $path_prefix = null;
 
     $local_root = $diff->getSourceControlPath();
     if ($local_root) {
       // We're in a working copy which supports subdirectory checkouts (e.g.,
       // SVN) so we need to figure out what prefix we should add to each path
       // (e.g., trunk/projects/example/) to get the absolute path from the
       // root of the repository. DVCS systems like Git and Mercurial are not
       // affected.
 
       // Normalize both paths and check if the repository root is a prefix of
       // the local root. If so, throw it away. Note that this correctly handles
       // the case where the remote path is "/".
       $local_root = id(new PhutilURI($local_root))->getPath();
       $local_root = rtrim($local_root, '/');
 
       $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath();
       $repo_root = rtrim($repo_root, '/');
 
       if (!strncmp($repo_root, $local_root, strlen($repo_root))) {
         $path_prefix = substr($local_root, strlen($repo_root));
       }
     }
 
     $changesets = $diff->getChangesets();
     $paths = array();
     foreach ($changesets as $changeset) {
       $paths[] = $path_prefix.'/'.$changeset->getFilename();
     }
 
     // Mark this as also touching all parent paths, so you can see all pending
     // changes to any file within a directory.
     $all_paths = array();
     foreach ($paths as $local) {
       foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) {
         $all_paths[$path] = true;
       }
     }
     $all_paths = array_keys($all_paths);
 
     $path_ids =
       PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
         $all_paths);
 
     $table = new DifferentialAffectedPath();
     $conn_w = $table->establishConnection('w');
 
     $sql = array();
     foreach ($path_ids as $path_id) {
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %d, %d, %d)',
         $repository->getID(),
         $path_id,
         time(),
         $revision->getID());
     }
 
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE revisionID = %d',
       $table->getTableName(),
       $revision->getID());
     foreach (array_chunk($sql, 256) as $chunk) {
       queryfx(
         $conn_w,
         'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q',
         $table->getTableName(),
         implode(', ', $chunk));
     }
   }
 
   /**
    * Update the table connecting revisions to DVCS local hashes, so we can
    * identify revisions by commit/tree hashes.
    */
   private function updateRevisionHashTable(
     DifferentialRevision $revision,
     DifferentialDiff $diff) {
 
     $vcs = $diff->getSourceControlSystem();
     if ($vcs == DifferentialRevisionControlSystem::SVN) {
       // Subversion has no local commit or tree hash information, so we don't
       // have to do anything.
       return;
     }
 
     $property = id(new DifferentialDiffProperty())->loadOneWhere(
       'diffID = %d AND name = %s',
       $diff->getID(),
       'local:commits');
     if (!$property) {
       return;
     }
 
     $hashes = array();
 
     $data = $property->getData();
     switch ($vcs) {
       case DifferentialRevisionControlSystem::GIT:
         foreach ($data as $commit) {
           $hashes[] = array(
             ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
             $commit['commit'],
           );
           $hashes[] = array(
             ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
             $commit['tree'],
           );
         }
         break;
       case DifferentialRevisionControlSystem::MERCURIAL:
         foreach ($data as $commit) {
           $hashes[] = array(
             ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
             $commit['rev'],
           );
         }
         break;
     }
 
     $conn_w = $revision->establishConnection('w');
 
     $sql = array();
     foreach ($hashes as $info) {
       list($type, $hash) = $info;
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %s, %s)',
         $revision->getID(),
         $type,
         $hash);
     }
 
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE revisionID = %d',
       ArcanistDifferentialRevisionHash::TABLE_NAME,
       $revision->getID());
 
     if ($sql) {
       queryfx(
         $conn_w,
         'INSERT INTO %T (revisionID, type, hash) VALUES %Q',
         ArcanistDifferentialRevisionHash::TABLE_NAME,
         implode(', ', $sql));
     }
   }
 
   private function renderAffectedFilesForMail(DifferentialDiff $diff) {
     $changesets = $diff->getChangesets();
 
     $filenames = mpull($changesets, 'getDisplayFilename');
     sort($filenames);
 
     $count = count($filenames);
     $max = 250;
     if ($count > $max) {
       $filenames = array_slice($filenames, 0, $max);
       $filenames[] = pht('(%d more files...)', ($count - $max));
     }
 
     return implode("\n", $filenames);
   }
 
   private function renderPatchForMail(DifferentialDiff $diff) {
     $format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
 
     return id(new DifferentialRawDiffRenderer())
       ->setViewer($this->getActor())
       ->setFormat($format)
       ->setChangesets($diff->getChangesets())
       ->buildPatch();
   }
 
 }
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index aa3e07b4f..7edff793f 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,648 +1,648 @@
 <?php
 
 final class ManiphestTransactionEditor
   extends PhabricatorApplicationTransactionEditor {
 
   private $heraldEmailPHIDs = array();
 
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
     $types[] = PhabricatorTransactions::TYPE_COMMENT;
     $types[] = PhabricatorTransactions::TYPE_EDGE;
     $types[] = ManiphestTransaction::TYPE_PRIORITY;
     $types[] = ManiphestTransaction::TYPE_STATUS;
     $types[] = ManiphestTransaction::TYPE_TITLE;
     $types[] = ManiphestTransaction::TYPE_DESCRIPTION;
     $types[] = ManiphestTransaction::TYPE_OWNER;
     $types[] = ManiphestTransaction::TYPE_CCS;
     $types[] = ManiphestTransaction::TYPE_SUBPRIORITY;
     $types[] = ManiphestTransaction::TYPE_PROJECT_COLUMN;
     $types[] = ManiphestTransaction::TYPE_UNBLOCK;
     $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
     $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
 
     return $types;
   }
 
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_PRIORITY:
         if ($this->getIsNewObject()) {
           return null;
         }
         return (int)$object->getPriority();
       case ManiphestTransaction::TYPE_STATUS:
         if ($this->getIsNewObject()) {
           return null;
         }
         return $object->getStatus();
       case ManiphestTransaction::TYPE_TITLE:
         if ($this->getIsNewObject()) {
           return null;
         }
         return $object->getTitle();
       case ManiphestTransaction::TYPE_DESCRIPTION:
         if ($this->getIsNewObject()) {
           return null;
         }
         return $object->getDescription();
       case ManiphestTransaction::TYPE_OWNER:
         return nonempty($object->getOwnerPHID(), null);
       case ManiphestTransaction::TYPE_CCS:
         return array_values(array_unique($object->getCCPHIDs()));
       case ManiphestTransaction::TYPE_PROJECT_COLUMN:
         // These are pre-populated.
         return $xaction->getOldValue();
       case ManiphestTransaction::TYPE_SUBPRIORITY:
         return $object->getSubpriority();
     }
   }
 
   protected function getCustomTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_PRIORITY:
         return (int)$xaction->getNewValue();
       case ManiphestTransaction::TYPE_CCS:
         return array_values(array_unique($xaction->getNewValue()));
       case ManiphestTransaction::TYPE_OWNER:
         return nonempty($xaction->getNewValue(), null);
       case ManiphestTransaction::TYPE_STATUS:
       case ManiphestTransaction::TYPE_TITLE:
       case ManiphestTransaction::TYPE_DESCRIPTION:
       case ManiphestTransaction::TYPE_SUBPRIORITY:
       case ManiphestTransaction::TYPE_PROJECT_COLUMN:
       case ManiphestTransaction::TYPE_UNBLOCK:
         return $xaction->getNewValue();
     }
   }
 
   protected function transactionHasEffect(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $old = $xaction->getOldValue();
     $new = $xaction->getNewValue();
 
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_CCS:
         sort($old);
         sort($new);
         return ($old !== $new);
       case ManiphestTransaction::TYPE_PROJECT_COLUMN:
         $new_column_phids = $new['columnPHIDs'];
         $old_column_phids = $old['columnPHIDs'];
         sort($new_column_phids);
         sort($old_column_phids);
         return ($old !== $new);
     }
 
     return parent::transactionHasEffect($object, $xaction);
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_PRIORITY:
         return $object->setPriority($xaction->getNewValue());
       case ManiphestTransaction::TYPE_STATUS:
         return $object->setStatus($xaction->getNewValue());
       case ManiphestTransaction::TYPE_TITLE:
         return $object->setTitle($xaction->getNewValue());
       case ManiphestTransaction::TYPE_DESCRIPTION:
         return $object->setDescription($xaction->getNewValue());
       case ManiphestTransaction::TYPE_OWNER:
         $phid = $xaction->getNewValue();
 
         // Update the "ownerOrdering" column to contain the full name of the
         // owner, if the task is assigned.
 
         $handle = null;
         if ($phid) {
           $handle = id(new PhabricatorHandleQuery())
             ->setViewer($this->getActor())
             ->withPHIDs(array($phid))
             ->executeOne();
         }
 
         if ($handle) {
           $object->setOwnerOrdering($handle->getName());
         } else {
           $object->setOwnerOrdering(null);
         }
 
         return $object->setOwnerPHID($phid);
       case ManiphestTransaction::TYPE_CCS:
         return $object->setCCPHIDs($xaction->getNewValue());
       case ManiphestTransaction::TYPE_SUBPRIORITY:
         $data = $xaction->getNewValue();
         $new_sub = $this->getNextSubpriority(
           $data['newPriority'],
           $data['newSubpriorityBase'],
           $data['direction']);
         $object->setSubpriority($new_sub);
         return;
       case ManiphestTransaction::TYPE_PROJECT_COLUMN:
         // these do external (edge) updates
         return;
     }
   }
 
   protected function expandTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $xactions = parent::expandTransaction($object, $xaction);
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_SUBPRIORITY:
         $data = $xaction->getNewValue();
         $new_pri = $data['newPriority'];
         if ($new_pri != $object->getPriority()) {
           $xactions[] = id(new ManiphestTransaction())
             ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
             ->setNewValue($new_pri);
         }
         break;
       default:
         break;
     }
 
     return $xactions;
   }
 
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_PROJECT_COLUMN:
         $board_phid = idx($xaction->getNewValue(), 'projectPHID');
         if (!$board_phid) {
           throw new Exception(
             pht("Expected 'projectPHID' in column transaction."));
         }
 
         $old_phids = idx($xaction->getOldValue(), 'columnPHIDs', array());
         $new_phids = idx($xaction->getNewValue(), 'columnPHIDs', array());
         if (count($new_phids) !== 1) {
           throw new Exception(
             pht("Expected exactly one 'columnPHIDs' in column transaction."));
         }
 
         $columns = id(new PhabricatorProjectColumnQuery())
           ->setViewer($this->requireActor())
           ->withPHIDs($new_phids)
           ->execute();
         $columns = mpull($columns, null, 'getPHID');
 
         $positions = id(new PhabricatorProjectColumnPositionQuery())
           ->setViewer($this->requireActor())
           ->withObjectPHIDs(array($object->getPHID()))
           ->withBoardPHIDs(array($board_phid))
           ->execute();
 
         $before_phid = idx($xaction->getNewValue(), 'beforePHID');
         $after_phid = idx($xaction->getNewValue(), 'afterPHID');
 
         if (!$before_phid && !$after_phid && ($old_phids == $new_phids)) {
           // If we are not moving the object between columns and also not
           // reordering the position, this is a move on some other order
           // (like priority). We can leave the positions untouched and just
           // bail, there's no work to be done.
           return;
         }
 
         // Otherwise, we're either moving between columns or adjusting the
         // object's position in the "natural" ordering, so we do need to update
         // some rows.
 
         // Remove all existing column positions on the board.
 
         foreach ($positions as $position) {
           $position->delete();
         }
 
         // Add the new column positions.
 
         foreach ($new_phids as $phid) {
           $column = idx($columns, $phid);
           if (!$column) {
             throw new Exception(
               pht('No such column "%s" exists!', $phid));
           }
 
           // Load the other object positions in the column. Note that we must
           // skip implicit column creation to avoid generating a new position
           // if the target column is a backlog column.
 
           $other_positions = id(new PhabricatorProjectColumnPositionQuery())
             ->setViewer($this->requireActor())
             ->withColumns(array($column))
             ->withBoardPHIDs(array($board_phid))
             ->setSkipImplicitCreate(true)
             ->execute();
           $other_positions = msort($other_positions, 'getOrderingKey');
 
           // Set up the new position object. We're going to figure out the
           // right sequence number and then persist this object with that
           // sequence number.
           $new_position = id(new PhabricatorProjectColumnPosition())
             ->setBoardPHID($board_phid)
             ->setColumnPHID($column->getPHID())
             ->setObjectPHID($object->getPHID());
 
           $updates = array();
           $sequence = 0;
 
           // If we're just dropping this into the column without any specific
           // position information, put it at the top.
           if (!$before_phid && !$after_phid) {
             $new_position->setSequence($sequence)->save();
             $sequence++;
           }
 
           foreach ($other_positions as $position) {
             $object_phid = $position->getObjectPHID();
 
             // If this is the object we're moving before and we haven't
             // saved yet, insert here.
             if (($before_phid == $object_phid) && !$new_position->getID()) {
               $new_position->setSequence($sequence)->save();
               $sequence++;
             }
 
             // This object goes here in the sequence; we might need to update
             // the row.
             if ($sequence != $position->getSequence()) {
               $updates[$position->getID()] = $sequence;
             }
             $sequence++;
 
             // If this is the object we're moving after and we haven't saved
             // yet, insert here.
             if (($after_phid == $object_phid) && !$new_position->getID()) {
               $new_position->setSequence($sequence)->save();
               $sequence++;
             }
           }
 
           // We should have found a place to put it.
           if (!$new_position->getID()) {
             throw new Exception(
               pht('Unable to find a place to insert object on column!'));
           }
 
           // If we changed other objects' column positions, bulk reorder them.
 
           if ($updates) {
             $position = new PhabricatorProjectColumnPosition();
             $conn_w = $position->establishConnection('w');
 
             $pairs = array();
             foreach ($updates as $id => $sequence) {
               // This is ugly because MySQL gets upset with us if it is
               // configured strictly and we attempt inserts which can't work.
               // We'll never actually do these inserts since they'll always
               // collide (triggering the ON DUPLICATE KEY logic), so we just
               // provide dummy values in order to get there.
 
               $pairs[] = qsprintf(
                 $conn_w,
                 '(%d, %d, "", "", "")',
                 $id,
                 $sequence);
             }
 
             queryfx(
               $conn_w,
               'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID)
                 VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)',
               $position->getTableName(),
               implode(', ', $pairs));
           }
         }
         break;
       default:
         break;
     }
   }
 
   protected function applyFinalEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     // When we change the status of a task, update tasks this tasks blocks
     // with a message to the effect of "alincoln resolved blocking task Txxx."
     $unblock_xaction = null;
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         case ManiphestTransaction::TYPE_STATUS:
           $unblock_xaction = $xaction;
           break;
       }
     }
 
     if ($unblock_xaction !== null) {
       $blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $object->getPHID(),
         PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK);
       if ($blocked_phids) {
         // In theory we could apply these through policies, but that seems a
         // little bit surprising. For now, use the actor's vision.
         $blocked_tasks = id(new ManiphestTaskQuery())
           ->setViewer($this->getActor())
           ->withPHIDs($blocked_phids)
           ->execute();
 
         $old = $unblock_xaction->getOldValue();
         $new = $unblock_xaction->getNewValue();
 
         foreach ($blocked_tasks as $blocked_task) {
           $unblock_xactions = array();
 
           $unblock_xactions[] = id(new ManiphestTransaction())
             ->setTransactionType(ManiphestTransaction::TYPE_UNBLOCK)
             ->setOldValue(array($object->getPHID() => $old))
             ->setNewValue(array($object->getPHID() => $new));
 
           // TODO: We should avoid notifiying users about these indirect
           // changes if they are getting a notification about the current
           // change, so you don't get a pile of extra notifications if you are
           // subscribed to this task.
 
           id(new ManiphestTransactionEditor())
             ->setActor($this->getActor())
             ->setContentSource($this->getContentSource())
             ->setContinueOnNoEffect(true)
             ->setContinueOnMissingFields(true)
             ->applyTransactions($blocked_task, $unblock_xactions);
         }
       }
     }
 
     return $xactions;
   }
 
   protected function shouldSendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $xactions = mfilter($xactions, 'shouldHide', true);
     return $xactions;
   }
 
   protected function getMailSubjectPrefix() {
     return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
   }
 
   protected function getMailThreadID(PhabricatorLiskDAO $object) {
     return 'maniphest-task-'.$object->getPHID();
   }
 
   protected function getMailTo(PhabricatorLiskDAO $object) {
     return array(
       $object->getOwnerPHID(),
-      $this->requireActor()->getPHID(),
+      $this->getActingAsPHID(),
     );
   }
 
   protected function getMailCC(PhabricatorLiskDAO $object) {
     $phids = array();
 
     foreach ($object->getCCPHIDs() as $phid) {
       $phids[] = $phid;
     }
 
     foreach (parent::getMailCC($object) as $phid) {
       $phids[] = $phid;
     }
 
     foreach ($this->heraldEmailPHIDs as $phid) {
       $phids[] = $phid;
     }
 
     return $phids;
   }
 
   protected function buildReplyHandler(PhabricatorLiskDAO $object) {
     return id(new ManiphestReplyHandler())
       ->setMailReceiver($object);
   }
 
   protected function buildMailTemplate(PhabricatorLiskDAO $object) {
     $id = $object->getID();
     $title = $object->getTitle();
 
     return id(new PhabricatorMetaMTAMail())
       ->setSubject("T{$id}: {$title}")
       ->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle());
   }
 
   protected function buildMailBody(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $body = parent::buildMailBody($object, $xactions);
 
     if ($this->getIsNewObject()) {
       $body->addTextSection(
         pht('TASK DESCRIPTION'),
         $object->getDescription());
     }
 
     $body->addTextSection(
       pht('TASK DETAIL'),
       PhabricatorEnv::getProductionURI('/T'.$object->getID()));
 
     return $body;
   }
 
   protected function shouldPublishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $this->shouldSendMail($object, $xactions);
   }
 
   protected function supportsSearch() {
     return true;
   }
 
   protected function shouldApplyHeraldRules(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return true;
   }
 
   protected function buildHeraldAdapter(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     return id(new HeraldManiphestTaskAdapter())
       ->setTask($object);
   }
 
   protected function didApplyHeraldRules(
     PhabricatorLiskDAO $object,
     HeraldAdapter $adapter,
     HeraldTranscript $transcript) {
 
     // TODO: Convert these to transactions. The way Maniphest deals with these
     // transactions is currently unconventional and messy.
 
     $save_again = false;
     $cc_phids = $adapter->getCcPHIDs();
     if ($cc_phids) {
       $existing_cc = $object->getCCPHIDs();
       $new_cc = array_unique(array_merge($cc_phids, $existing_cc));
       $object->setCCPHIDs($new_cc);
       $object->save();
     }
 
     $this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
 
     $xactions = array();
 
     $assign_phid = $adapter->getAssignPHID();
     if ($assign_phid) {
       $xactions[] = id(new ManiphestTransaction())
         ->setTransactionType(ManiphestTransaction::TYPE_OWNER)
         ->setNewValue($assign_phid);
     }
 
     $project_phids = $adapter->getProjectPHIDs();
     if ($project_phids) {
       $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
       $xactions[] = id(new ManiphestTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
         ->setMetadataValue('edge:type', $project_type)
         ->setNewValue(
           array(
             '+' => array_fuse($project_phids),
           ));
     }
 
     return $xactions;
   }
 
   protected function requireCapabilities(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     parent::requireCapabilities($object, $xaction);
 
     $app_capability_map = array(
       ManiphestTransaction::TYPE_PRIORITY =>
         ManiphestEditPriorityCapability::CAPABILITY,
       ManiphestTransaction::TYPE_STATUS =>
         ManiphestEditStatusCapability::CAPABILITY,
       ManiphestTransaction::TYPE_OWNER =>
         ManiphestEditAssignCapability::CAPABILITY,
       PhabricatorTransactions::TYPE_EDIT_POLICY =>
         ManiphestEditPoliciesCapability::CAPABILITY,
       PhabricatorTransactions::TYPE_VIEW_POLICY =>
         ManiphestEditPoliciesCapability::CAPABILITY,
     );
 
 
     $transaction_type = $xaction->getTransactionType();
 
     $app_capability = null;
     if ($transaction_type == PhabricatorTransactions::TYPE_EDGE) {
       switch ($xaction->getMetadataValue('edge:type')) {
         case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
           $app_capability = ManiphestEditProjectsCapability::CAPABILITY;
           break;
       }
     } else {
       $app_capability = idx($app_capability_map, $transaction_type);
     }
 
     if ($app_capability) {
       $app = id(new PhabricatorApplicationQuery())
         ->setViewer($this->getActor())
         ->withClasses(array('PhabricatorManiphestApplication'))
         ->executeOne();
       PhabricatorPolicyFilter::requireCapability(
         $this->getActor(),
         $app,
         $app_capability);
     }
   }
 
   protected function adjustObjectForPolicyChecks(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $copy = parent::adjustObjectForPolicyChecks($object, $xactions);
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         case ManiphestTransaction::TYPE_OWNER:
           $copy->setOwnerPHID($xaction->getNewValue());
           break;
         default:
           continue;
       }
     }
 
     return $copy;
   }
 
   private function getNextSubpriority($pri, $sub, $dir = '>') {
     switch ($dir) {
       case '>':
         $order = 'ASC';
         break;
       case '<':
         $order = 'DESC';
         break;
       default:
         throw new Exception('$dir must be ">" or "<".');
         break;
     }
 
     if ($sub === null) {
       $base = 0;
     } else {
       $base = $sub;
     }
 
     if ($sub === null) {
       $next = id(new ManiphestTask())->loadOneWhere(
         'priority = %d ORDER BY subpriority %Q LIMIT 1',
         $pri,
         $order);
       if ($next) {
         if ($dir == '>') {
           return $next->getSubpriority() - ((double)(2 << 16));
         } else {
           return $next->getSubpriority() + ((double)(2 << 16));
         }
       }
     } else {
       $next = id(new ManiphestTask())->loadOneWhere(
         'priority = %d AND subpriority %Q %f ORDER BY subpriority %Q LIMIT 1',
         $pri,
         $dir,
         $sub,
         $order);
       if ($next) {
         return ($sub + $next->getSubpriority()) / 2;
       }
     }
 
     if ($dir == '>') {
       return $base + (double)(2 << 32);
     } else {
       return $base - (double)(2 << 32);
     }
   }
 
 }
diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
index 4d0399901..637c5ac17 100644
--- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
+++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
@@ -1,498 +1,509 @@
 <?php
 
 abstract class PhabricatorRepositoryCommitMessageParserWorker
   extends PhabricatorRepositoryCommitParserWorker {
 
   final protected function updateCommitData(DiffusionCommitRef $ref) {
     $commit = $this->commit;
     $author = $ref->getAuthor();
     $message = $ref->getMessage();
     $committer = $ref->getCommitter();
     $hashes = $ref->getHashes();
 
     $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
       'commitID = %d',
       $commit->getID());
     if (!$data) {
       $data = new PhabricatorRepositoryCommitData();
     }
     $data->setCommitID($commit->getID());
     $data->setAuthorName((string)$author);
     $data->setCommitDetail(
       'authorPHID',
       $this->resolveUserPHID($commit, $author));
 
     $data->setCommitMessage($message);
 
     if (strlen($committer)) {
       $data->setCommitDetail('committer', $committer);
       $data->setCommitDetail(
         'committerPHID',
         $this->resolveUserPHID($commit, $committer));
     }
 
     $repository = $this->repository;
 
     $author_phid = $data->getCommitDetail('authorPHID');
     $committer_phid = $data->getCommitDetail('committerPHID');
 
     $user = new PhabricatorUser();
     if ($author_phid) {
       $user = $user->loadOneWhere(
         'phid = %s',
         $author_phid);
     }
 
-    $field_values = id(new DiffusionLowLevelCommitFieldsQuery())
-      ->setRepository($repository)
-      ->withCommitRef($ref)
-      ->execute();
-    $revision_id = idx($field_values, 'revisionID');
+    $differential_app = 'PhabricatorDifferentialApplication';
+    $revision_id = null;
+    if (PhabricatorApplication::isClassInstalled($differential_app)) {
+      $field_values = id(new DiffusionLowLevelCommitFieldsQuery())
+        ->setRepository($repository)
+        ->withCommitRef($ref)
+        ->execute();
+      $revision_id = idx($field_values, 'revisionID');
 
-    if (!empty($field_values['reviewedByPHIDs'])) {
-      $data->setCommitDetail(
-        'reviewerPHID',
-        reset($field_values['reviewedByPHIDs']));
-    }
+      if (!empty($field_values['reviewedByPHIDs'])) {
+        $data->setCommitDetail(
+          'reviewerPHID',
+          reset($field_values['reviewedByPHIDs']));
+      }
 
-    $data->setCommitDetail('differential.revisionID', $revision_id);
+      $data->setCommitDetail('differential.revisionID', $revision_id);
+    }
 
     if ($author_phid != $commit->getAuthorPHID()) {
       $commit->setAuthorPHID($author_phid);
     }
 
     $commit->setSummary($data->getSummary());
     $commit->save();
 
+    // When updating related objects, we'll act under an omnipotent user to
+    // ensure we can see them, but take actions as either the committer or
+    // author (if we recognize their accounts) or the Diffusion application
+    // (if we do not).
+
+    $actor = PhabricatorUser::getOmnipotentUser();
+    $acting_as_phid = nonempty(
+      $committer_phid,
+      $author_phid,
+      id(new PhabricatorDiffusionApplication())->getPHID());
+
     $conn_w = id(new DifferentialRevision())->establishConnection('w');
 
     // NOTE: The `differential_commit` table has a unique ID on `commitPHID`,
     // preventing more than one revision from being associated with a commit.
     // Generally this is good and desirable, but with the advent of hash
     // tracking we may end up in a situation where we match several different
     // revisions. We just kind of ignore this and pick one, we might want to
     // revisit this and do something differently. (If we match several revisions
     // someone probably did something very silly, though.)
 
     $revision = null;
     $should_autoclose = $repository->shouldAutocloseCommit($commit, $data);
 
     if ($revision_id) {
-      // TODO: Check if a more restrictive viewer could be set here
       $revision_query = id(new DifferentialRevisionQuery())
         ->withIDs(array($revision_id))
-        ->setViewer(PhabricatorUser::getOmnipotentUser())
+        ->setViewer($actor)
         ->needReviewerStatus(true)
         ->needActiveDiffs(true);
 
       $revision = $revision_query->executeOne();
 
       if ($revision) {
         $commit_drev = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV;
         id(new PhabricatorEdgeEditor())
           ->addEdge($commit->getPHID(), $commit_drev, $revision->getPHID())
           ->save();
 
         queryfx(
           $conn_w,
           'INSERT IGNORE INTO %T (revisionID, commitPHID) VALUES (%d, %s)',
           DifferentialRevision::TABLE_COMMIT,
           $revision->getID(),
           $commit->getPHID());
 
         $status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
         $should_close = ($revision->getStatus() != $status_closed) &&
                         $should_autoclose;
 
         if ($should_close) {
-          $actor_phid = nonempty(
-            $committer_phid,
-            $author_phid,
-            $revision->getAuthorPHID());
-
-          $actor = id(new PhabricatorUser())
-            ->loadOneWhere('phid = %s', $actor_phid);
-
           $commit_name = $repository->formatCommitName(
             $commit->getCommitIdentifier());
 
           $committer_name = $this->loadUserName(
             $committer_phid,
             $data->getCommitDetail('committer'),
             $actor);
 
           $author_name = $this->loadUserName(
             $author_phid,
             $data->getAuthorName(),
             $actor);
 
           if ($committer_name && ($committer_name != $author_name)) {
             $revision_update_comment = pht(
               'Closed by commit %s (authored by %s, committed by %s).',
               $commit_name,
               $author_name,
               $committer_name);
           } else {
             $revision_update_comment = pht(
               'Closed by commit %s (authored by %s).',
               $commit_name,
               $author_name);
           }
 
-          $diff = $this->generateFinalDiff($revision, $actor_phid);
+          $diff = $this->generateFinalDiff($revision, $acting_as_phid);
 
           $vs_diff = $this->loadChangedByCommit($revision, $diff);
           $changed_uri = null;
           if ($vs_diff) {
             $data->setCommitDetail('vsDiff', $vs_diff->getID());
 
             $changed_uri = PhabricatorEnv::getProductionURI(
               '/D'.$revision->getID().
               '?vs='.$vs_diff->getID().
               '&id='.$diff->getID().
               '#toc');
           }
 
           $xactions = array();
 
           $xactions[] = id(new DifferentialTransaction())
             ->setTransactionType(DifferentialTransaction::TYPE_ACTION)
             ->setNewValue(DifferentialAction::ACTION_CLOSE);
 
           $xactions[] = id(new DifferentialTransaction())
             ->setTransactionType(DifferentialTransaction::TYPE_UPDATE)
             ->setIgnoreOnNoEffect(true)
             ->setNewValue($diff->getPHID());
 
           $xactions[] = id(new DifferentialTransaction())
             ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
             ->setIgnoreOnNoEffect(true)
             ->attachComment(
               id(new DifferentialTransactionComment())
                 ->setContent($revision_update_comment));
 
           $content_source = PhabricatorContentSource::newForSource(
             PhabricatorContentSource::SOURCE_DAEMON,
             array());
 
           $editor = id(new DifferentialTransactionEditor())
             ->setActor($actor)
+            ->setActingAsPHID($acting_as_phid)
             ->setContinueOnMissingFields(true)
             ->setContentSource($content_source)
             ->setChangedPriorToCommitURI($changed_uri)
             ->setIsCloseByCommit(true);
 
           try {
             $editor->applyTransactions($revision, $xactions);
           } catch (PhabricatorApplicationTransactionNoEffectException $ex) {
             // NOTE: We've marked transactions other than the CLOSE transaction
             // as ignored when they don't have an effect, so this means that we
             // lost a race to close the revision. That's perfectly fine, we can
             // just continue normally.
           }
         }
       }
     }
 
     if ($should_autoclose) {
-      // TODO: This isn't as general as it could be.
-      if ($user->getPHID()) {
-        $this->closeTasks($user, $repository, $commit, $message);
-      }
+      $this->closeTasks(
+        $actor,
+        $acting_as_phid,
+        $repository,
+        $commit,
+        $message);
     }
 
     $data->save();
 
     $commit->writeImportStatusFlag(
       PhabricatorRepositoryCommit::IMPORTED_MESSAGE);
   }
 
   private function loadUserName($user_phid, $default, PhabricatorUser $actor) {
     if (!$user_phid) {
       return $default;
     }
     $handle = id(new PhabricatorHandleQuery())
       ->setViewer($actor)
       ->withPHIDs(array($user_phid))
       ->executeOne();
 
     return '@'.$handle->getName();
   }
 
   private function generateFinalDiff(
     DifferentialRevision $revision,
     $actor_phid) {
 
     $viewer = PhabricatorUser::getOmnipotentUser();
 
     $drequest = DiffusionRequest::newFromDictionary(array(
       'user' => $viewer,
       'repository' => $this->repository,
     ));
 
     $raw_diff = DiffusionQuery::callConduitWithDiffusionRequest(
       $viewer,
       $drequest,
       'diffusion.rawdiffquery',
       array(
         'commit' => $this->commit->getCommitIdentifier(),
       ));
 
     // TODO: Support adds, deletes and moves under SVN.
     if (strlen($raw_diff)) {
       $changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);
     } else {
       // This is an empty diff, maybe made with `git commit --allow-empty`.
       // NOTE: These diffs have the same tree hash as their ancestors, so
       // they may attach to revisions in an unexpected way. Just let this
       // happen for now, although it might make sense to special case it
       // eventually.
       $changes = array();
     }
 
     $diff = DifferentialDiff::newFromRawChanges($changes)
       ->setRepositoryPHID($this->repository->getPHID())
       ->setAuthorPHID($actor_phid)
       ->setCreationMethod('commit')
       ->setSourceControlSystem($this->repository->getVersionControlSystem())
       ->setLintStatus(DifferentialLintStatus::LINT_SKIP)
       ->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP)
       ->setDateCreated($this->commit->getEpoch())
       ->setDescription(
         'Commit r'.
         $this->repository->getCallsign().
         $this->commit->getCommitIdentifier());
 
     // TODO: This is not correct in SVN where one repository can have multiple
     // Arcanist projects.
     $arcanist_project = id(new PhabricatorRepositoryArcanistProject())
       ->loadOneWhere('repositoryID = %d LIMIT 1', $this->repository->getID());
     if ($arcanist_project) {
       $diff->setArcanistProjectPHID($arcanist_project->getPHID());
     }
 
     $parents = DiffusionQuery::callConduitWithDiffusionRequest(
       $viewer,
       $drequest,
       'diffusion.commitparentsquery',
       array(
         'commit' => $this->commit->getCommitIdentifier(),
       ));
     if ($parents) {
       $diff->setSourceControlBaseRevision(head($parents));
     }
 
     // TODO: Attach binary files.
 
     return $diff->save();
   }
 
   private function loadChangedByCommit(
     DifferentialRevision $revision,
     DifferentialDiff $diff) {
 
     $repository = $this->repository;
 
     $vs_diff = id(new DifferentialDiffQuery())
       ->setViewer(PhabricatorUser::getOmnipotentUser())
       ->withRevisionIDs(array($revision->getID()))
       ->needChangesets(true)
       ->setLimit(1)
       ->executeOne();
     if (!$vs_diff) {
       return null;
     }
 
     if ($vs_diff->getCreationMethod() == 'commit') {
       return null;
     }
 
     $vs_changesets = array();
     foreach ($vs_diff->getChangesets() as $changeset) {
       $path = $changeset->getAbsoluteRepositoryPath($repository, $vs_diff);
       $path = ltrim($path, '/');
       $vs_changesets[$path] = $changeset;
     }
 
     $changesets = array();
     foreach ($diff->getChangesets() as $changeset) {
       $path = $changeset->getAbsoluteRepositoryPath($repository, $diff);
       $path = ltrim($path, '/');
       $changesets[$path] = $changeset;
     }
 
     if (array_fill_keys(array_keys($changesets), true) !=
         array_fill_keys(array_keys($vs_changesets), true)) {
       return $vs_diff;
     }
 
     $file_phids = array();
     foreach ($vs_changesets as $changeset) {
       $metadata = $changeset->getMetadata();
       $file_phid = idx($metadata, 'new:binary-phid');
       if ($file_phid) {
         $file_phids[$file_phid] = $file_phid;
       }
     }
 
     $files = array();
     if ($file_phids) {
       $files = id(new PhabricatorFileQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withPHIDs($file_phids)
         ->execute();
       $files = mpull($files, null, 'getPHID');
     }
 
     foreach ($changesets as $path => $changeset) {
       $vs_changeset = $vs_changesets[$path];
 
       $file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid');
       if ($file_phid) {
         if (!isset($files[$file_phid])) {
           return $vs_diff;
         }
         $drequest = DiffusionRequest::newFromDictionary(array(
           'user' => PhabricatorUser::getOmnipotentUser(),
           'initFromConduit' => false,
           'repository' => $this->repository,
           'commit' => $this->commit->getCommitIdentifier(),
           'path' => $path,
         ));
         $corpus = DiffusionFileContentQuery::newFromDiffusionRequest($drequest)
           ->setViewer(PhabricatorUser::getOmnipotentUser())
           ->loadFileContent()
           ->getCorpus();
         if ($files[$file_phid]->loadFileData() != $corpus) {
           return $vs_diff;
         }
       } else {
         $context = implode("\n", $changeset->makeChangesWithContext());
         $vs_context = implode("\n", $vs_changeset->makeChangesWithContext());
 
         // We couldn't just compare $context and $vs_context because following
         // diffs will be considered different:
         //
         //   -(empty line)
         //   -echo 'test';
         //    (empty line)
         //
         //    (empty line)
         //   -echo "test";
         //   -(empty line)
 
         $hunk = id(new DifferentialHunkModern())->setChanges($context);
         $vs_hunk = id(new DifferentialHunkModern())->setChanges($vs_context);
         if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() ||
             $hunk->makeNewFile() != $vs_hunk->makeNewFile()) {
           return $vs_diff;
         }
       }
     }
 
     return null;
   }
 
   private function resolveUserPHID(
     PhabricatorRepositoryCommit $commit,
     $user_name) {
 
     return id(new DiffusionResolveUserQuery())
       ->withCommit($commit)
       ->withName($user_name)
       ->execute();
   }
 
   private function closeTasks(
     PhabricatorUser $actor,
+    $acting_as,
     PhabricatorRepository $repository,
     PhabricatorRepositoryCommit $commit,
     $message) {
 
     $maniphest = 'PhabricatorManiphestApplication';
     if (!PhabricatorApplication::isClassInstalled($maniphest)) {
       return;
     }
 
     $prefixes = ManiphestTaskStatus::getStatusPrefixMap();
     $suffixes = ManiphestTaskStatus::getStatusSuffixMap();
 
     $matches = id(new ManiphestCustomFieldStatusParser())
       ->parseCorpus($message);
 
     $task_statuses = array();
     foreach ($matches as $match) {
       $prefix = phutil_utf8_strtolower($match['prefix']);
       $suffix = phutil_utf8_strtolower($match['suffix']);
 
       $status = idx($suffixes, $suffix);
       if (!$status) {
         $status = idx($prefixes, $prefix);
       }
 
       foreach ($match['monograms'] as $task_monogram) {
         $task_id = (int)trim($task_monogram, 'tT');
         $task_statuses[$task_id] = $status;
       }
     }
 
     if (!$task_statuses) {
       return;
     }
 
     $tasks = id(new ManiphestTaskQuery())
       ->setViewer($actor)
       ->withIDs(array_keys($task_statuses))
       ->execute();
 
     foreach ($tasks as $task_id => $task) {
       $xactions = array();
 
       $edge_type = ManiphestTaskHasCommitEdgeType::EDGECONST;
       $xactions[] = id(new ManiphestTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
         ->setMetadataValue('edge:type', $edge_type)
         ->setNewValue(
           array(
             '+' => array(
               $commit->getPHID() => $commit->getPHID(),
             ),
           ));
 
       $status = $task_statuses[$task_id];
       if ($status) {
         if ($task->getStatus() != $status) {
           $xactions[] = id(new ManiphestTransaction())
             ->setTransactionType(ManiphestTransaction::TYPE_STATUS)
             ->setNewValue($status);
 
           $commit_name = $repository->formatCommitName(
             $commit->getCommitIdentifier());
 
           $status_message = pht(
             'Closed by commit %s.',
             $commit_name);
 
           $xactions[] = id(new ManiphestTransaction())
             ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
             ->attachComment(
               id(new ManiphestTransactionComment())
                 ->setContent($status_message));
         }
       }
 
       $content_source = PhabricatorContentSource::newForSource(
         PhabricatorContentSource::SOURCE_DAEMON,
         array());
 
       $editor = id(new ManiphestTransactionEditor())
         ->setActor($actor)
+        ->setActingAsPHID($acting_as)
         ->setContinueOnNoEffect(true)
         ->setContinueOnMissingFields(true)
         ->setContentSource($content_source);
 
       $editor->applyTransactions($task, $xactions);
     }
   }
 
 }
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
index 47eb66f5e..3ba62978e 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php
@@ -1,110 +1,123 @@
 <?php
 
 final class PhabricatorApplicationTransactionCommentEditor
   extends PhabricatorEditor {
 
   private $contentSource;
+  private $actingAsPHID;
+
+  public function setActingAsPHID($acting_as_phid) {
+    $this->actingAsPHID = $acting_as_phid;
+    return $this;
+  }
+
+  public function getActingAsPHID() {
+    if ($this->actingAsPHID) {
+      return $this->actingAsPHID;
+    }
+    return $this->getActor()->getPHID();
+  }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
 
   public function getContentSource() {
     return $this->contentSource;
   }
 
   /**
    * Edit a transaction's comment. This method effects the required create,
    * update or delete to set the transaction's comment to the provided comment.
    */
   public function applyEdit(
     PhabricatorApplicationTransaction $xaction,
     PhabricatorApplicationTransactionComment $comment) {
 
     $this->validateEdit($xaction, $comment);
 
     $actor = $this->requireActor();
 
     $comment->setContentSource($this->getContentSource());
-    $comment->setAuthorPHID($actor->getPHID());
+    $comment->setAuthorPHID($this->getActingAsPHID());
 
     // TODO: This needs to be more sophisticated once we have meta-policies.
     $comment->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
-    $comment->setEditPolicy($actor->getPHID());
+    $comment->setEditPolicy($this->getActingAsPHID());
 
     $xaction->openTransaction();
       $xaction->beginReadLocking();
         if ($xaction->getID()) {
           $xaction->reload();
         }
 
         $new_version = $xaction->getCommentVersion() + 1;
 
         $comment->setCommentVersion($new_version);
         $comment->setTransactionPHID($xaction->getPHID());
         $comment->save();
 
         $xaction->setCommentVersion($new_version);
         $xaction->setCommentPHID($comment->getPHID());
         $xaction->setViewPolicy($comment->getViewPolicy());
         $xaction->setEditPolicy($comment->getEditPolicy());
         $xaction->save();
 
       $xaction->endReadLocking();
     $xaction->saveTransaction();
 
     $xaction->attachComment($comment);
 
     // TODO: Emit an event for notifications/feed? Can we handle them
     // generically?
 
     return $this;
   }
 
   /**
    * Validate that the edit is permissible, and the actor has permission to
    * perform it.
    */
   private function validateEdit(
     PhabricatorApplicationTransaction $xaction,
     PhabricatorApplicationTransactionComment $comment) {
 
     if (!$xaction->getPHID()) {
       throw new Exception(
         'Transaction must have a PHID before calling applyEdit()!');
     }
 
     $type_comment = PhabricatorTransactions::TYPE_COMMENT;
     if ($xaction->getTransactionType() == $type_comment) {
       if ($comment->getPHID()) {
         throw new Exception(
           'Transaction comment must not yet have a PHID!');
       }
     }
 
     if (!$this->getContentSource()) {
       throw new Exception(
         'Call setContentSource() before applyEdit()!');
     }
 
     $actor = $this->requireActor();
 
     PhabricatorPolicyFilter::requireCapability(
       $actor,
       $xaction,
       PhabricatorPolicyCapability::CAN_VIEW);
 
     if ($comment->getIsRemoved() && $actor->getIsAdmin()) {
       // NOTE: Administrators can remove comments by any user, and don't need
       // to pass the edit check.
     } else {
       PhabricatorPolicyFilter::requireCapability(
         $actor,
         $xaction,
         PhabricatorPolicyCapability::CAN_EDIT);
     }
   }
 
 
 }
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index 471e0f304..ac6a1d6f9 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1,2403 +1,2413 @@
 <?php
 
 /**
  * @task mail   Sending Mail
  * @task feed   Publishing Feed Stories
  * @task search Search Index
  * @task files  Integration with Files
  */
 abstract class PhabricatorApplicationTransactionEditor
   extends PhabricatorEditor {
 
   private $contentSource;
   private $object;
   private $xactions;
 
   private $isNewObject;
   private $mentionedPHIDs;
   private $continueOnNoEffect;
   private $continueOnMissingFields;
   private $parentMessageID;
   private $heraldAdapter;
   private $heraldTranscript;
   private $subscribers;
 
   private $isPreview;
   private $isHeraldEditor;
   private $isInverseEdgeEditor;
   private $actingAsPHID;
   private $disableEmail;
 
   public function setActingAsPHID($acting_as_phid) {
     $this->actingAsPHID = $acting_as_phid;
     return $this;
   }
 
   public function getActingAsPHID() {
     if ($this->actingAsPHID) {
       return $this->actingAsPHID;
     }
     return $this->getActor()->getPHID();
   }
 
   /**
    * When the editor tries to apply transactions that have no effect, should
    * it raise an exception (default) or drop them and continue?
    *
    * Generally, you will set this flag for edits coming from "Edit" interfaces,
    * and leave it cleared for edits coming from "Comment" interfaces, so the
    * user will get a useful error if they try to submit a comment that does
    * nothing (e.g., empty comment with a status change that has already been
    * performed by another user).
    *
    * @param bool  True to drop transactions without effect and continue.
    * @return this
    */
   public function setContinueOnNoEffect($continue) {
     $this->continueOnNoEffect = $continue;
     return $this;
   }
 
   public function getContinueOnNoEffect() {
     return $this->continueOnNoEffect;
   }
 
 
   /**
    * When the editor tries to apply transactions which don't populate all of
    * an object's required fields, should it raise an exception (default) or
    * drop them and continue?
    *
    * For example, if a user adds a new required custom field (like "Severity")
    * to a task, all existing tasks won't have it populated. When users
    * manually edit existing tasks, it's usually desirable to have them provide
    * a severity. However, other operations (like batch editing just the
    * owner of a task) will fail by default.
    *
    * By setting this flag for edit operations which apply to specific fields
    * (like the priority, batch, and merge editors in Maniphest), these
    * operations can continue to function even if an object is outdated.
    *
    * @param bool  True to continue when transactions don't completely satisfy
    *              all required fields.
    * @return this
    */
   public function setContinueOnMissingFields($continue_on_missing_fields) {
     $this->continueOnMissingFields = $continue_on_missing_fields;
     return $this;
   }
 
   public function getContinueOnMissingFields() {
     return $this->continueOnMissingFields;
   }
 
 
   /**
    * Not strictly necessary, but reply handlers ideally set this value to
    * make email threading work better.
    */
   public function setParentMessageID($parent_message_id) {
     $this->parentMessageID = $parent_message_id;
     return $this;
   }
   public function getParentMessageID() {
     return $this->parentMessageID;
   }
 
   public function getIsNewObject() {
     return $this->isNewObject;
   }
 
   protected function getMentionedPHIDs() {
     return $this->mentionedPHIDs;
   }
 
   public function setIsPreview($is_preview) {
     $this->isPreview = $is_preview;
     return $this;
   }
 
   public function getIsPreview() {
     return $this->isPreview;
   }
 
   public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
     $this->isInverseEdgeEditor = $is_inverse_edge_editor;
     return $this;
   }
 
   public function getIsInverseEdgeEditor() {
     return $this->isInverseEdgeEditor;
   }
 
   public function setIsHeraldEditor($is_herald_editor) {
     $this->isHeraldEditor = $is_herald_editor;
     return $this;
   }
 
   public function getIsHeraldEditor() {
     return $this->isHeraldEditor;
   }
 
   /**
    * Prevent this editor from generating email when applying transactions.
    *
    * @param bool  True to disable email.
    * @return this
    */
   public function setDisableEmail($disable_email) {
     $this->disableEmail = $disable_email;
     return $this;
   }
 
   public function getDisableEmail() {
     return $this->disableEmail;
   }
 
   public function getTransactionTypes() {
     $types = array();
 
     if ($this->object instanceof PhabricatorSubscribableInterface) {
       $types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
     }
 
     if ($this->object instanceof PhabricatorCustomFieldInterface) {
       $types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
     }
 
     if ($this->object instanceof HarbormasterBuildableInterface) {
       $types[] = PhabricatorTransactions::TYPE_BUILDABLE;
     }
 
     if ($this->object instanceof PhabricatorTokenReceiverInterface) {
       $types[] = PhabricatorTransactions::TYPE_TOKEN;
     }
 
     if ($this->object instanceof PhabricatorProjectInterface) {
       $types[] = PhabricatorTransactions::TYPE_EDGE;
     }
 
     return $types;
   }
 
   private function adjustTransactionValues(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     if ($xaction->shouldGenerateOldValue()) {
       $old = $this->getTransactionOldValue($object, $xaction);
       $xaction->setOldValue($old);
     }
 
     $new = $this->getTransactionNewValue($object, $xaction);
     $xaction->setNewValue($new);
   }
 
   private function getTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return array_values($this->subscribers);
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return $object->getViewPolicy();
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return $object->getEditPolicy();
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return $object->getJoinPolicy();
       case PhabricatorTransactions::TYPE_EDGE:
         $edge_type = $xaction->getMetadataValue('edge:type');
         if (!$edge_type) {
           throw new Exception("Edge transaction has no 'edge:type'!");
         }
 
         $old_edges = array();
         if ($object->getPHID()) {
           $edge_src = $object->getPHID();
 
           $old_edges = id(new PhabricatorEdgeQuery())
             ->withSourcePHIDs(array($edge_src))
             ->withEdgeTypes(array($edge_type))
             ->needEdgeData(true)
             ->execute();
 
           $old_edges = $old_edges[$edge_src][$edge_type];
         }
         return $old_edges;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         // NOTE: Custom fields have their old value pre-populated when they are
         // built by PhabricatorCustomFieldList.
         return $xaction->getOldValue();
       case PhabricatorTransactions::TYPE_COMMENT:
         return null;
       default:
         return $this->getCustomTransactionOldValue($object, $xaction);
     }
   }
 
   private function getTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return $this->getPHIDTransactionNewValue($xaction);
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
         return $xaction->getNewValue();
       case PhabricatorTransactions::TYPE_EDGE:
         return $this->getEdgeTransactionNewValue($xaction);
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->getNewValueFromApplicationTransactions($xaction);
       case PhabricatorTransactions::TYPE_COMMENT:
         return null;
       default:
         return $this->getCustomTransactionNewValue($object, $xaction);
     }
   }
 
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     throw new Exception('Capability not supported!');
   }
 
   protected function getCustomTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     throw new Exception('Capability not supported!');
   }
 
   protected function transactionHasEffect(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return $xaction->hasComment();
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->getApplicationTransactionHasEffect($xaction);
       case PhabricatorTransactions::TYPE_EDGE:
         // A straight value comparison here doesn't always get the right
         // result, because newly added edges aren't fully populated. Instead,
         // compare the changes in a more granular way.
         $old = $xaction->getOldValue();
         $new = $xaction->getNewValue();
 
         $old_dst = array_keys($old);
         $new_dst = array_keys($new);
 
         // NOTE: For now, we don't consider edge reordering to be a change.
         // We have very few order-dependent edges and effectively no order
         // oriented UI. This might change in the future.
         sort($old_dst);
         sort($new_dst);
 
         if ($old_dst !== $new_dst) {
           // We've added or removed edges, so this transaction definitely
           // has an effect.
           return true;
         }
 
         // We haven't added or removed edges, but we might have changed
         // edge data.
         foreach ($old as $key => $old_value) {
           $new_value = $new[$key];
           if ($old_value['data'] !== $new_value['data']) {
             return true;
           }
         }
 
         return false;
     }
 
     return ($xaction->getOldValue() !== $xaction->getNewValue());
   }
 
   protected function shouldApplyInitialEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
   protected function applyInitialEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
     throw new PhutilMethodNotImplementedException();
   }
 
   private function applyInternalEffects(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
         return;
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         $object->setViewPolicy($xaction->getNewValue());
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         $object->setEditPolicy($xaction->getNewValue());
         break;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->applyApplicationTransactionInternalEffects($xaction);
     }
 
     return $this->applyCustomInternalTransaction($object, $xaction);
   }
 
   private function applyExternalEffects(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
         return;
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $subeditor = id(new PhabricatorSubscriptionsEditor())
           ->setObject($object)
           ->setActor($this->requireActor());
 
         $old_map = array_fuse($xaction->getOldValue());
         $new_map = array_fuse($xaction->getNewValue());
 
         $subeditor->unsubscribe(
           array_keys(
             array_diff_key($old_map, $new_map)));
 
         $subeditor->subscribeExplicit(
           array_keys(
             array_diff_key($new_map, $old_map)));
 
         $subeditor->save();
 
         // for the rest of these edits, subscribers should include those just
         // added as well as those just removed.
         $subscribers = array_unique(array_merge(
           $this->subscribers,
           $xaction->getOldValue(),
           $xaction->getNewValue()));
         $this->subscribers = $subscribers;
 
         break;
       case PhabricatorTransactions::TYPE_EDGE:
         if ($this->getIsInverseEdgeEditor()) {
           // If we're writing an inverse edge transaction, don't actually
           // do anything. The initiating editor on the other side of the
           // transaction will take care of the edge writes.
           break;
         }
 
         $old = $xaction->getOldValue();
         $new = $xaction->getNewValue();
         $src = $object->getPHID();
         $const = $xaction->getMetadataValue('edge:type');
 
         $type = PhabricatorEdgeType::getByConstant($const);
         if ($type->shouldWriteInverseTransactions()) {
           $this->applyInverseEdgeTransactions(
             $object,
             $xaction,
             $type->getInverseEdgeConstant());
         }
 
         foreach ($new as $dst_phid => $edge) {
           $new[$dst_phid]['src'] = $src;
         }
 
         $editor = new PhabricatorEdgeEditor();
 
         foreach ($old as $dst_phid => $edge) {
           if (!empty($new[$dst_phid])) {
             if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
               continue;
             }
           }
           $editor->removeEdge($src, $const, $dst_phid);
         }
 
         foreach ($new as $dst_phid => $edge) {
           if (!empty($old[$dst_phid])) {
             if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
               continue;
             }
           }
 
           $data = array(
             'data' => $edge['data'],
           );
 
           $editor->addEdge($src, $const, $dst_phid, $data);
         }
 
         $editor->save();
         break;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getCustomFieldForTransaction($object, $xaction);
         return $field->applyApplicationTransactionExternalEffects($xaction);
     }
 
     return $this->applyCustomExternalTransaction($object, $xaction);
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     $type = $xaction->getTransactionType();
     throw new Exception(
       "Transaction type '{$type}' is missing an internal apply ".
       "implementation!");
   }
 
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     $type = $xaction->getTransactionType();
     throw new Exception(
       "Transaction type '{$type}' is missing an external apply ".
       "implementation!");
   }
 
   /**
    * Fill in a transaction's common values, like author and content source.
    */
   protected function populateTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $actor = $this->getActor();
 
     // TODO: This needs to be more sophisticated once we have meta-policies.
     $xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
 
     if ($actor->isOmnipotent()) {
       $xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
     } else {
-      $xaction->setEditPolicy($actor->getPHID());
+      $xaction->setEditPolicy($this->getActingAsPHID());
     }
 
     $xaction->setAuthorPHID($this->getActingAsPHID());
     $xaction->setContentSource($this->getContentSource());
     $xaction->attachViewer($actor);
     $xaction->attachObject($object);
 
     if ($object->getPHID()) {
       $xaction->setObjectPHID($object->getPHID());
     }
 
     return $xaction;
   }
 
 
   protected function applyFinalEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $xactions;
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
 
   public function setContentSourceFromRequest(AphrontRequest $request) {
     return $this->setContentSource(
       PhabricatorContentSource::newFromRequest($request));
   }
 
   public function setContentSourceFromConduitRequest(
     ConduitAPIRequest $request) {
 
     $content_source = PhabricatorContentSource::newForSource(
       PhabricatorContentSource::SOURCE_CONDUIT,
       array());
 
     return $this->setContentSource($content_source);
   }
 
   public function getContentSource() {
     return $this->contentSource;
   }
 
   final public function applyTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $this->object = $object;
     $this->xactions = $xactions;
     $this->isNewObject = ($object->getPHID() === null);
 
     $this->validateEditParameters($object, $xactions);
 
     $actor = $this->requireActor();
 
     // NOTE: Some transaction expansion requires that the edited object be
     // attached.
     foreach ($xactions as $xaction) {
       $xaction->attachObject($object);
       $xaction->attachViewer($actor);
     }
 
     $xactions = $this->expandTransactions($object, $xactions);
     $xactions = $this->expandSupportTransactions($object, $xactions);
     $xactions = $this->combineTransactions($xactions);
 
     foreach ($xactions as $xaction) {
       $xaction = $this->populateTransaction($object, $xaction);
     }
 
     $is_preview = $this->getIsPreview();
     $read_locking = false;
     $transaction_open = false;
 
     if (!$is_preview) {
       $errors = array();
       $type_map = mgroup($xactions, 'getTransactionType');
       foreach ($this->getTransactionTypes() as $type) {
         $type_xactions = idx($type_map, $type, array());
         $errors[] = $this->validateTransaction($object, $type, $type_xactions);
       }
 
       $errors = array_mergev($errors);
 
       $continue_on_missing = $this->getContinueOnMissingFields();
       foreach ($errors as $key => $error) {
         if ($continue_on_missing && $error->getIsMissingFieldError()) {
           unset($errors[$key]);
         }
       }
 
       if ($errors) {
         throw new PhabricatorApplicationTransactionValidationException($errors);
       }
 
       $file_phids = $this->extractFilePHIDs($object, $xactions);
 
       if ($object->getID()) {
         foreach ($xactions as $xaction) {
 
           // If any of the transactions require a read lock, hold one and
           // reload the object. We need to do this fairly early so that the
           // call to `adjustTransactionValues()` (which populates old values)
           // is based on the synchronized state of the object, which may differ
           // from the state when it was originally loaded.
 
           if ($this->shouldReadLock($object, $xaction)) {
             $object->openTransaction();
             $object->beginReadLocking();
             $transaction_open = true;
             $read_locking = true;
             $object->reload();
             break;
           }
         }
       }
 
       if ($this->shouldApplyInitialEffects($object, $xactions)) {
         if (!$transaction_open) {
           $object->openTransaction();
           $transaction_open = true;
         }
       }
     }
 
     if ($this->shouldApplyInitialEffects($object, $xactions)) {
       $this->applyInitialEffects($object, $xactions);
     }
 
     foreach ($xactions as $xaction) {
       $this->adjustTransactionValues($object, $xaction);
     }
 
     $xactions = $this->filterTransactions($object, $xactions);
 
     if (!$xactions) {
       if ($read_locking) {
         $object->endReadLocking();
         $read_locking = false;
       }
       if ($transaction_open) {
         $object->killTransaction();
         $transaction_open = false;
       }
       return array();
     }
 
     // Now that we've merged, filtered, and combined transactions, check for
     // required capabilities.
     foreach ($xactions as $xaction) {
       $this->requireCapabilities($object, $xaction);
     }
 
     $xactions = $this->sortTransactions($xactions);
 
     if ($is_preview) {
       $this->loadHandles($xactions);
       return $xactions;
     }
 
     $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
       ->setActor($actor)
+      ->setActingAsPHID($this->getActingAsPHID())
       ->setContentSource($this->getContentSource());
 
     if (!$transaction_open) {
       $object->openTransaction();
     }
 
       foreach ($xactions as $xaction) {
         $this->applyInternalEffects($object, $xaction);
       }
 
       $object->save();
 
       foreach ($xactions as $xaction) {
         $xaction->setObjectPHID($object->getPHID());
         if ($xaction->getComment()) {
           $xaction->setPHID($xaction->generatePHID());
           $comment_editor->applyEdit($xaction, $xaction->getComment());
         } else {
           $xaction->save();
         }
       }
 
       if ($file_phids) {
         $this->attachFiles($object, $file_phids);
       }
 
       foreach ($xactions as $xaction) {
         $this->applyExternalEffects($object, $xaction);
       }
 
       $xactions = $this->applyFinalEffects($object, $xactions);
 
       if ($read_locking) {
         $object->endReadLocking();
         $read_locking = false;
       }
 
     $object->saveTransaction();
 
     // Now that we've completely applied the core transaction set, try to apply
     // Herald rules. Herald rules are allowed to either take direct actions on
     // the database (like writing flags), or take indirect actions (like saving
     // some targets for CC when we generate mail a little later), or return
     // transactions which we'll apply normally using another Editor.
 
     // First, check if *this* is a sub-editor which is itself applying Herald
     // rules: if it is, stop working and return so we don't descend into
     // madness.
 
     // Otherwise, we're not a Herald editor, so process Herald rules (possibly
     // using a Herald editor to apply resulting transactions) and then send out
     // mail, notifications, and feed updates about everything.
 
     if ($this->getIsHeraldEditor()) {
       // We are the Herald editor, so stop work here and return the updated
       // transactions.
       return $xactions;
     } else if ($this->shouldApplyHeraldRules($object, $xactions)) {
       // We are not the Herald editor, so try to apply Herald rules.
       $herald_xactions = $this->applyHeraldRules($object, $xactions);
 
       if ($herald_xactions) {
         $xscript_id = $this->getHeraldTranscript()->getID();
         foreach ($herald_xactions as $herald_xaction) {
           $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
         }
 
         // NOTE: We're acting as the omnipotent user because rules deal with
         // their own policy issues. We use a synthetic author PHID (the
         // Herald application) as the author of record, so that transactions
         // will render in a reasonable way ("Herald assigned this task ...").
         $herald_actor = PhabricatorUser::getOmnipotentUser();
         $herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
 
         // TODO: It would be nice to give transactions a more specific source
         // which points at the rule which generated them. You can figure this
         // out from transcripts, but it would be cleaner if you didn't have to.
 
         $herald_source = PhabricatorContentSource::newForSource(
           PhabricatorContentSource::SOURCE_HERALD,
           array());
 
         $herald_editor = newv(get_class($this), array())
           ->setContinueOnNoEffect(true)
           ->setContinueOnMissingFields(true)
           ->setParentMessageID($this->getParentMessageID())
           ->setIsHeraldEditor(true)
           ->setActor($herald_actor)
           ->setActingAsPHID($herald_phid)
           ->setContentSource($herald_source);
 
         $herald_xactions = $herald_editor->applyTransactions(
           $object,
           $herald_xactions);
 
         // Merge the new transactions into the transaction list: we want to
         // send email and publish feed stories about them, too.
         $xactions = array_merge($xactions, $herald_xactions);
       }
     }
 
     // Before sending mail or publishing feed stories, reload the object
     // subscribers to pick up changes caused by Herald (or by other side effects
     // in various transaction phases).
     $this->loadSubscribers($object);
 
     $this->loadHandles($xactions);
 
     $mail = null;
     if (!$this->getDisableEmail()) {
       if ($this->shouldSendMail($object, $xactions)) {
         $mail = $this->sendMail($object, $xactions);
       }
     }
 
     if ($this->supportsSearch()) {
       id(new PhabricatorSearchIndexer())
         ->queueDocumentForIndexing($object->getPHID());
     }
 
     if ($this->shouldPublishFeedStory($object, $xactions)) {
       $mailed = array();
       if ($mail) {
         $mailed = $mail->buildRecipientList();
       }
       $this->publishFeedStory(
         $object,
         $xactions,
         $mailed);
     }
 
     $this->didApplyTransactions($xactions);
 
     if ($object instanceof PhabricatorCustomFieldInterface) {
       // Maybe this makes more sense to move into the search index itself? For
       // now I'm putting it here since I think we might end up with things that
       // need it to be up to date once the next page loads, but if we don't go
       // there we we could move it into search once search moves to the daemons.
 
       // It now happens in the search indexer as well, but the search indexer is
       // always daemonized, so the logic above still potentially holds. We could
       // possibly get rid of this. The major motivation for putting it in the
       // indexer was to enable reindexing to work.
 
       $fields = PhabricatorCustomField::getObjectFields(
         $object,
         PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
       $fields->readFieldsFromStorage($object);
       $fields->rebuildIndexes($object);
     }
 
     return $xactions;
   }
 
   protected function didApplyTransactions(array $xactions) {
     // Hook for subclasses.
     return;
   }
 
 
   /**
    * Determine if the editor should hold a read lock on the object while
    * applying a transaction.
    *
    * If the editor does not hold a lock, two editors may read an object at the
    * same time, then apply their changes without any synchronization. For most
    * transactions, this does not matter much. However, it is important for some
    * transactions. For example, if an object has a transaction count on it, both
    * editors may read the object with `count = 23`, then independently update it
    * and save the object with `count = 24` twice. This will produce the wrong
    * state: the object really has 25 transactions, but the count is only 24.
    *
    * Generally, transactions fall into one of four buckets:
    *
    *   - Append operations: Actions like adding a comment to an object purely
    *     add information to its state, and do not depend on the current object
    *     state in any way. These transactions never need to hold locks.
    *   - Overwrite operations: Actions like changing the title or description
    *     of an object replace the current value with a new value, so the end
    *     state is consistent without a lock. We currently do not lock these
    *     transactions, although we may in the future.
    *   - Edge operations: Edge and subscription operations have internal
    *     synchronization which limits the damage race conditions can cause.
    *     We do not currently lock these transactions, although we may in the
    *     future.
    *   - Update operations: Actions like incrementing a count on an object.
    *     These operations generally should use locks, unless it is not
    *     important that the state remain consistent in the presence of races.
    *
    * @param   PhabricatorLiskDAO  Object being updated.
    * @param   PhabricatorApplicationTransaction Transaction being applied.
    * @return  bool                True to synchronize the edit with a lock.
    */
   protected function shouldReadLock(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return false;
   }
 
   private function loadHandles(array $xactions) {
     $phids = array();
     foreach ($xactions as $key => $xaction) {
       $phids[$key] = $xaction->getRequiredHandlePHIDs();
     }
     $handles = array();
     $merged = array_mergev($phids);
     if ($merged) {
       $handles = id(new PhabricatorHandleQuery())
         ->setViewer($this->requireActor())
         ->withPHIDs($merged)
         ->execute();
     }
     foreach ($xactions as $key => $xaction) {
       $xaction->setHandles(array_select_keys($handles, $phids[$key]));
     }
   }
 
   private function loadSubscribers(PhabricatorLiskDAO $object) {
     if ($object->getPHID() &&
         ($object instanceof PhabricatorSubscribableInterface)) {
       $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
         $object->getPHID());
       $this->subscribers = array_fuse($subs);
     } else {
       $this->subscribers = array();
     }
   }
 
   private function validateEditParameters(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     if (!$this->getContentSource()) {
       throw new Exception(
         'Call setContentSource() before applyTransactions()!');
     }
 
     // Do a bunch of sanity checks that the incoming transactions are fresh.
     // They should be unsaved and have only "transactionType" and "newValue"
     // set.
 
     $types = array_fill_keys($this->getTransactionTypes(), true);
 
     assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
     foreach ($xactions as $xaction) {
       if ($xaction->getPHID() || $xaction->getID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have IDs/PHIDs!'));
       }
       if ($xaction->getObjectPHID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have objectPHIDs!'));
       }
       if ($xaction->getAuthorPHID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have authorPHIDs!'));
       }
       if ($xaction->getCommentPHID()) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have '.
             'commentPHIDs!'));
       }
       if ($xaction->getCommentVersion() !== 0) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'You can not apply transactions which already have '.
             'commentVersions!'));
       }
 
       $expect_value = !$xaction->shouldGenerateOldValue();
       $has_value = $xaction->hasOldValue();
 
       if ($expect_value && !$has_value) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'This transaction is supposed to have an oldValue set, but '.
             'it does not!'));
       }
 
       if ($has_value && !$expect_value) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'This transaction should generate its oldValue automatically, '.
             'but has already had one set!'));
       }
 
       $type = $xaction->getTransactionType();
       if (empty($types[$type])) {
         throw new PhabricatorApplicationTransactionStructureException(
           $xaction,
           pht(
             'Transaction has type "%s", but that transaction type is not '.
             'supported by this editor (%s).',
             $type,
             get_class($this)));
       }
     }
   }
 
   protected function requireCapabilities(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     if ($this->getIsNewObject()) {
       return;
     }
 
     $actor = $this->requireActor();
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         PhabricatorPolicyFilter::requireCapability(
           $actor,
           $object,
           PhabricatorPolicyCapability::CAN_VIEW);
         break;
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         PhabricatorPolicyFilter::requireCapability(
           $actor,
           $object,
           PhabricatorPolicyCapability::CAN_EDIT);
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         PhabricatorPolicyFilter::requireCapability(
           $actor,
           $object,
           PhabricatorPolicyCapability::CAN_EDIT);
         break;
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         PhabricatorPolicyFilter::requireCapability(
           $actor,
           $object,
           PhabricatorPolicyCapability::CAN_EDIT);
         break;
     }
   }
 
   private function buildMentionTransaction(
     PhabricatorLiskDAO $object,
     array $xactions,
     array $blocks) {
 
     if (!($object instanceof PhabricatorSubscribableInterface)) {
       return null;
     }
 
     $texts = array_mergev($blocks);
     $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
       $this->getActor(),
       $texts);
 
     $this->mentionedPHIDs = $phids;
 
     if ($object->getPHID()) {
       // Don't try to subscribe already-subscribed mentions: we want to generate
       // a dialog about an action having no effect if the user explicitly adds
       // existing CCs, but not if they merely mention existing subscribers.
       $phids = array_diff($phids, $this->subscribers);
     }
 
     foreach ($phids as $key => $phid) {
       if ($object->isAutomaticallySubscribed($phid)) {
         unset($phids[$key]);
       }
     }
     $phids = array_values($phids);
 
     if (!$phids) {
       return null;
     }
 
     $xaction = newv(get_class(head($xactions)), array());
     $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
     $xaction->setNewValue(array('+' => $phids));
 
     return $xaction;
   }
 
   protected function getRemarkupBlocksFromTransaction(
     PhabricatorApplicationTransaction $transaction) {
     return $transaction->getRemarkupBlocks();
   }
 
   protected function mergeTransactions(
     PhabricatorApplicationTransaction $u,
     PhabricatorApplicationTransaction $v) {
 
     $type = $u->getTransactionType();
 
     switch ($type) {
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return $this->mergePHIDOrEdgeTransactions($u, $v);
       case PhabricatorTransactions::TYPE_EDGE:
         $u_type = $u->getMetadataValue('edge:type');
         $v_type = $v->getMetadataValue('edge:type');
         if ($u_type == $v_type) {
           return $this->mergePHIDOrEdgeTransactions($u, $v);
         }
         return null;
     }
 
     // By default, do not merge the transactions.
     return null;
   }
 
   /**
    * Optionally expand transactions which imply other effects. For example,
    * resigning from a revision in Differential implies removing yourself as
    * a reviewer.
    */
   private function expandTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $results = array();
     foreach ($xactions as $xaction) {
       foreach ($this->expandTransaction($object, $xaction) as $expanded) {
         $results[] = $expanded;
       }
     }
 
     return $results;
   }
 
   protected function expandTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return array($xaction);
   }
 
 
   private function expandSupportTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
     $this->loadSubscribers($object);
 
     $xactions = $this->applyImplicitCC($object, $xactions);
 
     $blocks = array();
     foreach ($xactions as $key => $xaction) {
       $blocks[$key] = $this->getRemarkupBlocksFromTransaction($xaction);
     }
 
     $mention_xaction = $this->buildMentionTransaction(
       $object,
       $xactions,
       $blocks);
     if ($mention_xaction) {
       $xactions[] = $mention_xaction;
     }
 
     // TODO: For now, this is just a placeholder.
     $engine = PhabricatorMarkupEngine::getEngine('extract');
     $engine->setConfig('viewer', $this->requireActor());
 
     $block_xactions = $this->expandRemarkupBlockTransactions(
       $object,
       $xactions,
       $blocks,
       $engine);
 
     foreach ($block_xactions as $xaction) {
       $xactions[] = $xaction;
     }
 
     return $xactions;
   }
 
   private function expandRemarkupBlockTransactions(
     PhabricatorLiskDAO $object,
     array $xactions,
     $blocks,
     PhutilMarkupEngine $engine) {
 
     $block_xactions = $this->expandCustomRemarkupBlockTransactions(
       $object,
       $xactions,
       $blocks,
       $engine);
 
     if ($object instanceof PhabricatorProjectInterface) {
       $phids = array();
       foreach ($blocks as $key => $xaction_blocks) {
         foreach ($xaction_blocks as $block) {
           $engine->markupText($block);
           $phids += $engine->getTextMetadata(
             PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS,
             array());
         }
       }
 
       $project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
       foreach ($phids as $key => $phid) {
         if (phid_get_type($phid) != $project_type) {
           unset($phids[$key]);
         }
       }
 
       if ($phids) {
         $edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
         $block_xactions[] = newv(get_class(head($xactions)), array())
           ->setIgnoreOnNoEffect(true)
           ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
           ->setMetadataValue('edge:type', $edge_type)
           ->setNewValue(array('+' => $phids));
       }
     }
 
     return $block_xactions;
   }
 
   protected function expandCustomRemarkupBlockTransactions(
     PhabricatorLiskDAO $object,
     array $xactions,
     $blocks,
     PhutilMarkupEngine $engine) {
     return array();
   }
 
 
   /**
    * Attempt to combine similar transactions into a smaller number of total
    * transactions. For example, two transactions which edit the title of an
    * object can be merged into a single edit.
    */
   private function combineTransactions(array $xactions) {
     $stray_comments = array();
 
     $result = array();
     $types = array();
     foreach ($xactions as $key => $xaction) {
       $type = $xaction->getTransactionType();
       if (isset($types[$type])) {
         foreach ($types[$type] as $other_key) {
           $merged = $this->mergeTransactions($result[$other_key], $xaction);
           if ($merged) {
             $result[$other_key] = $merged;
 
             if ($xaction->getComment() &&
                 ($xaction->getComment() !== $merged->getComment())) {
               $stray_comments[] = $xaction->getComment();
             }
 
             if ($result[$other_key]->getComment() &&
                 ($result[$other_key]->getComment() !== $merged->getComment())) {
               $stray_comments[] = $result[$other_key]->getComment();
             }
 
             // Move on to the next transaction.
             continue 2;
           }
         }
       }
       $result[$key] = $xaction;
       $types[$type][] = $key;
     }
 
     // If we merged any comments away, restore them.
     foreach ($stray_comments as $comment) {
       $xaction = newv(get_class(head($result)), array());
       $xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
       $xaction->setComment($comment);
       $result[] = $xaction;
     }
 
     return array_values($result);
   }
 
   protected function mergePHIDOrEdgeTransactions(
     PhabricatorApplicationTransaction $u,
     PhabricatorApplicationTransaction $v) {
 
     $result = $u->getNewValue();
     foreach ($v->getNewValue() as $key => $value) {
       if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) {
         if (empty($result[$key])) {
           $result[$key] = $value;
         } else {
           // We're merging two lists of edge adds, sets, or removes. Merge
           // them by merging individual PHIDs within them.
           $merged = $result[$key];
 
           foreach ($value as $dst => $v_spec) {
             if (empty($merged[$dst])) {
               $merged[$dst] = $v_spec;
             } else {
               // Two transactions are trying to perform the same operation on
               // the same edge. Normalize the edge data and then merge it. This
               // allows transactions to specify how data merges execute in a
               // precise way.
 
               $u_spec = $merged[$dst];
 
               if (!is_array($u_spec)) {
                 $u_spec = array('dst' => $u_spec);
               }
               if (!is_array($v_spec)) {
                 $v_spec = array('dst' => $v_spec);
               }
 
               $ux_data = idx($u_spec, 'data', array());
               $vx_data = idx($v_spec, 'data', array());
 
               $merged_data = $this->mergeEdgeData(
                 $u->getMetadataValue('edge:type'),
                 $ux_data,
                 $vx_data);
 
               $u_spec['data'] = $merged_data;
               $merged[$dst] = $u_spec;
             }
           }
 
           $result[$key] = $merged;
         }
       } else {
         $result[$key] = array_merge($value, idx($result, $key, array()));
       }
     }
     $u->setNewValue($result);
 
     // When combining an "ignore" transaction with a normal transaction, make
     // sure we don't propagate the "ignore" flag.
     if (!$v->getIgnoreOnNoEffect()) {
       $u->setIgnoreOnNoEffect(false);
     }
 
     return $u;
   }
 
   protected function mergeEdgeData($type, array $u, array $v) {
     return $v + $u;
   }
 
   protected function getPHIDTransactionNewValue(
     PhabricatorApplicationTransaction $xaction) {
 
     $old = array_fuse($xaction->getOldValue());
 
     $new = $xaction->getNewValue();
     $new_add = idx($new, '+', array());
     unset($new['+']);
     $new_rem = idx($new, '-', array());
     unset($new['-']);
     $new_set = idx($new, '=', null);
     if ($new_set !== null) {
       $new_set = array_fuse($new_set);
     }
     unset($new['=']);
 
     if ($new) {
       throw new Exception(
         "Invalid 'new' value for PHID transaction. Value should contain only ".
         "keys '+' (add PHIDs), '-' (remove PHIDs) and '=' (set PHIDS).");
     }
 
     $result = array();
 
     foreach ($old as $phid) {
       if ($new_set !== null && empty($new_set[$phid])) {
         continue;
       }
       $result[$phid] = $phid;
     }
 
     if ($new_set !== null) {
       foreach ($new_set as $phid) {
         $result[$phid] = $phid;
       }
     }
 
     foreach ($new_add as $phid) {
       $result[$phid] = $phid;
     }
 
     foreach ($new_rem as $phid) {
       unset($result[$phid]);
     }
 
     return array_values($result);
   }
 
   protected function getEdgeTransactionNewValue(
     PhabricatorApplicationTransaction $xaction) {
 
     $new = $xaction->getNewValue();
     $new_add = idx($new, '+', array());
     unset($new['+']);
     $new_rem = idx($new, '-', array());
     unset($new['-']);
     $new_set = idx($new, '=', null);
     unset($new['=']);
 
     if ($new) {
       throw new Exception(
         "Invalid 'new' value for Edge transaction. Value should contain only ".
         "keys '+' (add edges), '-' (remove edges) and '=' (set edges).");
     }
 
     $old = $xaction->getOldValue();
 
     $lists = array($new_set, $new_add, $new_rem);
     foreach ($lists as $list) {
       $this->checkEdgeList($list);
     }
 
     $result = array();
     foreach ($old as $dst_phid => $edge) {
       if ($new_set !== null && empty($new_set[$dst_phid])) {
         continue;
       }
       $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
         $xaction,
         $edge,
         $dst_phid);
     }
 
     if ($new_set !== null) {
       foreach ($new_set as $dst_phid => $edge) {
         $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
           $xaction,
           $edge,
           $dst_phid);
       }
     }
 
     foreach ($new_add as $dst_phid => $edge) {
       $result[$dst_phid] = $this->normalizeEdgeTransactionValue(
         $xaction,
         $edge,
         $dst_phid);
     }
 
     foreach ($new_rem as $dst_phid => $edge) {
       unset($result[$dst_phid]);
     }
 
     return $result;
   }
 
   private function checkEdgeList($list) {
     if (!$list) {
       return;
     }
     foreach ($list as $key => $item) {
       if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
         throw new Exception(
           "Edge transactions must have destination PHIDs as in edge ".
           "lists (found key '{$key}').");
       }
       if (!is_array($item) && $item !== $key) {
         throw new Exception(
           "Edge transactions must have PHIDs or edge specs as values ".
           "(found value '{$item}').");
       }
     }
   }
 
   private function normalizeEdgeTransactionValue(
     PhabricatorApplicationTransaction $xaction,
     $edge,
     $dst_phid) {
 
     if (!is_array($edge)) {
       if ($edge != $dst_phid) {
         throw new Exception(
           pht(
             'Transaction edge data must either be the edge PHID or an edge '.
             'specification dictionary.'));
       }
       $edge = array();
     } else {
       foreach ($edge as $key => $value) {
         switch ($key) {
           case 'src':
           case 'dst':
           case 'type':
           case 'data':
           case 'dateCreated':
           case 'dateModified':
           case 'seq':
           case 'dataID':
             break;
           default:
             throw new Exception(
               pht(
                 'Transaction edge specification contains unexpected key '.
                 '"%s".',
                 $key));
         }
       }
     }
 
     $edge['dst'] = $dst_phid;
 
     $edge_type = $xaction->getMetadataValue('edge:type');
     if (empty($edge['type'])) {
       $edge['type'] = $edge_type;
     } else {
       if ($edge['type'] != $edge_type) {
         $this_type = $edge['type'];
         throw new Exception(
           "Edge transaction includes edge of type '{$this_type}', but ".
           "transaction is of type '{$edge_type}'. Each edge transaction must ".
           "alter edges of only one type.");
       }
     }
 
     if (!isset($edge['data'])) {
       $edge['data'] = array();
     }
 
     return $edge;
   }
 
   protected function sortTransactions(array $xactions) {
     $head = array();
     $tail = array();
 
     // Move bare comments to the end, so the actions precede them.
     foreach ($xactions as $xaction) {
       $type = $xaction->getTransactionType();
       if ($type == PhabricatorTransactions::TYPE_COMMENT) {
         $tail[] = $xaction;
       } else {
         $head[] = $xaction;
       }
     }
 
     return array_values(array_merge($head, $tail));
   }
 
 
   protected function filterTransactions(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $type_comment = PhabricatorTransactions::TYPE_COMMENT;
 
     $no_effect = array();
     $has_comment = false;
     $any_effect = false;
     foreach ($xactions as $key => $xaction) {
       if ($this->transactionHasEffect($object, $xaction)) {
         if ($xaction->getTransactionType() != $type_comment) {
           $any_effect = true;
         }
       } else if ($xaction->getIgnoreOnNoEffect()) {
         unset($xactions[$key]);
       } else {
         $no_effect[$key] = $xaction;
       }
       if ($xaction->hasComment()) {
         $has_comment = true;
       }
     }
 
     if (!$no_effect) {
       return $xactions;
     }
 
     if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
       throw new PhabricatorApplicationTransactionNoEffectException(
         $no_effect,
         $any_effect,
         $has_comment);
     }
 
     if (!$any_effect && !$has_comment) {
       // If we only have empty comment transactions, just drop them all.
       return array();
     }
 
     foreach ($no_effect as $key => $xaction) {
       if ($xaction->getComment()) {
         $xaction->setTransactionType($type_comment);
         $xaction->setOldValue(null);
         $xaction->setNewValue(null);
       } else {
         unset($xactions[$key]);
       }
     }
 
     return $xactions;
   }
 
 
   /**
    * Hook for validating transactions. This callback will be invoked for each
    * available transaction type, even if an edit does not apply any transactions
    * of that type. This allows you to raise exceptions when required fields are
    * missing, by detecting that the object has no field value and there is no
    * transaction which sets one.
    *
    * @param PhabricatorLiskDAO Object being edited.
    * @param string Transaction type to validate.
    * @param list<PhabricatorApplicationTransaction> Transactions of given type,
    *   which may be empty if the edit does not apply any transactions of the
    *   given type.
    * @return list<PhabricatorApplicationTransactionValidationError> List of
    *   validation errors.
    */
   protected function validateTransaction(
     PhabricatorLiskDAO $object,
     $type,
     array $xactions) {
 
     $errors = array();
     switch ($type) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         $errors[] = $this->validatePolicyTransaction(
           $object,
           $xactions,
           $type,
           PhabricatorPolicyCapability::CAN_VIEW);
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         $errors[] = $this->validatePolicyTransaction(
           $object,
           $xactions,
           $type,
           PhabricatorPolicyCapability::CAN_EDIT);
         break;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $groups = array();
         foreach ($xactions as $xaction) {
           $groups[$xaction->getMetadataValue('customfield:key')][] = $xaction;
         }
 
         $field_list = PhabricatorCustomField::getObjectFields(
           $object,
           PhabricatorCustomField::ROLE_EDIT);
         $field_list->setViewer($this->getActor());
 
         $role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
         foreach ($field_list->getFields() as $field) {
           if (!$field->shouldEnableForRole($role_xactions)) {
             continue;
           }
           $errors[] = $field->validateApplicationTransactions(
             $this,
             $type,
             idx($groups, $field->getFieldKey(), array()));
         }
         break;
     }
 
     return array_mergev($errors);
   }
 
   private function validatePolicyTransaction(
     PhabricatorLiskDAO $object,
     array $xactions,
     $transaction_type,
     $capability) {
 
     $actor = $this->requireActor();
     $errors = array();
     // Note $this->xactions is necessary; $xactions is $this->xactions of
     // $transaction_type
     $policy_object = $this->adjustObjectForPolicyChecks(
       $object,
       $this->xactions);
 
     // Make sure the user isn't editing away their ability to $capability this
     // object.
     foreach ($xactions as $xaction) {
       try {
         PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
           $actor,
           $policy_object,
           $capability,
           $xaction->getNewValue());
       } catch (PhabricatorPolicyException $ex) {
         $errors[] = new PhabricatorApplicationTransactionValidationError(
           $transaction_type,
           pht('Invalid'),
           pht(
             'You can not select this %s policy, because you would no longer '.
             'be able to %s the object.',
             $capability,
             $capability),
           $xaction);
       }
     }
 
     if ($this->getIsNewObject()) {
       if (!$xactions) {
         $has_capability = PhabricatorPolicyFilter::hasCapability(
           $actor,
           $policy_object,
           $capability);
         if (!$has_capability) {
           $errors[] = new PhabricatorApplicationTransactionValidationError(
             $transaction_type,
             pht('Invalid'),
             pht('The selected %s policy excludes you. Choose a %s policy '.
                 'which allows you to %s the object.',
             $capability,
             $capability,
             $capability));
         }
       }
     }
 
     return $errors;
   }
 
   protected function adjustObjectForPolicyChecks(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     return clone $object;
   }
 
   /**
    * Check for a missing text field.
    *
    * A text field is missing if the object has no value and there are no
    * transactions which set a value, or if the transactions remove the value.
    * This method is intended to make implementing @{method:validateTransaction}
    * more convenient:
    *
    *   $missing = $this->validateIsEmptyTextField(
    *     $object->getName(),
    *     $xactions);
    *
    * This will return `true` if the net effect of the object and transactions
    * is an empty field.
    *
    * @param wild Current field value.
    * @param list<PhabricatorApplicationTransaction> Transactions editing the
    *          field.
    * @return bool True if the field will be an empty text field after edits.
    */
   protected function validateIsEmptyTextField($field_value, array $xactions) {
     if (strlen($field_value) && empty($xactions)) {
       return false;
     }
 
     if ($xactions && strlen(last($xactions)->getNewValue())) {
       return false;
     }
 
     return true;
   }
 
 
 /* -(  Implicit CCs  )------------------------------------------------------- */
 
 
   /**
    * When a user interacts with an object, we might want to add them to CC.
    */
   final public function applyImplicitCC(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     if (!($object instanceof PhabricatorSubscribableInterface)) {
       // If the object isn't subscribable, we can't CC them.
       return $xactions;
     }
 
-    $actor_phid = $this->requireActor()->getPHID();
+    $actor_phid = $this->getActingAsPHID();
+
+    $type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
+    if (phid_get_type($actor_phid) != $type_user) {
+      // Transactions by application actors like Herald, Harbormaster and
+      // Diffusion should not CC the applications.
+      return $xactions;
+    }
+
     if ($object->isAutomaticallySubscribed($actor_phid)) {
       // If they're auto-subscribed, don't CC them.
       return $xactions;
     }
 
     $should_cc = false;
     foreach ($xactions as $xaction) {
       if ($this->shouldImplyCC($object, $xaction)) {
         $should_cc = true;
         break;
       }
     }
 
     if (!$should_cc) {
       // Only some types of actions imply a CC (like adding a comment).
       return $xactions;
     }
 
     if ($object->getPHID()) {
       if (isset($this->subscribers[$actor_phid])) {
         // If the user is already subscribed, don't implicitly CC them.
         return $xactions;
       }
 
       $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $object->getPHID(),
         PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER);
       $unsub = array_fuse($unsub);
       if (isset($unsub[$actor_phid])) {
         // If the user has previously unsubscribed from this object explicitly,
         // don't implicitly CC them.
         return $xactions;
       }
     }
 
     $xaction = newv(get_class(head($xactions)), array());
     $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
     $xaction->setNewValue(array('+' => array($actor_phid)));
 
     array_unshift($xactions, $xaction);
 
     return $xactions;
   }
 
   protected function shouldImplyCC(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     return $xaction->isCommentTransaction();
   }
 
 
 /* -(  Sending Mail  )------------------------------------------------------- */
 
 
   /**
    * @task mail
    */
   protected function shouldSendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
 
   /**
    * @task mail
    */
   protected function sendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     // Check if any of the transactions are visible. If we don't have any
     // visible transactions, don't send the mail.
 
     $any_visible = false;
     foreach ($xactions as $xaction) {
       if (!$xaction->shouldHideForMail($xactions)) {
         $any_visible = true;
         break;
       }
     }
 
     if (!$any_visible) {
       return;
     }
 
     $email_to = array_filter(array_unique($this->getMailTo($object)));
     $email_cc = array_filter(array_unique($this->getMailCC($object)));
 
     $phids = array_merge($email_to, $email_cc);
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($this->requireActor())
       ->withPHIDs($phids)
       ->execute();
 
     $template = $this->buildMailTemplate($object);
     $body = $this->buildMailBody($object, $xactions);
 
     $mail_tags = $this->getMailTags($object, $xactions);
     $action = $this->getMailAction($object, $xactions);
 
     $reply_handler = $this->buildReplyHandler($object);
     $reply_section = $reply_handler->getReplyHandlerInstructions();
     if ($reply_section !== null) {
       $body->addReplySection($reply_section);
     }
 
     $template
-      ->setFrom($this->requireActor()->getPHID())
+      ->setFrom($this->getActingAsPHID())
       ->setSubjectPrefix($this->getMailSubjectPrefix())
       ->setVarySubjectPrefix('['.$action.']')
       ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
       ->setRelatedPHID($object->getPHID())
       ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
       ->setMailTags($mail_tags)
       ->setIsBulk(true)
       ->setBody($body->render());
 
     foreach ($body->getAttachments() as $attachment) {
       $template->addAttachment($attachment);
     }
 
     $herald_xscript = $this->getHeraldTranscript();
     if ($herald_xscript) {
       $herald_header = $herald_xscript->getXHeraldRulesHeader();
       $herald_header = HeraldTranscript::saveXHeraldRulesHeader(
         $object->getPHID(),
         $herald_header);
     } else {
       $herald_header = HeraldTranscript::loadXHeraldRulesHeader(
         $object->getPHID());
     }
 
     if ($herald_header) {
       $template->addHeader('X-Herald-Rules', $herald_header);
     }
 
     if ($this->getParentMessageID()) {
       $template->setParentMessageID($this->getParentMessageID());
     }
 
     $mails = $reply_handler->multiplexMail(
         $template,
         array_select_keys($handles, $email_to),
         array_select_keys($handles, $email_cc));
 
     foreach ($mails as $mail) {
       $mail->saveAndSend();
     }
 
     $template->addTos($email_to);
     $template->addCCs($email_cc);
 
     return $template;
   }
 
   protected function getMailThreadID(PhabricatorLiskDAO $object) {
     return $object->getPHID();
   }
 
 
   /**
    * @task mail
    */
   protected function getStrongestAction(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return last(msort($xactions, 'getActionStrength'));
   }
 
 
   /**
    * @task mail
    */
   protected function buildReplyHandler(PhabricatorLiskDAO $object) {
     throw new Exception('Capability not supported.');
   }
 
 
   /**
    * @task mail
    */
   protected function getMailSubjectPrefix() {
     throw new Exception('Capability not supported.');
   }
 
 
   /**
    * @task mail
    */
   protected function getMailTags(
     PhabricatorLiskDAO $object,
     array $xactions) {
     $tags = array();
 
     foreach ($xactions as $xaction) {
       $tags[] = $xaction->getMailTags();
     }
 
     return array_mergev($tags);
   }
 
   /**
    * @task mail
    */
   protected function getMailAction(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $this->getStrongestAction($object, $xactions)->getActionName();
   }
 
 
   /**
    * @task mail
    */
   protected function buildMailTemplate(PhabricatorLiskDAO $object) {
     throw new Exception('Capability not supported.');
   }
 
 
   /**
    * @task mail
    */
   protected function getMailTo(PhabricatorLiskDAO $object) {
     throw new Exception('Capability not supported.');
   }
 
 
   /**
    * @task mail
    */
   protected function getMailCC(PhabricatorLiskDAO $object) {
     $phids = array();
     $has_support = false;
 
     if ($object instanceof PhabricatorSubscribableInterface) {
       $phids[] = $this->subscribers;
       $has_support = true;
     }
 
     if ($object instanceof PhabricatorProjectInterface) {
       $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $object->getPHID(),
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
 
       if ($project_phids) {
         $watcher_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER;
 
         $query = id(new PhabricatorEdgeQuery())
           ->withSourcePHIDs($project_phids)
           ->withEdgeTypes(array($watcher_type));
         $query->execute();
 
         $watcher_phids = $query->getDestinationPHIDs();
         if ($watcher_phids) {
           // We need to do a visibility check for all the watchers, as
           // watching a project is not a guarantee that you can see objects
           // associated with it.
           $users = id(new PhabricatorPeopleQuery())
             ->setViewer($this->requireActor())
             ->withPHIDs($watcher_phids)
             ->execute();
 
           $watchers = array();
           foreach ($users as $user) {
             $can_see = PhabricatorPolicyFilter::hasCapability(
               $user,
               $object,
               PhabricatorPolicyCapability::CAN_VIEW);
             if ($can_see) {
               $watchers[] = $user->getPHID();
             }
           }
           $phids[] = $watchers;
         }
       }
 
       $has_support = true;
     }
 
     if (!$has_support) {
       throw new Exception('Capability not supported.');
     }
 
     return array_mergev($phids);
   }
 
 
   /**
    * @task mail
    */
   protected function buildMailBody(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $headers = array();
     $comments = array();
 
     foreach ($xactions as $xaction) {
       if ($xaction->shouldHideForMail($xactions)) {
         continue;
       }
 
       $header = $xaction->getTitleForMail();
       if ($header !== null) {
         $headers[] = $header;
       }
 
       $comment = $xaction->getBodyForMail();
       if ($comment !== null) {
         $comments[] = $comment;
       }
     }
 
     $body = new PhabricatorMetaMTAMailBody();
     $body->addRawSection(implode("\n", $headers));
 
     foreach ($comments as $comment) {
       $body->addRawSection($comment);
     }
 
     if ($object instanceof PhabricatorCustomFieldInterface) {
       $field_list = PhabricatorCustomField::getObjectFields(
         $object,
         PhabricatorCustomField::ROLE_TRANSACTIONMAIL);
       $field_list->setViewer($this->getActor());
       $field_list->readFieldsFromStorage($object);
 
       foreach ($field_list->getFields() as $field) {
         $field->updateTransactionMailBody(
           $body,
           $this,
           $xactions);
       }
     }
 
     return $body;
   }
 
 
 /* -(  Publishing Feed Stories  )-------------------------------------------- */
 
 
   /**
    * @task feed
    */
   protected function shouldPublishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedStoryType() {
     return 'PhabricatorApplicationTransactionFeedStory';
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedRelatedPHIDs(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     return array(
       $object->getPHID(),
-      $this->requireActor()->getPHID(),
+      $this->getActingAsPHID(),
     );
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedNotifyPHIDs(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     return array_unique(array_merge(
       $this->getMailTo($object),
       $this->getMailCC($object)));
   }
 
 
   /**
    * @task feed
    */
   protected function getFeedStoryData(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $xactions = msort($xactions, 'getActionStrength');
     $xactions = array_reverse($xactions);
 
     return array(
       'objectPHID'        => $object->getPHID(),
       'transactionPHIDs'  => mpull($xactions, 'getPHID'),
     );
   }
 
 
   /**
    * @task feed
    */
   protected function publishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions,
     array $mailed_phids) {
 
     $xactions = mfilter($xactions, 'shouldHideForFeed', true);
 
     if (!$xactions) {
       return;
     }
 
     $related_phids = $this->getFeedRelatedPHIDs($object, $xactions);
     $subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions);
 
     $story_type = $this->getFeedStoryType();
     $story_data = $this->getFeedStoryData($object, $xactions);
 
     id(new PhabricatorFeedStoryPublisher())
       ->setStoryType($story_type)
       ->setStoryData($story_data)
       ->setStoryTime(time())
-      ->setStoryAuthorPHID($this->requireActor()->getPHID())
+      ->setStoryAuthorPHID($this->getActingAsPHID())
       ->setRelatedPHIDs($related_phids)
       ->setPrimaryObjectPHID($object->getPHID())
       ->setSubscribedPHIDs($subscribed_phids)
       ->setMailRecipientPHIDs($mailed_phids)
       ->publish();
   }
 
 
 /* -(  Search Index  )------------------------------------------------------- */
 
 
   /**
    * @task search
    */
   protected function supportsSearch() {
     return false;
   }
 
 
 /* -(  Herald Integration )-------------------------------------------------- */
 
 
   protected function shouldApplyHeraldRules(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return false;
   }
 
   protected function buildHeraldAdapter(
     PhabricatorLiskDAO $object,
     array $xactions) {
     throw new Exception('No herald adapter specified.');
   }
 
   private function setHeraldAdapter(HeraldAdapter $adapter) {
     $this->heraldAdapter = $adapter;
     return $this;
   }
 
   protected function getHeraldAdapter() {
     return $this->heraldAdapter;
   }
 
   private function setHeraldTranscript(HeraldTranscript $transcript) {
     $this->heraldTranscript = $transcript;
     return $this;
   }
 
   protected function getHeraldTranscript() {
     return $this->heraldTranscript;
   }
 
   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());
   }
 
   protected function didApplyHeraldRules(
     PhabricatorLiskDAO $object,
     HeraldAdapter $adapter,
     HeraldTranscript $transcript) {
     return array();
   }
 
 
 /* -(  Custom Fields  )------------------------------------------------------ */
 
 
   /**
    * @task customfield
    */
   private function getCustomFieldForTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $field_key = $xaction->getMetadataValue('customfield:key');
     if (!$field_key) {
       throw new Exception(
         "Custom field transaction has no 'customfield:key'!");
     }
 
     $field = PhabricatorCustomField::getObjectField(
       $object,
       PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
       $field_key);
 
     if (!$field) {
       throw new Exception(
         "Custom field transaction has invalid 'customfield:key'; field ".
         "'{$field_key}' is disabled or does not exist.");
     }
 
     if (!$field->shouldAppearInApplicationTransactions()) {
       throw new Exception(
         "Custom field transaction '{$field_key}' does not implement ".
         "integration for ApplicationTransactions.");
     }
 
     $field->setViewer($this->getActor());
 
     return $field;
   }
 
 
 /* -(  Files  )-------------------------------------------------------------- */
 
 
   /**
    * Extract the PHIDs of any files which these transactions attach.
    *
    * @task files
    */
   private function extractFilePHIDs(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $blocks = array();
     foreach ($xactions as $xaction) {
       $blocks[] = $this->getRemarkupBlocksFromTransaction($xaction);
     }
     $blocks = array_mergev($blocks);
 
     $phids = array();
     if ($blocks) {
       $phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
         $this->getActor(),
         $blocks);
     }
 
     foreach ($xactions as $xaction) {
       $phids[] = $this->extractFilePHIDsFromCustomTransaction(
         $object,
         $xaction);
     }
 
     $phids = array_unique(array_filter(array_mergev($phids)));
     if (!$phids) {
       return array();
     }
 
     // Only let a user attach files they can actually see, since this would
     // otherwise let you access any file by attaching it to an object you have
     // view permission on.
 
     $files = id(new PhabricatorFileQuery())
       ->setViewer($this->getActor())
       ->withPHIDs($phids)
       ->execute();
 
     return mpull($files, 'getPHID');
   }
 
   /**
    * @task files
    */
   protected function extractFilePHIDsFromCustomTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
     return array();
   }
 
 
   /**
    * @task files
    */
   private function attachFiles(
     PhabricatorLiskDAO $object,
     array $file_phids) {
 
     if (!$file_phids) {
       return;
     }
 
     $editor = new PhabricatorEdgeEditor();
 
     $src = $object->getPHID();
     $type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
     foreach ($file_phids as $dst) {
       $editor->addEdge($src, $type, $dst);
     }
 
     $editor->save();
   }
 
   private function applyInverseEdgeTransactions(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction,
     $inverse_type) {
 
     $old = $xaction->getOldValue();
     $new = $xaction->getNewValue();
 
     $add = array_keys(array_diff_key($new, $old));
     $rem = array_keys(array_diff_key($old, $new));
 
     $add = array_fuse($add);
     $rem = array_fuse($rem);
     $all = $add + $rem;
 
     $nodes = id(new PhabricatorObjectQuery())
       ->setViewer($this->requireActor())
       ->withPHIDs($all)
       ->execute();
 
     foreach ($nodes as $node) {
       if (!($node instanceof PhabricatorApplicationTransactionInterface)) {
         continue;
       }
 
       $editor = $node->getApplicationTransactionEditor();
       $template = $node->getApplicationTransactionTemplate();
       $target = $node->getApplicationTransactionObject();
 
       if (isset($add[$node->getPHID()])) {
         $edge_edit_type = '+';
       } else {
         $edge_edit_type = '-';
       }
 
       $template
         ->setTransactionType($xaction->getTransactionType())
         ->setMetadataValue('edge:type', $inverse_type)
         ->setNewValue(
           array(
             $edge_edit_type => array($object->getPHID() => $object->getPHID()),
           ));
 
       $editor
         ->setContinueOnNoEffect(true)
         ->setContinueOnMissingFields(true)
         ->setParentMessageID($this->getParentMessageID())
         ->setIsInverseEdgeEditor(true)
         ->setActor($this->requireActor())
+        ->setActingAsPHID($this->getActingAsPHID())
         ->setContentSource($this->getContentSource());
 
       $editor->applyTransactions($target, array($template));
     }
   }
 
 }