diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
index 0427f6587..3c9199fc8 100644
--- a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
+++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
@@ -1,316 +1,317 @@
 <?php
 
 final class HeraldPreCommitContentAdapter extends HeraldPreCommitAdapter {
 
   private $changesets;
   private $commitRef;
   private $fields;
   private $revision = false;
 
   public function getAdapterContentName() {
     return pht('Commit Hook: Commit Content');
   }
 
   public function getAdapterSortOrder() {
     return 2500;
   }
 
   public function getAdapterContentDescription() {
     return pht(
       "React to commits being pushed to hosted repositories.\n".
       "Hook rules can block changes.");
   }
 
   public function getFields() {
     return array_merge(
       array(
         self::FIELD_BODY,
         self::FIELD_AUTHOR,
         self::FIELD_AUTHOR_RAW,
         self::FIELD_COMMITTER,
         self::FIELD_COMMITTER_RAW,
         self::FIELD_BRANCHES,
         self::FIELD_DIFF_FILE,
         self::FIELD_DIFF_CONTENT,
         self::FIELD_DIFF_ADDED_CONTENT,
         self::FIELD_DIFF_REMOVED_CONTENT,
         self::FIELD_DIFF_ENORMOUS,
         self::FIELD_REPOSITORY,
         self::FIELD_REPOSITORY_PROJECTS,
         self::FIELD_PUSHER,
         self::FIELD_PUSHER_PROJECTS,
+        self::FIELD_PUSHER_IS_COMMITTER,
         self::FIELD_DIFFERENTIAL_REVISION,
         self::FIELD_DIFFERENTIAL_ACCEPTED,
         self::FIELD_DIFFERENTIAL_REVIEWERS,
         self::FIELD_DIFFERENTIAL_CCS,
         self::FIELD_IS_MERGE_COMMIT,
       ),
       parent::getFields());
   }
 
   public function getHeraldName() {
     return pht('Push Log (Content)');
   }
 
   public function getHeraldField($field) {
     $log = $this->getObject();
     switch ($field) {
       case self::FIELD_BODY:
         return $this->getCommitRef()->getMessage();
       case self::FIELD_AUTHOR:
         return $this->getAuthorPHID();
       case self::FIELD_AUTHOR_RAW:
         return $this->getAuthorRaw();
       case self::FIELD_COMMITTER:
         return $this->getCommitterPHID();
       case self::FIELD_COMMITTER_RAW:
         return $this->getCommitterRaw();
       case self::FIELD_BRANCHES:
         return $this->getBranches();
       case self::FIELD_DIFF_FILE:
         return $this->getDiffContent('name');
       case self::FIELD_DIFF_CONTENT:
         return $this->getDiffContent('*');
       case self::FIELD_DIFF_ADDED_CONTENT:
         return $this->getDiffContent('+');
       case self::FIELD_DIFF_REMOVED_CONTENT:
         return $this->getDiffContent('-');
       case self::FIELD_DIFF_ENORMOUS:
         $this->getDiffContent('*');
         return ($this->changesets instanceof Exception);
       case self::FIELD_REPOSITORY:
         return $this->getHookEngine()->getRepository()->getPHID();
       case self::FIELD_REPOSITORY_PROJECTS:
         return $this->getHookEngine()->getRepository()->getProjectPHIDs();
       case self::FIELD_PUSHER:
         return $this->getHookEngine()->getViewer()->getPHID();
       case self::FIELD_PUSHER_PROJECTS:
         return $this->getHookEngine()->loadViewerProjectPHIDsForHerald();
       case self::FIELD_DIFFERENTIAL_REVISION:
         $revision = $this->getRevision();
         if (!$revision) {
           return null;
         }
         return $revision->getPHID();
       case self::FIELD_DIFFERENTIAL_ACCEPTED:
         $revision = $this->getRevision();
         if (!$revision) {
           return null;
         }
         $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
         if ($revision->getStatus() != $status_accepted) {
           return null;
         }
         return $revision->getPHID();
       case self::FIELD_DIFFERENTIAL_REVIEWERS:
         $revision = $this->getRevision();
         if (!$revision) {
           return array();
         }
         return $revision->getReviewers();
       case self::FIELD_DIFFERENTIAL_CCS:
         $revision = $this->getRevision();
         if (!$revision) {
           return array();
         }
         return $revision->getCCPHIDs();
       case self::FIELD_IS_MERGE_COMMIT:
         return $this->getIsMergeCommit();
+      case self::FIELD_PUSHER_IS_COMMITTER:
+        $pusher_phid = $this->getHookEngine()->getViewer()->getPHID();
+        return ($this->getCommitterPHID() == $pusher_phid);
     }
 
     return parent::getHeraldField($field);
   }
 
   private function getDiffContent($type) {
     if ($this->changesets === null) {
       try {
         $this->changesets = $this->getHookEngine()->loadChangesetsForCommit(
           $this->getObject()->getRefNew());
       } catch (Exception $ex) {
         $this->changesets = $ex;
       }
     }
 
     if ($this->changesets instanceof Exception) {
       $ex_class = get_class($this->changesets);
       $ex_message = $this->changesets->getmessage();
       if ($type === 'name') {
         return array("<{$ex_class}: {$ex_message}>");
       } else {
         return array("<{$ex_class}>" => $ex_message);
       }
     }
 
     $result = array();
     if ($type === 'name') {
       foreach ($this->changesets as $change) {
         $result[] = $change->getFilename();
       }
     } else {
       foreach ($this->changesets as $change) {
         $lines = array();
         foreach ($change->getHunks() as $hunk) {
           switch ($type) {
             case '-':
               $lines[] = $hunk->makeOldFile();
               break;
             case '+':
               $lines[] = $hunk->makeNewFile();
               break;
             case '*':
             default:
               $lines[] = $hunk->makeChanges();
               break;
           }
         }
         $result[$change->getFilename()] = implode('', $lines);
       }
     }
 
     return $result;
   }
 
   private function getCommitRef() {
     if ($this->commitRef === null) {
       $this->commitRef = $this->getHookEngine()->loadCommitRefForCommit(
         $this->getObject()->getRefNew());
     }
     return $this->commitRef;
   }
 
   private function getAuthorPHID() {
     $repository = $this->getHookEngine()->getRepository();
     $vcs = $repository->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         $ref = $this->getCommitRef();
         $author = $ref->getAuthor();
         if (!strlen($author)) {
           return null;
         }
         return $this->lookupUser($author);
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         // In Subversion, the pusher is always the author.
         return $this->getHookEngine()->getViewer()->getPHID();
     }
   }
 
   private function getCommitterPHID() {
     $repository = $this->getHookEngine()->getRepository();
     $vcs = $repository->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
-        // Here, if there's no committer, we're going to return the author
-        // instead.
+        // If there's no committer information, we're going to return the
+        // author instead. However, if there's committer information and we
+        // can't resolve it, return `null`.
         $ref = $this->getCommitRef();
         $committer = $ref->getCommitter();
         if (!strlen($committer)) {
           return $this->getAuthorPHID();
         }
-        $phid = $this->lookupUser($committer);
-        if (!$phid) {
-          return $this->getAuthorPHID();
-        }
-        return $phid;
+        return $this->lookupUser($committer);
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         // In Subversion, the pusher is always the committer.
         return $this->getHookEngine()->getViewer()->getPHID();
     }
   }
 
   private function getAuthorRaw() {
     $repository = $this->getHookEngine()->getRepository();
     $vcs = $repository->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         $ref = $this->getCommitRef();
         return $ref->getAuthor();
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         // In Subversion, the pusher is always the author.
         return $this->getHookEngine()->getViewer()->getUsername();
     }
   }
 
   private function getCommitterRaw() {
     $repository = $this->getHookEngine()->getRepository();
     $vcs = $repository->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         // Here, if there's no committer, we're going to return the author
         // instead.
         $ref = $this->getCommitRef();
         $committer = $ref->getCommitter();
         if (strlen($committer)) {
           return $committer;
         }
         return $ref->getAuthor();
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         // In Subversion, the pusher is always the committer.
         return $this->getHookEngine()->getViewer()->getUsername();
     }
   }
 
   private function lookupUser($author) {
     return id(new DiffusionResolveUserQuery())
       ->withName($author)
       ->execute();
   }
 
   private function getCommitFields() {
     if ($this->fields === null) {
       $this->fields = id(new DiffusionLowLevelCommitFieldsQuery())
         ->setRepository($this->getHookEngine()->getRepository())
         ->withCommitRef($this->getCommitRef())
         ->execute();
     }
     return $this->fields;
   }
 
   private function getRevision() {
     if ($this->revision === false) {
       $fields = $this->getCommitFields();
       $revision_id = idx($fields, 'revisionID');
       if (!$revision_id) {
         $this->revision = null;
       } else {
         $this->revision = id(new DifferentialRevisionQuery())
           ->setViewer(PhabricatorUser::getOmnipotentUser())
           ->withIDs(array($revision_id))
           ->needRelationships(true)
           ->executeOne();
       }
     }
 
     return $this->revision;
   }
 
   private function getIsMergeCommit() {
     $repository = $this->getHookEngine()->getRepository();
     $vcs = $repository->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         $parents = id(new DiffusionLowLevelParentsQuery())
           ->setRepository($repository)
           ->withIdentifier($this->getObject()->getRefNew())
           ->execute();
 
         return (count($parents) > 1);
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         // NOTE: For now, we ignore "svn:mergeinfo" at all levels. We might
         // change this some day, but it's not nearly as clear a signal as
         // ancestry is in Git/Mercurial.
         return false;
     }
   }
 
   private function getBranches() {
     return $this->getHookEngine()->loadBranches(
       $this->getObject()->getRefNew());
   }
 
 }
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index 6f4a9cb14..c6551b3f1 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,1085 +1,1088 @@
 <?php
 
 /**
  * @group herald
  */
 abstract class HeraldAdapter {
 
   const FIELD_TITLE                  = 'title';
   const FIELD_BODY                   = 'body';
   const FIELD_AUTHOR                 = 'author';
   const FIELD_ASSIGNEE               = 'assignee';
   const FIELD_REVIEWER               = 'reviewer';
   const FIELD_REVIEWERS              = 'reviewers';
   const FIELD_COMMITTER              = 'committer';
   const FIELD_CC                     = 'cc';
   const FIELD_TAGS                   = 'tags';
   const FIELD_DIFF_FILE              = 'diff-file';
   const FIELD_DIFF_CONTENT           = 'diff-content';
   const FIELD_DIFF_ADDED_CONTENT     = 'diff-added-content';
   const FIELD_DIFF_REMOVED_CONTENT   = 'diff-removed-content';
   const FIELD_DIFF_ENORMOUS          = 'diff-enormous';
   const FIELD_REPOSITORY             = 'repository';
   const FIELD_REPOSITORY_PROJECTS    = 'repository-projects';
   const FIELD_RULE                   = 'rule';
   const FIELD_AFFECTED_PACKAGE       = 'affected-package';
   const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner';
   const FIELD_CONTENT_SOURCE         = 'contentsource';
   const FIELD_ALWAYS                 = 'always';
   const FIELD_AUTHOR_PROJECTS        = 'authorprojects';
   const FIELD_PROJECTS               = 'projects';
   const FIELD_PUSHER                 = 'pusher';
   const FIELD_PUSHER_PROJECTS        = 'pusher-projects';
   const FIELD_DIFFERENTIAL_REVISION  = 'differential-revision';
   const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
   const FIELD_DIFFERENTIAL_CCS       = 'differential-ccs';
   const FIELD_DIFFERENTIAL_ACCEPTED  = 'differential-accepted';
   const FIELD_IS_MERGE_COMMIT        = 'is-merge-commit';
   const FIELD_BRANCHES               = 'branches';
   const FIELD_AUTHOR_RAW             = 'author-raw';
   const FIELD_COMMITTER_RAW          = 'committer-raw';
   const FIELD_IS_NEW_OBJECT          = 'new-object';
   const FIELD_TASK_PRIORITY          = 'taskpriority';
   const FIELD_ARCANIST_PROJECT       = 'arcanist-project';
+  const FIELD_PUSHER_IS_COMMITTER    = 'pusher-is-committer';
 
   const CONDITION_CONTAINS        = 'contains';
   const CONDITION_NOT_CONTAINS    = '!contains';
   const CONDITION_IS              = 'is';
   const CONDITION_IS_NOT          = '!is';
   const CONDITION_IS_ANY          = 'isany';
   const CONDITION_IS_NOT_ANY      = '!isany';
   const CONDITION_INCLUDE_ALL     = 'all';
   const CONDITION_INCLUDE_ANY     = 'any';
   const CONDITION_INCLUDE_NONE    = 'none';
   const CONDITION_IS_ME           = 'me';
   const CONDITION_IS_NOT_ME       = '!me';
   const CONDITION_REGEXP          = 'regexp';
   const CONDITION_RULE            = 'conditions';
   const CONDITION_NOT_RULE        = '!conditions';
   const CONDITION_EXISTS          = 'exists';
   const CONDITION_NOT_EXISTS      = '!exists';
   const CONDITION_UNCONDITIONALLY = 'unconditionally';
   const CONDITION_REGEXP_PAIR     = 'regexp-pair';
   const CONDITION_HAS_BIT         = 'bit';
   const CONDITION_NOT_BIT         = '!bit';
   const CONDITION_IS_TRUE         = 'true';
   const CONDITION_IS_FALSE        = 'false';
 
   const ACTION_ADD_CC       = 'addcc';
   const ACTION_REMOVE_CC    = 'remcc';
   const ACTION_EMAIL        = 'email';
   const ACTION_NOTHING      = 'nothing';
   const ACTION_AUDIT        = 'audit';
   const ACTION_FLAG         = 'flag';
   const ACTION_ASSIGN_TASK  = 'assigntask';
   const ACTION_ADD_PROJECTS = 'addprojects';
   const ACTION_ADD_REVIEWERS = 'addreviewers';
   const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers';
   const ACTION_APPLY_BUILD_PLANS = 'applybuildplans';
   const ACTION_BLOCK = 'block';
 
   const VALUE_TEXT            = 'text';
   const VALUE_NONE            = 'none';
   const VALUE_EMAIL           = 'email';
   const VALUE_USER            = 'user';
   const VALUE_TAG             = 'tag';
   const VALUE_RULE            = 'rule';
   const VALUE_REPOSITORY      = 'repository';
   const VALUE_OWNERS_PACKAGE  = 'package';
   const VALUE_PROJECT         = 'project';
   const VALUE_FLAG_COLOR      = 'flagcolor';
   const VALUE_CONTENT_SOURCE  = 'contentsource';
   const VALUE_USER_OR_PROJECT = 'userorproject';
   const VALUE_BUILD_PLAN      = 'buildplan';
   const VALUE_TASK_PRIORITY   = 'taskpriority';
   const VALUE_ARCANIST_PROJECT = 'arcanistprojects';
 
   private $contentSource;
   private $isNewObject;
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
   public function getContentSource() {
     return $this->contentSource;
   }
 
   public function getIsNewObject() {
     if (is_bool($this->isNewObject)) {
       return $this->isNewObject;
     }
 
     throw new Exception(pht('You must setIsNewObject to a boolean first!'));
   }
   public function setIsNewObject($new) {
     $this->isNewObject = (bool) $new;
     return $this;
   }
 
   abstract public function getPHID();
   abstract public function getHeraldName();
 
   public function getHeraldField($field_name) {
     switch ($field_name) {
       case self::FIELD_RULE:
         return null;
       case self::FIELD_CONTENT_SOURCE:
         return $this->getContentSource()->getSource();
       case self::FIELD_ALWAYS:
         return true;
       case self::FIELD_IS_NEW_OBJECT:
         return $this->getIsNewObject();
       default:
         throw new Exception(
           "Unknown field '{$field_name}'!");
     }
   }
 
   abstract public function applyHeraldEffects(array $effects);
 
   public function isAvailableToUser(PhabricatorUser $viewer) {
     $applications = id(new PhabricatorApplicationQuery())
       ->setViewer($viewer)
       ->withInstalled(true)
       ->withClasses(array($this->getAdapterApplicationClass()))
       ->execute();
 
     return !empty($applications);
   }
 
 
   /**
    * NOTE: You generally should not override this; it exists to support legacy
    * adapters which had hard-coded content types.
    */
   public function getAdapterContentType() {
     return get_class($this);
   }
 
   abstract public function getAdapterContentName();
   abstract public function getAdapterContentDescription();
   abstract public function getAdapterApplicationClass();
   abstract public function getObject();
 
   public function supportsRuleType($rule_type) {
     return false;
   }
 
   public function canTriggerOnObject($object) {
     return false;
   }
 
   public function explainValidTriggerObjects() {
     return pht('This adapter can not trigger on objects.');
   }
 
   public function getTriggerObjectPHIDs() {
     return array($this->getPHID());
   }
 
   public function getAdapterSortKey() {
     return sprintf(
       '%08d%s',
       $this->getAdapterSortOrder(),
       $this->getAdapterContentName());
   }
 
   public function getAdapterSortOrder() {
     return 1000;
   }
 
 
 /* -(  Fields  )------------------------------------------------------------- */
 
 
   public function getFields() {
     return array(
       self::FIELD_ALWAYS,
       self::FIELD_RULE,
     );
   }
 
   public function getFieldNameMap() {
     return array(
       self::FIELD_TITLE => pht('Title'),
       self::FIELD_BODY => pht('Body'),
       self::FIELD_AUTHOR => pht('Author'),
       self::FIELD_ASSIGNEE => pht('Assignee'),
       self::FIELD_COMMITTER => pht('Committer'),
       self::FIELD_REVIEWER => pht('Reviewer'),
       self::FIELD_REVIEWERS => pht('Reviewers'),
       self::FIELD_CC => pht('CCs'),
       self::FIELD_TAGS => pht('Tags'),
       self::FIELD_DIFF_FILE => pht('Any changed filename'),
       self::FIELD_DIFF_CONTENT => pht('Any changed file content'),
       self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'),
       self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'),
       self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'),
       self::FIELD_REPOSITORY => pht('Repository'),
       self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'),
       self::FIELD_RULE => pht('Another Herald rule'),
       self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'),
       self::FIELD_AFFECTED_PACKAGE_OWNER =>
         pht("Any affected package's owner"),
       self::FIELD_CONTENT_SOURCE => pht('Content Source'),
       self::FIELD_ALWAYS => pht('Always'),
       self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"),
       self::FIELD_PROJECTS => pht("Projects"),
       self::FIELD_PUSHER => pht('Pusher'),
       self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"),
       self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'),
       self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'),
       self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'),
       self::FIELD_DIFFERENTIAL_ACCEPTED
         => pht('Accepted Differential revision'),
       self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'),
       self::FIELD_BRANCHES => pht('Commit\'s branches'),
       self::FIELD_AUTHOR_RAW => pht('Raw author name'),
       self::FIELD_COMMITTER_RAW => pht('Raw committer name'),
       self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'),
       self::FIELD_TASK_PRIORITY => pht('Task priority'),
       self::FIELD_ARCANIST_PROJECT => pht('Arcanist Project'),
+      self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'),
     );
   }
 
 
 /* -(  Conditions  )--------------------------------------------------------- */
 
 
   public function getConditionNameMap() {
     return array(
       self::CONDITION_CONTAINS        => pht('contains'),
       self::CONDITION_NOT_CONTAINS    => pht('does not contain'),
       self::CONDITION_IS              => pht('is'),
       self::CONDITION_IS_NOT          => pht('is not'),
       self::CONDITION_IS_ANY          => pht('is any of'),
       self::CONDITION_IS_TRUE         => pht('is true'),
       self::CONDITION_IS_FALSE        => pht('is false'),
       self::CONDITION_IS_NOT_ANY      => pht('is not any of'),
       self::CONDITION_INCLUDE_ALL     => pht('include all of'),
       self::CONDITION_INCLUDE_ANY     => pht('include any of'),
       self::CONDITION_INCLUDE_NONE    => pht('do not include'),
       self::CONDITION_IS_ME           => pht('is myself'),
       self::CONDITION_IS_NOT_ME       => pht('is not myself'),
       self::CONDITION_REGEXP          => pht('matches regexp'),
       self::CONDITION_RULE            => pht('matches:'),
       self::CONDITION_NOT_RULE        => pht('does not match:'),
       self::CONDITION_EXISTS          => pht('exists'),
       self::CONDITION_NOT_EXISTS      => pht('does not exist'),
       self::CONDITION_UNCONDITIONALLY => '',  // don't show anything!
       self::CONDITION_REGEXP_PAIR     => pht('matches regexp pair'),
       self::CONDITION_HAS_BIT         => pht('has bit'),
       self::CONDITION_NOT_BIT         => pht('lacks bit'),
     );
   }
 
   public function getConditionsForField($field) {
     switch ($field) {
       case self::FIELD_TITLE:
       case self::FIELD_BODY:
       case self::FIELD_COMMITTER_RAW:
       case self::FIELD_AUTHOR_RAW:
         return array(
           self::CONDITION_CONTAINS,
           self::CONDITION_NOT_CONTAINS,
           self::CONDITION_IS,
           self::CONDITION_IS_NOT,
           self::CONDITION_REGEXP,
         );
-      case self::FIELD_AUTHOR:
-      case self::FIELD_COMMITTER:
       case self::FIELD_REVIEWER:
       case self::FIELD_PUSHER:
       case self::FIELD_TASK_PRIORITY:
       case self::FIELD_ARCANIST_PROJECT:
         return array(
           self::CONDITION_IS_ANY,
           self::CONDITION_IS_NOT_ANY,
         );
       case self::FIELD_REPOSITORY:
       case self::FIELD_ASSIGNEE:
+      case self::FIELD_AUTHOR:
+      case self::FIELD_COMMITTER:
         return array(
           self::CONDITION_IS_ANY,
           self::CONDITION_IS_NOT_ANY,
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_TAGS:
       case self::FIELD_REVIEWERS:
       case self::FIELD_CC:
       case self::FIELD_AUTHOR_PROJECTS:
       case self::FIELD_PROJECTS:
       case self::FIELD_AFFECTED_PACKAGE:
       case self::FIELD_AFFECTED_PACKAGE_OWNER:
       case self::FIELD_PUSHER_PROJECTS:
       case self::FIELD_REPOSITORY_PROJECTS:
         return array(
           self::CONDITION_INCLUDE_ALL,
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_DIFF_FILE:
       case self::FIELD_BRANCHES:
         return array(
           self::CONDITION_CONTAINS,
           self::CONDITION_REGEXP,
         );
       case self::FIELD_DIFF_CONTENT:
       case self::FIELD_DIFF_ADDED_CONTENT:
       case self::FIELD_DIFF_REMOVED_CONTENT:
         return array(
           self::CONDITION_CONTAINS,
           self::CONDITION_REGEXP,
           self::CONDITION_REGEXP_PAIR,
         );
       case self::FIELD_RULE:
         return array(
           self::CONDITION_RULE,
           self::CONDITION_NOT_RULE,
         );
       case self::FIELD_CONTENT_SOURCE:
         return array(
           self::CONDITION_IS,
           self::CONDITION_IS_NOT,
         );
       case self::FIELD_ALWAYS:
         return array(
           self::CONDITION_UNCONDITIONALLY,
         );
       case self::FIELD_DIFFERENTIAL_REVIEWERS:
         return array(
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
           self::CONDITION_INCLUDE_ALL,
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
         );
       case self::FIELD_DIFFERENTIAL_CCS:
         return array(
           self::CONDITION_INCLUDE_ALL,
           self::CONDITION_INCLUDE_ANY,
           self::CONDITION_INCLUDE_NONE,
         );
       case self::FIELD_DIFFERENTIAL_REVISION:
       case self::FIELD_DIFFERENTIAL_ACCEPTED:
         return array(
           self::CONDITION_EXISTS,
           self::CONDITION_NOT_EXISTS,
         );
       case self::FIELD_IS_MERGE_COMMIT:
       case self::FIELD_DIFF_ENORMOUS:
       case self::FIELD_IS_NEW_OBJECT:
+      case self::FIELD_PUSHER_IS_COMMITTER:
         return array(
           self::CONDITION_IS_TRUE,
           self::CONDITION_IS_FALSE,
         );
       default:
         throw new Exception(
           "This adapter does not define conditions for field '{$field}'!");
     }
   }
 
   public function doesConditionMatch(
     HeraldEngine $engine,
     HeraldRule $rule,
     HeraldCondition $condition,
     $field_value) {
 
     $condition_type = $condition->getFieldCondition();
     $condition_value = $condition->getValue();
 
     switch ($condition_type) {
       case self::CONDITION_CONTAINS:
         // "Contains" can take an array of strings, as in "Any changed
         // filename" for diffs.
         foreach ((array)$field_value as $value) {
           if (stripos($value, $condition_value) !== false) {
             return true;
           }
         }
         return false;
       case self::CONDITION_NOT_CONTAINS:
         return (stripos($field_value, $condition_value) === false);
       case self::CONDITION_IS:
         return ($field_value == $condition_value);
       case self::CONDITION_IS_NOT:
         return ($field_value != $condition_value);
       case self::CONDITION_IS_ME:
         return ($field_value == $rule->getAuthorPHID());
       case self::CONDITION_IS_NOT_ME:
         return ($field_value != $rule->getAuthorPHID());
       case self::CONDITION_IS_ANY:
         if (!is_array($condition_value)) {
           throw new HeraldInvalidConditionException(
             "Expected condition value to be an array.");
         }
         $condition_value = array_fuse($condition_value);
         return isset($condition_value[$field_value]);
       case self::CONDITION_IS_NOT_ANY:
         if (!is_array($condition_value)) {
           throw new HeraldInvalidConditionException(
             "Expected condition value to be an array.");
         }
         $condition_value = array_fuse($condition_value);
         return !isset($condition_value[$field_value]);
       case self::CONDITION_INCLUDE_ALL:
         if (!is_array($field_value)) {
           throw new HeraldInvalidConditionException(
             "Object produced non-array value!");
         }
         if (!is_array($condition_value)) {
           throw new HeraldInvalidConditionException(
             "Expected condition value to be an array.");
         }
 
         $have = array_select_keys(array_fuse($field_value), $condition_value);
         return (count($have) == count($condition_value));
       case self::CONDITION_INCLUDE_ANY:
         return (bool)array_select_keys(
           array_fuse($field_value),
           $condition_value);
       case self::CONDITION_INCLUDE_NONE:
         return !array_select_keys(
           array_fuse($field_value),
           $condition_value);
       case self::CONDITION_EXISTS:
       case self::CONDITION_IS_TRUE:
         return (bool)$field_value;
       case self::CONDITION_NOT_EXISTS:
       case self::CONDITION_IS_FALSE:
         return !$field_value;
       case self::CONDITION_UNCONDITIONALLY:
         return (bool)$field_value;
       case self::CONDITION_REGEXP:
         foreach ((array)$field_value as $value) {
           // We add the 'S' flag because we use the regexp multiple times.
           // It shouldn't cause any troubles if the flag is already there
           // - /.*/S is evaluated same as /.*/SS.
           $result = @preg_match($condition_value . 'S', $value);
           if ($result === false) {
             throw new HeraldInvalidConditionException(
               "Regular expression is not valid!");
           }
           if ($result) {
             return true;
           }
         }
         return false;
       case self::CONDITION_REGEXP_PAIR:
         // Match a JSON-encoded pair of regular expressions against a
         // dictionary. The first regexp must match the dictionary key, and the
         // second regexp must match the dictionary value. If any key/value pair
         // in the dictionary matches both regexps, the condition is satisfied.
         $regexp_pair = json_decode($condition_value, true);
         if (!is_array($regexp_pair)) {
           throw new HeraldInvalidConditionException(
             "Regular expression pair is not valid JSON!");
         }
         if (count($regexp_pair) != 2) {
           throw new HeraldInvalidConditionException(
             "Regular expression pair is not a pair!");
         }
 
         $key_regexp   = array_shift($regexp_pair);
         $value_regexp = array_shift($regexp_pair);
 
         foreach ((array)$field_value as $key => $value) {
           $key_matches = @preg_match($key_regexp, $key);
           if ($key_matches === false) {
             throw new HeraldInvalidConditionException(
               "First regular expression is invalid!");
           }
           if ($key_matches) {
             $value_matches = @preg_match($value_regexp, $value);
             if ($value_matches === false) {
               throw new HeraldInvalidConditionException(
                 "Second regular expression is invalid!");
             }
             if ($value_matches) {
               return true;
             }
           }
         }
         return false;
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
         $rule = $engine->getRule($condition_value);
         if (!$rule) {
           throw new HeraldInvalidConditionException(
             "Condition references a rule which does not exist!");
         }
 
         $is_not = ($condition_type == self::CONDITION_NOT_RULE);
         $result = $engine->doesRuleMatch($rule, $this);
         if ($is_not) {
           $result = !$result;
         }
         return $result;
       case self::CONDITION_HAS_BIT:
         return (($condition_value & $field_value) === $condition_value);
       case self::CONDITION_NOT_BIT:
         return (($condition_value & $field_value) !== $condition_value);
       default:
         throw new HeraldInvalidConditionException(
           "Unknown condition '{$condition_type}'.");
     }
   }
 
   public function willSaveCondition(HeraldCondition $condition) {
     $condition_type = $condition->getFieldCondition();
     $condition_value = $condition->getValue();
 
     switch ($condition_type) {
       case self::CONDITION_REGEXP:
         $ok = @preg_match($condition_value, '');
         if ($ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression "%s" is not valid. Regular expressions '.
               'must have enclosing characters (e.g. "@/path/to/file@", not '.
               '"/path/to/file") and be syntactically correct.',
               $condition_value));
         }
         break;
       case self::CONDITION_REGEXP_PAIR:
         $json = json_decode($condition_value, true);
         if (!is_array($json)) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression pair "%s" is not valid JSON. Enter a '.
               'valid JSON array with two elements.',
               $condition_value));
         }
 
         if (count($json) != 2) {
           throw new HeraldInvalidConditionException(
             pht(
               'The regular expression pair "%s" must have exactly two '.
               'elements.',
               $condition_value));
         }
 
         $key_regexp = array_shift($json);
         $val_regexp = array_shift($json);
 
         $key_ok = @preg_match($key_regexp, '');
         if ($key_ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The first regexp in the regexp pair, "%s", is not a valid '.
               'regexp.',
               $key_regexp));
         }
 
         $val_ok = @preg_match($val_regexp, '');
         if ($val_ok === false) {
           throw new HeraldInvalidConditionException(
             pht(
               'The second regexp in the regexp pair, "%s", is not a valid '.
               'regexp.',
               $val_regexp));
         }
         break;
       case self::CONDITION_CONTAINS:
       case self::CONDITION_NOT_CONTAINS:
       case self::CONDITION_IS:
       case self::CONDITION_IS_NOT:
       case self::CONDITION_IS_ANY:
       case self::CONDITION_IS_NOT_ANY:
       case self::CONDITION_INCLUDE_ALL:
       case self::CONDITION_INCLUDE_ANY:
       case self::CONDITION_INCLUDE_NONE:
       case self::CONDITION_IS_ME:
       case self::CONDITION_IS_NOT_ME:
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
       case self::CONDITION_EXISTS:
       case self::CONDITION_NOT_EXISTS:
       case self::CONDITION_UNCONDITIONALLY:
       case self::CONDITION_HAS_BIT:
       case self::CONDITION_NOT_BIT:
       case self::CONDITION_IS_TRUE:
       case self::CONDITION_IS_FALSE:
         // No explicit validation for these types, although there probably
         // should be in some cases.
         break;
       default:
         throw new HeraldInvalidConditionException(
           pht(
             'Unknown condition "%s"!',
             $condition_type));
     }
   }
 
 
 /* -(  Actions  )------------------------------------------------------------ */
 
   abstract public function getActions($rule_type);
 
   public function getActionNameMap($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
       case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
         return array(
           self::ACTION_NOTHING      => pht('Do nothing'),
           self::ACTION_ADD_CC       => pht('Add emails to CC'),
           self::ACTION_REMOVE_CC    => pht('Remove emails from CC'),
           self::ACTION_EMAIL        => pht('Send an email to'),
           self::ACTION_AUDIT        => pht('Trigger an Audit by'),
           self::ACTION_FLAG         => pht('Mark with flag'),
           self::ACTION_ASSIGN_TASK  => pht('Assign task to'),
           self::ACTION_ADD_PROJECTS => pht('Add projects'),
           self::ACTION_ADD_REVIEWERS => pht('Add reviewers'),
           self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'),
           self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'),
           self::ACTION_BLOCK => pht('Block change with message'),
         );
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return array(
           self::ACTION_NOTHING      => pht('Do nothing'),
           self::ACTION_ADD_CC       => pht('Add me to CC'),
           self::ACTION_REMOVE_CC    => pht('Remove me from CC'),
           self::ACTION_EMAIL        => pht('Send me an email'),
           self::ACTION_AUDIT        => pht('Trigger an Audit by me'),
           self::ACTION_FLAG         => pht('Mark with flag'),
           self::ACTION_ASSIGN_TASK  => pht('Assign task to me'),
           self::ACTION_ADD_PROJECTS => pht('Add projects'),
           self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'),
           self::ACTION_ADD_BLOCKING_REVIEWERS =>
             pht('Add me as a blocking reviewer'),
         );
       default:
         throw new Exception("Unknown rule type '{$rule_type}'!");
     }
   }
 
   public function willSaveAction(
     HeraldRule $rule,
     HeraldAction $action) {
 
     $target = $action->getTarget();
     if (is_array($target)) {
       $target = array_keys($target);
     }
 
     $author_phid = $rule->getAuthorPHID();
 
     $rule_type = $rule->getRuleType();
     if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
       switch ($action->getAction()) {
         case self::ACTION_EMAIL:
         case self::ACTION_ADD_CC:
         case self::ACTION_REMOVE_CC:
         case self::ACTION_AUDIT:
         case self::ACTION_ASSIGN_TASK:
         case self::ACTION_ADD_REVIEWERS:
         case self::ACTION_ADD_BLOCKING_REVIEWERS:
           // For personal rules, force these actions to target the rule owner.
           $target = array($author_phid);
           break;
         case self::ACTION_FLAG:
           // Make sure flag color is valid; set to blue if not.
           $color_map = PhabricatorFlagColor::getColorNameMap();
           if (empty($color_map[$target])) {
             $target = PhabricatorFlagColor::COLOR_BLUE;
           }
           break;
         case self::ACTION_BLOCK:
         case self::ACTION_NOTHING:
           break;
         default:
           throw new HeraldInvalidActionException(
             pht(
               'Unrecognized action type "%s"!',
               $action->getAction()));
       }
     }
 
     $action->setTarget($target);
   }
 
 
 
 /* -(  Values  )------------------------------------------------------------- */
 
 
   public function getValueTypeForFieldAndCondition($field, $condition) {
     switch ($condition) {
       case self::CONDITION_CONTAINS:
       case self::CONDITION_NOT_CONTAINS:
       case self::CONDITION_REGEXP:
       case self::CONDITION_REGEXP_PAIR:
         return self::VALUE_TEXT;
       case self::CONDITION_IS:
       case self::CONDITION_IS_NOT:
         switch ($field) {
           case self::FIELD_CONTENT_SOURCE:
             return self::VALUE_CONTENT_SOURCE;
           default:
             return self::VALUE_TEXT;
         }
         break;
       case self::CONDITION_IS_ANY:
       case self::CONDITION_IS_NOT_ANY:
         switch ($field) {
           case self::FIELD_REPOSITORY:
             return self::VALUE_REPOSITORY;
           case self::FIELD_TASK_PRIORITY:
             return self::VALUE_TASK_PRIORITY;
           case self::FIELD_ARCANIST_PROJECT:
             return self::VALUE_ARCANIST_PROJECT;
           default:
             return self::VALUE_USER;
         }
         break;
       case self::CONDITION_INCLUDE_ALL:
       case self::CONDITION_INCLUDE_ANY:
       case self::CONDITION_INCLUDE_NONE:
         switch ($field) {
           case self::FIELD_REPOSITORY:
             return self::VALUE_REPOSITORY;
           case self::FIELD_CC:
             return self::VALUE_EMAIL;
           case self::FIELD_TAGS:
             return self::VALUE_TAG;
           case self::FIELD_AFFECTED_PACKAGE:
             return self::VALUE_OWNERS_PACKAGE;
           case self::FIELD_AUTHOR_PROJECTS:
           case self::FIELD_PUSHER_PROJECTS:
           case self::FIELD_PROJECTS:
           case self::FIELD_REPOSITORY_PROJECTS:
             return self::VALUE_PROJECT;
           case self::FIELD_REVIEWERS:
             return self::VALUE_USER_OR_PROJECT;
           default:
             return self::VALUE_USER;
         }
         break;
       case self::CONDITION_IS_ME:
       case self::CONDITION_IS_NOT_ME:
       case self::CONDITION_EXISTS:
       case self::CONDITION_NOT_EXISTS:
       case self::CONDITION_UNCONDITIONALLY:
       case self::CONDITION_IS_TRUE:
       case self::CONDITION_IS_FALSE:
         return self::VALUE_NONE;
       case self::CONDITION_RULE:
       case self::CONDITION_NOT_RULE:
         return self::VALUE_RULE;
       default:
         throw new Exception("Unknown condition '{$condition}'.");
     }
   }
 
   public static function getValueTypeForAction($action, $rule_type) {
     $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
 
     if ($is_personal) {
       switch ($action) {
         case self::ACTION_ADD_CC:
         case self::ACTION_REMOVE_CC:
         case self::ACTION_EMAIL:
         case self::ACTION_NOTHING:
         case self::ACTION_AUDIT:
         case self::ACTION_ASSIGN_TASK:
         case self::ACTION_ADD_REVIEWERS:
         case self::ACTION_ADD_BLOCKING_REVIEWERS:
           return self::VALUE_NONE;
         case self::ACTION_FLAG:
           return self::VALUE_FLAG_COLOR;
         case self::ACTION_ADD_PROJECTS:
           return self::VALUE_PROJECT;
         default:
           throw new Exception("Unknown or invalid action '{$action}'.");
       }
     } else {
       switch ($action) {
         case self::ACTION_ADD_CC:
         case self::ACTION_REMOVE_CC:
         case self::ACTION_EMAIL:
           return self::VALUE_EMAIL;
         case self::ACTION_NOTHING:
           return self::VALUE_NONE;
         case self::ACTION_ADD_PROJECTS:
           return self::VALUE_PROJECT;
         case self::ACTION_FLAG:
           return self::VALUE_FLAG_COLOR;
         case self::ACTION_ASSIGN_TASK:
           return self::VALUE_USER;
         case self::ACTION_AUDIT:
         case self::ACTION_ADD_REVIEWERS:
         case self::ACTION_ADD_BLOCKING_REVIEWERS:
           return self::VALUE_USER_OR_PROJECT;
         case self::ACTION_APPLY_BUILD_PLANS:
           return self::VALUE_BUILD_PLAN;
         case self::ACTION_BLOCK:
           return self::VALUE_TEXT;
         default:
           throw new Exception("Unknown or invalid action '{$action}'.");
       }
     }
   }
 
 
 /* -(  Repetition  )--------------------------------------------------------- */
 
 
   public function getRepetitionOptions() {
     return array(
       HeraldRepetitionPolicyConfig::EVERY,
     );
   }
 
 
   public static function applyFlagEffect(HeraldEffect $effect, $phid) {
     $color = $effect->getTarget();
 
     // TODO: Silly that we need to load this again here.
     $rule = id(new HeraldRule())->load($effect->getRuleID());
     $user = id(new PhabricatorUser())->loadOneWhere(
       'phid = %s',
       $rule->getAuthorPHID());
 
     $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
     if ($flag) {
       return new HeraldApplyTranscript(
         $effect,
         false,
         pht('Object already flagged.'));
     }
 
     $handle = id(new PhabricatorHandleQuery())
       ->setViewer($user)
       ->withPHIDs(array($phid))
       ->executeOne();
 
     $flag = new PhabricatorFlag();
     $flag->setOwnerPHID($user->getPHID());
     $flag->setType($handle->getType());
     $flag->setObjectPHID($handle->getPHID());
 
     // TOOD: Should really be transcript PHID, but it doesn't exist yet.
     $flag->setReasonPHID($user->getPHID());
 
     $flag->setColor($color);
     $flag->setNote(
       pht('Flagged by Herald Rule "%s".', $rule->getName()));
     $flag->save();
 
     return new HeraldApplyTranscript(
       $effect,
       true,
       pht('Added flag.'));
   }
 
   public static function getAllAdapters() {
     static $adapters;
     if (!$adapters) {
       $adapters = id(new PhutilSymbolLoader())
         ->setAncestorClass(__CLASS__)
         ->loadObjects();
       $adapters = msort($adapters, 'getAdapterSortKey');
     }
     return $adapters;
   }
 
   public static function getAdapterForContentType($content_type) {
     $adapters = self::getAllAdapters();
 
     foreach ($adapters as $adapter) {
       if ($adapter->getAdapterContentType() == $content_type) {
         return $adapter;
       }
     }
 
     throw new Exception(
       pht(
         'No adapter exists for Herald content type "%s".',
         $content_type));
   }
 
   public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
     $map = array();
 
     $adapters = HeraldAdapter::getAllAdapters();
     foreach ($adapters as $adapter) {
       if (!$adapter->isAvailableToUser($viewer)) {
         continue;
       }
       $type = $adapter->getAdapterContentType();
       $name = $adapter->getAdapterContentName();
       $map[$type] = $name;
     }
 
     return $map;
   }
 
   public function renderRuleAsText(HeraldRule $rule, array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
 
     $out = array();
 
     if ($rule->getMustMatchAll()) {
       $out[] = pht('When all of these conditions are met:');
     } else {
       $out[] = pht('When any of these conditions are met:');
     }
 
     $out[] = null;
     foreach ($rule->getConditions() as $condition) {
       $out[] = $this->renderConditionAsText($condition, $handles);
     }
     $out[] = null;
 
     $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
       HeraldRepetitionPolicyConfig::EVERY);
 
     if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
       $out[] = pht('Take these actions every time this rule matches:');
     } else {
       $out[] = pht('Take these actions the first time this rule matches:');
     }
 
     $out[] = null;
     foreach ($rule->getActions() as $action) {
       $out[] = $this->renderActionAsText($action, $handles);
     }
 
     return phutil_implode_html("\n", $out);
   }
 
   private function renderConditionAsText(
     HeraldCondition $condition,
     array $handles) {
 
     $field_type = $condition->getFieldName();
     $field_name = idx($this->getFieldNameMap(), $field_type);
 
     $condition_type = $condition->getFieldCondition();
     $condition_name = idx($this->getConditionNameMap(), $condition_type);
 
     $value = $this->renderConditionValueAsText($condition, $handles);
 
     return hsprintf('    %s %s %s', $field_name, $condition_name, $value);
   }
 
   private function renderActionAsText(
     HeraldAction $action,
     array $handles) {
     $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
 
     $action_type = $action->getAction();
     $action_name = idx($this->getActionNameMap($rule_global), $action_type);
 
     $target = $this->renderActionTargetAsText($action, $handles);
 
     return hsprintf('    %s %s', $action_name, $target);
   }
 
   private function renderConditionValueAsText(
     HeraldCondition $condition,
     array $handles) {
 
     $value = $condition->getValue();
     if (!is_array($value)) {
       $value = array($value);
     }
     switch ($condition->getFieldName()) {
       case self::FIELD_TASK_PRIORITY:
         $priority_map = ManiphestTaskPriority::getTaskPriorityMap();
         foreach ($value as $index => $val) {
           $name = idx($priority_map, $val);
           if ($name) {
             $value[$index] = $name;
           }
         }
         break;
       default:
         foreach ($value as $index => $val) {
           $handle = idx($handles, $val);
           if ($handle) {
             $value[$index] = $handle->renderLink();
           }
         }
         break;
     }
     $value = phutil_implode_html(', ', $value);
     return $value;
   }
 
   private function renderActionTargetAsText(
     HeraldAction $action,
     array $handles) {
 
     $target = $action->getTarget();
     if (!is_array($target)) {
       $target = array($target);
     }
     foreach ($target as $index => $val) {
       switch ($action->getAction()) {
         case self::ACTION_FLAG:
           $target[$index] = PhabricatorFlagColor::getColorName($val);
           break;
         default:
           $handle = idx($handles, $val);
           if ($handle) {
             $target[$index] = $handle->renderLink();
           }
           break;
       }
     }
     $target = phutil_implode_html(', ', $target);
     return $target;
   }
 
   /**
    * Given a @{class:HeraldRule}, this function extracts all the phids that
    * we'll want to load as handles later.
    *
    * This function performs a somewhat hacky approach to figuring out what
    * is and is not a phid - try to get the phid type and if the type is
    * *not* unknown assume its a valid phid.
    *
    * Don't try this at home. Use more strongly typed data at home.
    *
    * Think of the children.
    */
   public static function getHandlePHIDs(HeraldRule $rule) {
     $phids = array($rule->getAuthorPHID());
     foreach ($rule->getConditions() as $condition) {
       $value = $condition->getValue();
       if (!is_array($value)) {
         $value = array($value);
       }
       foreach ($value as $val) {
         if (phid_get_type($val) !=
             PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
           $phids[] = $val;
         }
       }
     }
 
     foreach ($rule->getActions() as $action) {
       $target = $action->getTarget();
       if (!is_array($target)) {
         $target = array($target);
       }
       foreach ($target as $val) {
         if (phid_get_type($val) !=
             PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
           $phids[] = $val;
         }
       }
     }
 
     if ($rule->isObjectRule()) {
       $phids[] = $rule->getTriggerObjectPHID();
     }
 
     return $phids;
   }
 
 }
diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php
index 749ebd943..34ead08f6 100644
--- a/src/applications/herald/storage/HeraldRule.php
+++ b/src/applications/herald/storage/HeraldRule.php
@@ -1,253 +1,253 @@
 <?php
 
 final class HeraldRule extends HeraldDAO
   implements
     PhabricatorFlaggableInterface,
     PhabricatorPolicyInterface {
 
   const TABLE_RULE_APPLIED = 'herald_ruleapplied';
 
   protected $name;
   protected $authorPHID;
 
   protected $contentType;
   protected $mustMatchAll;
   protected $repetitionPolicy;
   protected $ruleType;
   protected $isDisabled = 0;
   protected $triggerObjectPHID;
 
-  protected $configVersion = 32;
+  protected $configVersion = 33;
 
   // phids for which this rule has been applied
   private $ruleApplied = self::ATTACHABLE;
   private $validAuthor = self::ATTACHABLE;
   private $author = self::ATTACHABLE;
   private $conditions;
   private $actions;
   private $triggerObject = self::ATTACHABLE;
 
   public function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(HeraldPHIDTypeRule::TYPECONST);
   }
 
   public function getRuleApplied($phid) {
     return $this->assertAttachedKey($this->ruleApplied, $phid);
   }
 
   public function setRuleApplied($phid, $applied) {
     if ($this->ruleApplied === self::ATTACHABLE) {
       $this->ruleApplied = array();
     }
     $this->ruleApplied[$phid] = $applied;
     return $this;
   }
 
   public function loadConditions() {
     if (!$this->getID()) {
       return array();
     }
     return id(new HeraldCondition())->loadAllWhere(
       'ruleID = %d',
       $this->getID());
   }
 
   public function attachConditions(array $conditions) {
     assert_instances_of($conditions, 'HeraldCondition');
     $this->conditions = $conditions;
     return $this;
   }
 
   public function getConditions() {
     // TODO: validate conditions have been attached.
     return $this->conditions;
   }
 
   public function loadActions() {
     if (!$this->getID()) {
       return array();
     }
     return id(new HeraldAction())->loadAllWhere(
       'ruleID = %d',
       $this->getID());
   }
 
   public function attachActions(array $actions) {
     // TODO: validate actions have been attached.
     assert_instances_of($actions, 'HeraldAction');
     $this->actions = $actions;
     return $this;
   }
 
   public function getActions() {
     return $this->actions;
   }
 
   public function loadEdits() {
     if (!$this->getID()) {
       return array();
     }
     $edits = id(new HeraldRuleEdit())->loadAllWhere(
       'ruleID = %d ORDER BY dateCreated DESC',
       $this->getID());
 
     return $edits;
   }
 
   public function logEdit($editor_phid, $action) {
     id(new HeraldRuleEdit())
       ->setRuleID($this->getID())
       ->setRuleName($this->getName())
       ->setEditorPHID($editor_phid)
       ->setAction($action)
       ->save();
   }
 
   public function saveConditions(array $conditions) {
     assert_instances_of($conditions, 'HeraldCondition');
     return $this->saveChildren(
       id(new HeraldCondition())->getTableName(),
       $conditions);
   }
 
   public function saveActions(array $actions) {
     assert_instances_of($actions, 'HeraldAction');
     return $this->saveChildren(
       id(new HeraldAction())->getTableName(),
       $actions);
   }
 
   protected function saveChildren($table_name, array $children) {
     assert_instances_of($children, 'HeraldDAO');
 
     if (!$this->getID()) {
       throw new Exception("Save rule before saving children.");
     }
 
     foreach ($children as $child) {
       $child->setRuleID($this->getID());
     }
 
     $this->openTransaction();
       queryfx(
         $this->establishConnection('w'),
         'DELETE FROM %T WHERE ruleID = %d',
         $table_name,
         $this->getID());
       foreach ($children as $child) {
         $child->save();
       }
     $this->saveTransaction();
   }
 
   public function delete() {
     $this->openTransaction();
       queryfx(
         $this->establishConnection('w'),
         'DELETE FROM %T WHERE ruleID = %d',
         id(new HeraldCondition())->getTableName(),
         $this->getID());
       queryfx(
         $this->establishConnection('w'),
         'DELETE FROM %T WHERE ruleID = %d',
         id(new HeraldAction())->getTableName(),
         $this->getID());
       $result = parent::delete();
     $this->saveTransaction();
 
     return $result;
   }
 
   public function hasValidAuthor() {
     return $this->assertAttached($this->validAuthor);
   }
 
   public function attachValidAuthor($valid) {
     $this->validAuthor = $valid;
     return $this;
   }
 
   public function getAuthor() {
     return $this->assertAttached($this->author);
   }
 
   public function attachAuthor(PhabricatorUser $user) {
     $this->author = $user;
     return $this;
   }
 
   public function isGlobalRule() {
     return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_GLOBAL);
   }
 
   public function isPersonalRule() {
     return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
   }
 
   public function isObjectRule() {
     return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT);
   }
 
   public function attachTriggerObject($trigger_object) {
     $this->triggerObject = $trigger_object;
     return $this;
   }
 
   public function getTriggerObject() {
     return $this->assertAttached($this->triggerObject);
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
   public function getPolicy($capability) {
     if ($this->isGlobalRule()) {
       switch ($capability) {
         case PhabricatorPolicyCapability::CAN_VIEW:
           return PhabricatorPolicies::POLICY_USER;
         case PhabricatorPolicyCapability::CAN_EDIT:
           $app = 'PhabricatorApplicationHerald';
           $herald = PhabricatorApplication::getByClass($app);
           $global = HeraldCapabilityManageGlobalRules::CAPABILITY;
           return $herald->getPolicy($global);
       }
     } else if ($this->isObjectRule()) {
       return $this->getTriggerObject()->getPolicy($capability);
     } else {
       return PhabricatorPolicies::POLICY_NOONE;
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     if ($this->isPersonalRule()) {
       return ($viewer->getPHID() == $this->getAuthorPHID());
     } else {
       return false;
     }
   }
 
   public function describeAutomaticCapability($capability) {
     if ($this->isPersonalRule()) {
       return pht("A personal rule's owner can always view and edit it.");
     } else if ($this->isObjectRule()) {
       return pht("Object rules inherit the policies of their objects.");
     }
 
     return null;
   }
 
 }