diff --git a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php
index 7190dc057..c4c9ed8b5 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php
@@ -1,390 +1,390 @@
 <?php
 
 final class PhabricatorRepositoryTransaction
   extends PhabricatorApplicationTransaction {
 
   const TYPE_VCS = 'repo:vcs';
   const TYPE_ACTIVATE     = 'repo:activate';
   const TYPE_NAME         = 'repo:name';
   const TYPE_DESCRIPTION  = 'repo:description';
   const TYPE_ENCODING     = 'repo:encoding';
   const TYPE_DEFAULT_BRANCH = 'repo:default-branch';
   const TYPE_TRACK_ONLY = 'repo:track-only';
   const TYPE_AUTOCLOSE_ONLY = 'repo:autoclose-only';
   const TYPE_SVN_SUBPATH = 'repo:svn-subpath';
   const TYPE_UUID = 'repo:uuid';
   const TYPE_NOTIFY = 'repo:notify';
   const TYPE_AUTOCLOSE = 'repo:autoclose';
   const TYPE_REMOTE_URI = 'repo:remote-uri';
   const TYPE_LOCAL_PATH = 'repo:local-path';
   const TYPE_HOSTING = 'repo:hosting';
   const TYPE_PROTOCOL_HTTP = 'repo:serve-http';
   const TYPE_PROTOCOL_SSH = 'repo:serve-ssh';
   const TYPE_PUSH_POLICY = 'repo:push-policy';
   const TYPE_CREDENTIAL = 'repo:credential';
   const TYPE_DANGEROUS = 'repo:dangerous';
   const TYPE_CLONE_NAME = 'repo:clone-name';
 
   // TODO: Clean up these legacy transaction types.
   const TYPE_SSH_LOGIN = 'repo:ssh-login';
   const TYPE_SSH_KEY = 'repo:ssh-key';
   const TYPE_SSH_KEYFILE = 'repo:ssh-keyfile';
   const TYPE_HTTP_LOGIN = 'repo:http-login';
   const TYPE_HTTP_PASS = 'repo:http-pass';
 
   public function getApplicationName() {
     return 'repository';
   }
 
   public function getApplicationTransactionType() {
     return PhabricatorRepositoryPHIDTypeRepository::TYPECONST;
   }
 
   public function getApplicationTransactionCommentObject() {
     return null;
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = parent::getRequiredHandlePHIDs();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_PUSH_POLICY:
         $phids[] = $old;
         $phids[] = $new;
         break;
     }
 
     return $phids;
   }
 
   public function shouldHide() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_REMOTE_URI:
       case self::TYPE_SSH_LOGIN:
       case self::TYPE_SSH_KEY:
       case self::TYPE_SSH_KEYFILE:
       case self::TYPE_HTTP_LOGIN:
       case self::TYPE_HTTP_PASS:
         // Hide null vs empty string changes.
         return (!strlen($old) && !strlen($new));
       case self::TYPE_LOCAL_PATH:
       case self::TYPE_NAME:
         // Hide these on create, they aren't interesting and we have an
         // explicit "create" transaction.
         if (!strlen($old)) {
           return true;
         }
         break;
     }
 
     return parent::shouldHide();
   }
 
   public function getIcon() {
     switch ($this->getTransactionType()) {
       case self::TYPE_VCS:
         return 'fa-plus';
     }
     return parent::getIcon();
   }
 
   public function getColor() {
     switch ($this->getTransactionType()) {
       case self::TYPE_VCS:
         return 'green';
     }
     return parent::getIcon();
   }
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_VCS:
         return pht(
           '%s created this repository.',
           $this->renderHandleLink($author_phid));
       case self::TYPE_ACTIVATE:
         if ($new) {
           return pht(
             '%s activated this repository.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s deactivated this repository.',
             $this->renderHandleLink($author_phid));
         }
       case self::TYPE_NAME:
         return pht(
           '%s renamed this repository from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $old,
           $new);
       case self::TYPE_DESCRIPTION:
         return pht(
           '%s updated the description of this repository.',
           $this->renderHandleLink($author_phid));
       case self::TYPE_ENCODING:
         if (strlen($old) && !strlen($new)) {
           return pht(
             '%s removed the "%s" encoding configured for this repository.',
             $this->renderHandleLink($author_phid),
             $old);
         } else if (strlen($new) && !strlen($old)) {
           return pht(
             '%s set the encoding for this repository to "%s".',
             $this->renderHandleLink($author_phid),
             $new);
         } else {
           return pht(
             '%s changed the repository encoding from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old,
             $new);
         }
       case self::TYPE_DEFAULT_BRANCH:
         if (!strlen($new)) {
           return pht(
             '%s removed "%s" as the default branch.',
             $this->renderHandleLink($author_phid),
             $old);
         } else if (!strlen($old)) {
           return pht(
             '%s set the default branch to "%s".',
             $this->renderHandleLink($author_phid),
             $new);
         } else {
           return pht(
             '%s changed the default branch from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old,
             $new);
         }
         break;
       case self::TYPE_TRACK_ONLY:
         if (!$new) {
           return pht(
             '%s set this repository to track all branches.',
             $this->renderHandleLink($author_phid));
         } else if (!$old) {
           return pht(
             '%s set this repository to track branches: %s.',
             $this->renderHandleLink($author_phid),
             implode(', ', $new));
         } else {
           return pht(
             '%s changed track branches from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             implode(', ', $old),
             implode(', ', $new));
         }
         break;
       case self::TYPE_AUTOCLOSE_ONLY:
         if (!$new) {
           return pht(
             '%s set this repository to autoclose on all branches.',
             $this->renderHandleLink($author_phid));
         } else if (!$old) {
           return pht(
             '%s set this repository to autoclose on branches: %s.',
             $this->renderHandleLink($author_phid),
             implode(', ', $new));
         } else {
           return pht(
             '%s changed autoclose branches from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             implode(', ', $old),
             implode(', ', $new));
         }
         break;
       case self::TYPE_UUID:
         if (!strlen($new)) {
           return pht(
             '%s removed "%s" as the repository UUID.',
             $this->renderHandleLink($author_phid),
             $old);
         } else if (!strlen($old)) {
           return pht(
             '%s set the repository UUID to "%s".',
             $this->renderHandleLink($author_phid),
             $new);
         } else {
           return pht(
             '%s changed the repository UUID from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old,
             $new);
         }
         break;
       case self::TYPE_SVN_SUBPATH:
         if (!strlen($new)) {
           return pht(
             '%s removed "%s" as the Import Only path.',
             $this->renderHandleLink($author_phid),
             $old);
         } else if (!strlen($old)) {
           return pht(
             '%s set the repository to import only "%s".',
             $this->renderHandleLink($author_phid),
             $new);
         } else {
           return pht(
             '%s changed the import path from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old,
             $new);
         }
         break;
       case self::TYPE_NOTIFY:
         if ($new) {
           return pht(
             '%s enabled notifications and publishing for this repository.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s disabled notifications and publishing for this repository.',
             $this->renderHandleLink($author_phid));
         }
         break;
       case self::TYPE_AUTOCLOSE:
         if ($new) {
           return pht(
             '%s enabled autoclose for this repository.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s disabled autoclose for this repository.',
             $this->renderHandleLink($author_phid));
         }
         break;
       case self::TYPE_REMOTE_URI:
         if (!strlen($old)) {
           return pht(
             '%s set the remote URI for this repository to "%s".',
             $this->renderHandleLink($author_phid),
             $new);
         } else if (!strlen($new)) {
           return pht(
             '%s removed the remote URI for this repository.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s changed the remote URI for this repository from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old,
             $new);
         }
         break;
       case self::TYPE_SSH_LOGIN:
         return pht(
           '%s updated the SSH login for this repository.',
           $this->renderHandleLink($author_phid));
       case self::TYPE_SSH_KEY:
         return pht(
           '%s updated the SSH key for this repository.',
           $this->renderHandleLink($author_phid));
       case self::TYPE_SSH_KEYFILE:
         return pht(
           '%s updated the SSH keyfile for this repository.',
           $this->renderHandleLink($author_phid));
       case self::TYPE_HTTP_LOGIN:
         return pht(
           '%s updated the HTTP login for this repository.',
           $this->renderHandleLink($author_phid));
       case self::TYPE_HTTP_PASS:
         return pht(
           '%s updated the HTTP password for this repository.',
           $this->renderHandleLink($author_phid));
       case self::TYPE_LOCAL_PATH:
         return pht(
           '%s changed the local path from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $old,
           $new);
       case self::TYPE_HOSTING:
         if ($new) {
           return pht(
             '%s changed this repository to be hosted on Phabricator.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s changed this repository to track a remote elsewhere.',
             $this->renderHandleLink($author_phid));
         }
       case self::TYPE_PROTOCOL_HTTP:
         return pht(
           '%s changed the availability of this repository over HTTP from '.
           '"%s" to "%s".',
           $this->renderHandleLink($author_phid),
           PhabricatorRepository::getProtocolAvailabilityName($old),
           PhabricatorRepository::getProtocolAvailabilityName($new));
       case self::TYPE_PROTOCOL_SSH:
         return pht(
           '%s changed the availability of this repository over SSH from '.
           '"%s" to "%s".',
           $this->renderHandleLink($author_phid),
           PhabricatorRepository::getProtocolAvailabilityName($old),
           PhabricatorRepository::getProtocolAvailabilityName($new));
       case self::TYPE_PUSH_POLICY:
         return pht(
           '%s changed the push policy of this repository from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
-          $this->renderPolicyName($old),
-          $this->renderPolicyName($new));
+          $this->renderPolicyName($old, 'old'),
+          $this->renderPolicyName($new, 'new'));
       case self::TYPE_DANGEROUS:
         if ($new) {
           return pht(
             '%s disabled protection against dangerous changes.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s enabled protection against dangerous changes.',
             $this->renderHandleLink($author_phid));
         }
       case self::TYPE_CLONE_NAME:
         if (strlen($old) && !strlen($new)) {
           return pht(
             '%s removed the clone name of this repository.',
             $this->renderHandleLink($author_phid));
         } else if (strlen($new) && !strlen($old)) {
           return pht(
             '%s set the clone name of this repository to "%s".',
             $this->renderHandleLink($author_phid),
             $new);
         } else {
           return pht(
             '%s changed the clone name of this repository from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old,
             $new);
         }
     }
 
     return parent::getTitle();
   }
 
   public function hasChangeDetails() {
     switch ($this->getTransactionType()) {
       case self::TYPE_DESCRIPTION:
         return true;
     }
     return parent::hasChangeDetails();
   }
 
   public function renderChangeDetails(PhabricatorUser $viewer) {
     return $this->renderTextCorpusChangeDetails(
       $viewer,
       $this->getOldValue(),
       $this->getNewValue());
   }
 
 }
diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php
index 6f40dc9c6..889bd8ddf 100644
--- a/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php
+++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php
@@ -1,141 +1,146 @@
 <?php
 
 final class PhabricatorApplicationTransactionValueController
   extends PhabricatorApplicationTransactionController {
 
   private $value;
   private $phid;
 
   public function willProcessRequest(array $data) {
     $this->phid = $data['phid'];
     $this->value = $data['value'];
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $viewer = $request->getUser();
 
     $xaction = id(new PhabricatorObjectQuery())
       ->setViewer($viewer)
       ->withPHIDs(array($this->phid))
       ->executeOne();
     if (!$xaction) {
       return new Aphront404Response();
     }
 
     // For now, this pathway only supports policy transactions
     // to show the details of custom policies. If / when this pathway
     // supports more transaction types, rendering coding should be moved
     // into PhabricatorTransactions e.g. feed rendering code.
+
+    // TODO: This should be some kind of "hey do you support this?" thing on
+    // the transactions themselves.
+
     switch ($xaction->getTransactionType()) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
+      case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY:
         break;
       default:
         return new Aphront404Response();
         break;
     }
 
     if ($this->value == 'old') {
       $value = $xaction->getOldValue();
     } else {
       $value = $xaction->getNewValue();
     }
     $policy = id(new PhabricatorPolicyQuery())
       ->setViewer($viewer)
       ->withPHIDs(array($value))
       ->executeOne();
     if (!$policy) {
       return new Aphront404Response();
     }
     if ($policy->getType() != PhabricatorPolicyType::TYPE_CUSTOM) {
       return new Aphront404Response();
     }
 
     $rule_objects = array();
     foreach ($policy->getCustomRuleClasses() as $class) {
       $rule_objects[$class] = newv($class, array());
     }
     $policy->attachRuleObjects($rule_objects);
     $handle_phids = $this->extractPHIDs($policy, $rule_objects);
     $handles = $this->loadHandles($handle_phids);
 
     $this->requireResource('policy-transaction-detail-css');
     $cancel_uri = $this->guessCancelURI($viewer, $xaction);
     $dialog = id(new AphrontDialogView())
       ->setUser($viewer)
       ->setTitle($policy->getFullName())
       ->setWidth(AphrontDialogView::WIDTH_FORM)
       ->appendChild(
         $this->renderPolicyDetails($policy, $rule_objects))
       ->addCancelButton($cancel_uri, pht('Close'));
 
     return id(new AphrontDialogResponse())->setDialog($dialog);
   }
 
   private function extractPHIDs(
     PhabricatorPolicy $policy,
     array $rule_objects) {
 
     $phids = array();
     foreach ($policy->getRules() as $rule) {
       $rule_object = $rule_objects[$rule['rule']];
       $phids[] =
         $rule_object->getRequiredHandlePHIDsForSummary($rule['value']);
     }
     return array_filter(array_mergev($phids));
   }
 
   private function renderPolicyDetails(
     PhabricatorPolicy $policy,
     array $rule_objects) {
     $details = array();
     $details[] = phutil_tag(
       'p',
       array(
         'class' => 'policy-transaction-detail-intro'
       ),
       pht('These rules are processed in order:'));
 
     foreach ($policy->getRules() as $index => $rule) {
       $rule_object = $rule_objects[$rule['rule']];
       if ($rule['action'] == 'allow') {
         $icon = 'fa-check-circle green';
       } else {
         $icon = 'fa-minus-circle red';
       }
       $icon = id(new PHUIIconView())
         ->setIconFont($icon)
         ->setText(
           ucfirst($rule['action']).' '.$rule_object->getRuleDescription());
       $handle_phids =
         $rule_object->getRequiredHandlePHIDsForSummary($rule['value']);
       if ($handle_phids) {
         $value = $this->renderHandlesForPHIDs($handle_phids, ',');
       } else {
         $value = $rule['value'];
       }
       $details[] = phutil_tag('div',
         array(
           'class' => 'policy-transaction-detail-row'
         ),
         array(
           $icon,
           $value));
     }
 
     $details[] = phutil_tag(
       'p',
       array(
         'class' => 'policy-transaction-detail-end'
       ),
       pht(
         'If no rules match, %s all other users.',
         phutil_tag('b',
         array(),
         $policy->getDefaultAction())));
     return $details;
   }
 
 }
diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
index 0e8236091..b5194c8e8 100644
--- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
@@ -1,936 +1,936 @@
 <?php
 
 abstract class PhabricatorApplicationTransaction
   extends PhabricatorLiskDAO
   implements PhabricatorPolicyInterface {
 
   const TARGET_TEXT = 'text';
   const TARGET_HTML = 'html';
 
   protected $phid;
   protected $objectPHID;
   protected $authorPHID;
   protected $viewPolicy;
   protected $editPolicy;
 
   protected $commentPHID;
   protected $commentVersion = 0;
   protected $transactionType;
   protected $oldValue;
   protected $newValue;
   protected $metadata = array();
 
   protected $contentSource;
 
   private $comment;
   private $commentNotLoaded;
 
   private $handles;
   private $renderingTarget = self::TARGET_HTML;
   private $transactionGroup = array();
   private $viewer = self::ATTACHABLE;
   private $object = self::ATTACHABLE;
   private $oldValueHasBeenSet = false;
 
   private $ignoreOnNoEffect;
 
 
   /**
    * Flag this transaction as a pure side-effect which should be ignored when
    * applying transactions if it has no effect, even if transaction application
    * would normally fail. This both provides users with better error messages
    * and allows transactions to perform optional side effects.
    */
   public function setIgnoreOnNoEffect($ignore) {
     $this->ignoreOnNoEffect = $ignore;
     return $this;
   }
 
   public function getIgnoreOnNoEffect() {
     return $this->ignoreOnNoEffect;
   }
 
   public function shouldGenerateOldValue() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_BUILDABLE:
       case PhabricatorTransactions::TYPE_TOKEN:
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         return false;
     }
     return true;
   }
 
   abstract public function getApplicationTransactionType();
 
   private function getApplicationObjectTypeName() {
     $types = PhabricatorPHIDType::getAllTypes();
 
     $type = idx($types, $this->getApplicationTransactionType());
     if ($type) {
       return $type->getTypeName();
     }
 
     return pht('Object');
   }
 
   public function getApplicationTransactionCommentObject() {
     throw new Exception("Not implemented!");
   }
 
   public function getApplicationTransactionViewObject() {
     return new PhabricatorApplicationTransactionView();
   }
 
   public function getMetadataValue($key, $default = null) {
     return idx($this->metadata, $key, $default);
   }
 
   public function setMetadataValue($key, $value) {
     $this->metadata[$key] = $value;
     return $this;
   }
 
   public function generatePHID() {
     $type = PhabricatorApplicationTransactionPHIDTypeTransaction::TYPECONST;
     $subtype = $this->getApplicationTransactionType();
 
     return PhabricatorPHID::generateNewPHID($type, $subtype);
   }
 
   public function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'oldValue' => self::SERIALIZATION_JSON,
         'newValue' => self::SERIALIZATION_JSON,
         'metadata' => self::SERIALIZATION_JSON,
       ),
     ) + parent::getConfiguration();
   }
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source->serialize();
     return $this;
   }
 
   public function getContentSource() {
     return PhabricatorContentSource::newFromSerialized($this->contentSource);
   }
 
   public function hasComment() {
     return $this->getComment() && strlen($this->getComment()->getContent());
   }
 
   public function getComment() {
     if ($this->commentNotLoaded) {
       throw new Exception("Comment for this transaction was not loaded.");
     }
     return $this->comment;
   }
 
   public function attachComment(
     PhabricatorApplicationTransactionComment $comment) {
     $this->comment = $comment;
     $this->commentNotLoaded = false;
     return $this;
   }
 
   public function setCommentNotLoaded($not_loaded) {
     $this->commentNotLoaded = $not_loaded;
     return $this;
   }
 
   public function attachObject($object) {
     $this->object = $object;
     return $this;
   }
 
   public function getObject() {
     return $this->assertAttached($this->object);
   }
 
   public function getRemarkupBlocks() {
     $blocks = array();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           $custom_blocks = $field->getApplicationTransactionRemarkupBlocks(
             $this);
           foreach ($custom_blocks as $custom_block) {
             $blocks[] = $custom_block;
           }
         }
         break;
     }
 
     if ($this->getComment()) {
       $blocks[] = $this->getComment()->getContent();
     }
 
     return $blocks;
   }
 
   public function setOldValue($value) {
     $this->oldValueHasBeenSet = true;
     $this->writeField('oldValue', $value);
     return $this;
   }
 
   public function hasOldValue() {
     return $this->oldValueHasBeenSet;
   }
 
 
 /* -(  Rendering  )---------------------------------------------------------- */
 
   public function setRenderingTarget($rendering_target) {
     $this->renderingTarget = $rendering_target;
     return $this;
   }
 
   public function getRenderingTarget() {
     return $this->renderingTarget;
   }
 
   public function attachViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->assertAttached($this->viewer);
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = array();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $phids[] = array($this->getAuthorPHID());
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           $phids[] = $field->getApplicationTransactionRequiredHandlePHIDs(
             $this);
         }
         break;
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $phids[] = $old;
         $phids[] = $new;
         break;
       case PhabricatorTransactions::TYPE_EDGE:
         $phids[] = ipull($old, 'dst');
         $phids[] = ipull($new, 'dst');
         break;
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) {
           $phids[] = array($old);
         }
         if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) {
           $phids[] = array($new);
         }
         break;
       case PhabricatorTransactions::TYPE_TOKEN:
         break;
       case PhabricatorTransactions::TYPE_BUILDABLE:
         $phid = $this->getMetadataValue('harbormaster:buildablePHID');
         if ($phid) {
           $phids[] = array($phid);
         }
         break;
     }
 
     return array_mergev($phids);
   }
 
   public function setHandles(array $handles) {
     $this->handles = $handles;
     return $this;
   }
 
   public function getHandle($phid) {
     if (empty($this->handles[$phid])) {
       throw new Exception(
         pht(
           'Transaction ("%s") requires a handle ("%s") that it did not '.
           'load.',
           $this->getPHID(),
           $phid));
     }
     return $this->handles[$phid];
   }
 
   public function getHandleIfExists($phid) {
     return idx($this->handles, $phid);
   }
 
   public function getHandles() {
     if ($this->handles === null) {
       throw new Exception(
         'Transaction requires handles and it did not load them.'
       );
     }
     return $this->handles;
   }
 
   public function renderHandleLink($phid) {
     if ($this->renderingTarget == self::TARGET_HTML) {
       return $this->getHandle($phid)->renderLink();
     } else {
       return $this->getHandle($phid)->getLinkName();
     }
   }
 
   public function renderHandleList(array $phids) {
     $links = array();
     foreach ($phids as $phid) {
       $links[] = $this->renderHandleLink($phid);
     }
     if ($this->renderingTarget == self::TARGET_HTML) {
       return phutil_implode_html(', ', $links);
     } else {
       return implode(', ', $links);
     }
   }
 
   private function renderSubscriberList(array $phids, $change_type) {
     if ($this->getRenderingTarget() == self::TARGET_TEXT) {
       return $this->renderHandleList($phids);
     } else {
       $handles = array_select_keys($this->getHandles(), $phids);
       return id(new SubscriptionListStringBuilder())
         ->setHandles($handles)
         ->setObjectPHID($this->getPHID())
         ->buildTransactionString($change_type);
     }
   }
 
-  private function renderPolicyName($phid, $state = 'old') {
+  protected function renderPolicyName($phid, $state = 'old') {
     $policy = PhabricatorPolicy::newFromPolicyAndHandle(
       $phid,
       $this->getHandleIfExists($phid));
     if ($this->renderingTarget == self::TARGET_HTML) {
       switch ($policy->getType()) {
         case PhabricatorPolicyType::TYPE_CUSTOM:
           $policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/');
           $policy->setWorkflow(true);
           break;
         default:
           break;
       }
       $output = $policy->renderDescription();
     } else {
       $output = hsprintf('%s', $policy->getFullName());
     }
     return $output;
   }
 
   public function getIcon() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return 'fa-comment';
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return 'fa-envelope';
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return 'fa-lock';
       case PhabricatorTransactions::TYPE_EDGE:
         return 'fa-link';
       case PhabricatorTransactions::TYPE_BUILDABLE:
         return 'fa-wrench';
       case PhabricatorTransactions::TYPE_TOKEN:
         if ($this->getNewValue()) {
           return 'fa-thumbs-o-up';
         } else {
           return 'fa-thumbs-o-down';
         }
     }
 
     return 'fa-pencil';
   }
 
   public function getToken() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_TOKEN:
         $old = $this->getOldValue();
         $new = $this->getNewValue();
         if ($new) {
           $icon = substr($new, 10);
         } else {
           $icon = substr($old, 10);
         }
         return array($icon, !$this->getNewValue());
     }
 
     return array(null, null);
   }
 
   public function getColor() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_PASSED:
             return 'green';
           case HarbormasterBuildable::STATUS_FAILED:
             return 'red';
         }
         break;
     }
     return null;
   }
 
   protected function getTransactionCustomField() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $key = $this->getMetadataValue('customfield:key');
         if (!$key) {
           return null;
         }
 
         $field = PhabricatorCustomField::getObjectField(
           $this->getObject(),
           PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
           $key);
         if (!$field) {
           return null;
         }
 
         $field->setViewer($this->getViewer());
         return $field;
     }
 
     return null;
   }
 
   public function shouldHide() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         if ($this->getOldValue() === null) {
           return true;
         } else {
           return false;
         }
         break;
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->shouldHideInApplicationTransactions($this);
         }
     }
 
     return false;
   }
 
   public function shouldHideForMail(array $xactions) {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_TOKEN:
         return true;
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_PASSED:
             // For now, just never send mail when builds pass. We might let
             // you customize this later, but in most cases this is probably
             // completely uninteresting.
             return true;
         }
     }
 
     return $this->shouldHide();
   }
 
   public function shouldHideForFeed() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_TOKEN:
         return true;
     }
 
     return $this->shouldHide();
   }
 
   public function getTitleForMail() {
     return id(clone $this)->setRenderingTarget('text')->getTitle();
   }
 
   public function getBodyForMail() {
     $comment = $this->getComment();
     if ($comment && strlen($comment->getContent())) {
       return $comment->getContent();
     }
     return null;
   }
 
   public function getNoEffectDescription() {
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht('You can not post an empty comment.');
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return pht(
           'This %s already has that view policy.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return pht(
           'This %s already has that edit policy.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht(
           'This %s already has that join policy.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return pht(
           'All users are already subscribed to this %s.',
           $this->getApplicationObjectTypeName());
       case PhabricatorTransactions::TYPE_EDGE:
         return pht('Edges already exist; transaction has no effect.');
     }
 
     return pht('Transaction has no effect.');
   }
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht(
           '%s added a comment.',
           $this->renderHandleLink($author_phid));
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return pht(
           '%s changed the visibility of this %s from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName(),
           $this->renderPolicyName($old, 'old'),
           $this->renderPolicyName($new, 'new'));
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return pht(
           '%s changed the edit policy of this %s from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName(),
           $this->renderPolicyName($old, 'old'),
           $this->renderPolicyName($new, 'new'));
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht(
           '%s changed the join policy of this %s from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName(),
           $this->renderPolicyName($old, 'old'),
           $this->renderPolicyName($new, 'new'));
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
 
         if ($add && $rem) {
           return pht(
             '%s edited subscriber(s), added %d: %s; removed %d: %s.',
             $this->renderHandleLink($author_phid),
             count($add),
             $this->renderSubscriberList($add, 'add'),
             count($rem),
             $this->renderSubscriberList($rem, 'rem'));
         } else if ($add) {
           return pht(
             '%s added %d subscriber(s): %s.',
             $this->renderHandleLink($author_phid),
             count($add),
             $this->renderSubscriberList($add, 'add'));
         } else if ($rem) {
           return pht(
             '%s removed %d subscriber(s): %s.',
             $this->renderHandleLink($author_phid),
             count($rem),
             $this->renderSubscriberList($rem, 'rem'));
         } else {
           // This is used when rendering previews, before the user actually
           // selects any CCs.
           return pht(
             '%s updated subscribers...',
             $this->renderHandleLink($author_phid));
         }
         break;
       case PhabricatorTransactions::TYPE_EDGE:
         $new = ipull($new, 'dst');
         $old = ipull($old, 'dst');
         $add = array_diff($new, $old);
         $rem = array_diff($old, $new);
         $type = $this->getMetadata('edge:type');
         $type = head($type);
 
         if ($add && $rem) {
           $string = PhabricatorEdgeConfig::getEditStringForEdgeType($type);
           return pht(
             $string,
             $this->renderHandleLink($author_phid),
             count($add),
             $this->renderHandleList($add),
             count($rem),
             $this->renderHandleList($rem));
         } else if ($add) {
           $string = PhabricatorEdgeConfig::getAddStringForEdgeType($type);
           return pht(
             $string,
             $this->renderHandleLink($author_phid),
             count($add),
             $this->renderHandleList($add));
         } else if ($rem) {
           $string = PhabricatorEdgeConfig::getRemoveStringForEdgeType($type);
           return pht(
             $string,
             $this->renderHandleLink($author_phid),
             count($rem),
             $this->renderHandleList($rem));
         } else {
           return pht(
             '%s edited edge metadata.',
             $this->renderHandleLink($author_phid));
         }
 
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionTitle($this);
         } else {
           return pht(
             '%s edited a custom field.',
             $this->renderHandleLink($author_phid));
         }
 
       case PhabricatorTransactions::TYPE_TOKEN:
         if ($old && $new) {
           return pht(
             '%s updated a token.',
             $this->renderHandleLink($author_phid));
         } else if ($old) {
           return pht(
             '%s rescinded a token.',
             $this->renderHandleLink($author_phid));
         } else {
           return pht(
             '%s awarded a token.',
             $this->renderHandleLink($author_phid));
         }
 
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_PASSED:
             return pht(
               '%s completed building %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')));
           case HarbormasterBuildable::STATUS_FAILED:
             return pht(
               '%s failed to build %s!',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')));
           default:
             return null;
         }
 
       default:
         return pht(
           '%s edited this %s.',
           $this->renderHandleLink($author_phid),
           $this->getApplicationObjectTypeName());
     }
   }
 
   public function getTitleForFeed(PhabricatorFeedStory $story) {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht(
           '%s added a comment to %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
         return pht(
           '%s changed the visibility for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
         return pht(
           '%s changed the edit policy for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht(
           '%s changed the join policy for %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return pht(
           '%s updated subscribers of %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_EDGE:
         $type = $this->getMetadata('edge:type');
         $type = head($type);
         $string = PhabricatorEdgeConfig::getFeedStringForEdgeType($type);
         return pht(
           $string,
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionTitleForFeed($this, $story);
         } else {
           return pht(
             '%s edited a custom field on %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         }
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_PASSED:
             return pht(
               '%s completed building %s for %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')),
               $this->renderHandleLink($object_phid));
           case HarbormasterBuildable::STATUS_FAILED:
             return pht(
               '%s failed to build %s for %s.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink(
                 $this->getMetadataValue('harbormaster:buildablePHID')),
               $this->renderHandleLink($object_phid));
           default:
             return null;
         }
 
     }
 
     return $this->getTitle();
   }
 
   public function getBodyForFeed(PhabricatorFeedStory $story) {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     $body = null;
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         $text = $this->getComment()->getContent();
         $body = phutil_escape_html_newlines(
           phutil_utf8_shorten($text, 128));
         break;
     }
     return $body;
   }
 
   public function getActionStrength() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return 0.5;
     }
     return 1.0;
   }
 
   public function isCommentTransaction() {
     if ($this->hasComment()) {
       return true;
     }
 
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return true;
     }
 
     return false;
   }
 
   public function getActionName() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_COMMENT:
         return pht('Commented On');
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
         return pht('Changed Policy');
       case PhabricatorTransactions::TYPE_SUBSCRIBERS:
         return pht('Changed Subscribers');
       case PhabricatorTransactions::TYPE_BUILDABLE:
         switch ($this->getNewValue()) {
           case HarbormasterBuildable::STATUS_PASSED:
             return pht('Build Passed');
           case HarbormasterBuildable::STATUS_FAILED:
             return pht('Build Failed');
           default:
             return pht('Build Status');
         }
       default:
         return pht('Updated');
     }
   }
 
   public function getMailTags() {
     return array();
   }
 
   public function hasChangeDetails() {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionHasChangeDetails($this);
         }
         break;
     }
     return false;
   }
 
   public function renderChangeDetails(PhabricatorUser $viewer) {
     switch ($this->getTransactionType()) {
       case PhabricatorTransactions::TYPE_CUSTOMFIELD:
         $field = $this->getTransactionCustomField();
         if ($field) {
           return $field->getApplicationTransactionChangeDetails($this, $viewer);
         }
         break;
     }
 
     return $this->renderTextCorpusChangeDetails();
   }
 
   public function renderTextCorpusChangeDetails(
     PhabricatorUser $viewer,
     $old,
     $new) {
 
     require_celerity_resource('differential-changeset-view-css');
 
     $view = id(new PhabricatorApplicationTransactionTextDiffDetailView())
       ->setUser($viewer)
       ->setOldText($old)
       ->setNewText($new);
 
     return $view->render();
   }
 
   public function attachTransactionGroup(array $group) {
     assert_instances_of($group, 'PhabricatorApplicationTransaction');
     $this->transactionGroup = $group;
     return $this;
   }
 
   public function getTransactionGroup() {
     return $this->transactionGroup;
   }
 
   /**
    * Should this transaction be visually grouped with an existing transaction
    * group?
    *
    * @param list<PhabricatorApplicationTransaction> List of transactions.
    * @return bool True to display in a group with the other transactions.
    */
   public function shouldDisplayGroupWith(array $group) {
     $this_source = null;
     if ($this->getContentSource()) {
       $this_source = $this->getContentSource()->getSource();
     }
 
     foreach ($group as $xaction) {
       // Don't group transactions by different authors.
       if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) {
         return false;
       }
 
       // Don't group transactions for different objects.
       if ($xaction->getObjectPHID() != $this->getObjectPHID()) {
         return false;
       }
 
       // Don't group anything into a group which already has a comment.
       if ($xaction->isCommentTransaction()) {
         return false;
       }
 
       // Don't group transactions from different content sources.
       $other_source = null;
       if ($xaction->getContentSource()) {
         $other_source = $xaction->getContentSource()->getSource();
       }
 
       if ($other_source != $this_source) {
         return false;
       }
 
       // Don't group transactions which happened more than 2 minutes apart.
       $apart = abs($xaction->getDateCreated() - $this->getDateCreated());
       if ($apart > (60 * 2)) {
         return false;
       }
     }
 
     return true;
   }
 
   public function renderExtraInformationLink() {
     $herald_xscript_id = $this->getMetadataValue('herald:transcriptID');
 
     if ($herald_xscript_id) {
       return phutil_tag(
         'a',
         array(
           'href' => '/herald/transcript/'.$herald_xscript_id.'/',
         ),
         pht('View Herald Transcript'));
     }
 
     return null;
   }
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return ($viewer->getPHID() == $this->getAuthorPHID());
   }
 
   public function describeAutomaticCapability($capability) {
     // TODO: (T603) Exact policies are unclear here.
     return null;
   }
 
 
 }