diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
index 667e2013b2..2b4a9ac33a 100644
--- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
+++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
@@ -1,977 +1,1006 @@
 <?php
 
 /**
  * @task config   Configuring the Hook Engine
  * @task hook     Hook Execution
  * @task git      Git Hooks
  * @task hg       Mercurial Hooks
  * @task svn      Subversion Hooks
  * @task internal Internals
  */
 final class DiffusionCommitHookEngine extends Phobject {
 
   const ENV_USER = 'PHABRICATOR_USER';
   const ENV_REMOTE_ADDRESS = 'PHABRICATOR_REMOTE_ADDRESS';
   const ENV_REMOTE_PROTOCOL = 'PHABRICATOR_REMOTE_PROTOCOL';
 
   const EMPTY_HASH = '0000000000000000000000000000000000000000';
 
   private $viewer;
   private $repository;
   private $stdin;
   private $subversionTransaction;
   private $subversionRepository;
   private $remoteAddress;
   private $remoteProtocol;
   private $transactionKey;
   private $mercurialHook;
   private $mercurialCommits = array();
+  private $gitCommits = array();
 
   private $heraldViewerProjects;
 
 
 /* -(  Config  )------------------------------------------------------------- */
 
 
   public function setRemoteProtocol($remote_protocol) {
     $this->remoteProtocol = $remote_protocol;
     return $this;
   }
 
   public function getRemoteProtocol() {
     return $this->remoteProtocol;
   }
 
   public function setRemoteAddress($remote_address) {
     $this->remoteAddress = $remote_address;
     return $this;
   }
 
   public function getRemoteAddress() {
     return $this->remoteAddress;
   }
 
   private function getRemoteAddressForLog() {
     // If whatever we have here isn't a valid IPv4 address, just store `null`.
     // Older versions of PHP return `-1` on failure instead of `false`.
     $remote_address = $this->getRemoteAddress();
     $remote_address = max(0, ip2long($remote_address));
     $remote_address = nonempty($remote_address, null);
     return $remote_address;
   }
 
   private function getTransactionKey() {
     if (!$this->transactionKey) {
       $entropy = Filesystem::readRandomBytes(64);
       $this->transactionKey = PhabricatorHash::digestForIndex($entropy);
     }
     return $this->transactionKey;
   }
 
   public function setSubversionTransactionInfo($transaction, $repository) {
     $this->subversionTransaction = $transaction;
     $this->subversionRepository = $repository;
     return $this;
   }
 
   public function setStdin($stdin) {
     $this->stdin = $stdin;
     return $this;
   }
 
   public function getStdin() {
     return $this->stdin;
   }
 
   public function setRepository(PhabricatorRepository $repository) {
     $this->repository = $repository;
     return $this;
   }
 
   public function getRepository() {
     return $this->repository;
   }
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   public function setMercurialHook($mercurial_hook) {
     $this->mercurialHook = $mercurial_hook;
     return $this;
   }
 
   public function getMercurialHook() {
     return $this->mercurialHook;
   }
 
 
 /* -(  Hook Execution  )----------------------------------------------------- */
 
 
   public function execute() {
     $ref_updates = $this->findRefUpdates();
     $all_updates = $ref_updates;
 
     $caught = null;
     try {
 
       try {
         $this->rejectDangerousChanges($ref_updates);
       } catch (DiffusionCommitHookRejectException $ex) {
         // If we're rejecting dangerous changes, flag everything that we've
         // seen as rejected so it's clear that none of it was accepted.
         foreach ($all_updates as $update) {
           $update->setRejectCode(
             PhabricatorRepositoryPushLog::REJECT_DANGEROUS);
         }
         throw $ex;
       }
 
       $this->applyHeraldRefRules($ref_updates, $all_updates);
 
       $content_updates = $this->findContentUpdates($ref_updates);
       $all_updates = array_merge($all_updates, $content_updates);
 
       $this->applyHeraldContentRules($content_updates, $all_updates);
 
       // TODO: Fire external hooks.
 
       // If we make it this far, we're accepting these changes. Mark all the
       // logs as accepted.
       foreach ($all_updates as $update) {
         $update->setRejectCode(PhabricatorRepositoryPushLog::REJECT_ACCEPT);
       }
     } catch (Exception $ex) {
       // We'll throw this again in a minute, but we want to save all the logs
       // first.
       $caught = $ex;
     }
 
     // Save all the logs no matter what the outcome was.
     foreach ($all_updates as $update) {
       $update->save();
     }
 
     if ($caught) {
       throw $caught;
     }
 
     return 0;
   }
 
   private function findRefUpdates() {
     $type = $this->getRepository()->getVersionControlSystem();
     switch ($type) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         return $this->findGitRefUpdates();
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         return $this->findMercurialRefUpdates();
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         return $this->findSubversionRefUpdates();
       default:
         throw new Exception(pht('Unsupported repository type "%s"!', $type));
     }
   }
 
   private function rejectDangerousChanges(array $ref_updates) {
     assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
 
     $repository = $this->getRepository();
     if ($repository->shouldAllowDangerousChanges()) {
       return;
     }
 
     $flag_dangerous = PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
 
     foreach ($ref_updates as $ref_update) {
       if (!$ref_update->hasChangeFlags($flag_dangerous)) {
         // This is not a dangerous change.
         continue;
       }
 
       // We either have a branch deletion or a non fast-forward branch update.
       // Format a message and reject the push.
 
       $message = pht(
         "DANGEROUS CHANGE: %s\n".
         "Dangerous change protection is enabled for this repository.\n".
         "Edit the repository configuration before making dangerous changes.",
         $ref_update->getDangerousChangeDescription());
 
       throw new DiffusionCommitHookRejectException($message);
     }
   }
 
   private function findContentUpdates(array $ref_updates) {
     assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
 
     $type = $this->getRepository()->getVersionControlSystem();
     switch ($type) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         return $this->findGitContentUpdates($ref_updates);
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         return $this->findMercurialContentUpdates($ref_updates);
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         return $this->findSubversionContentUpdates($ref_updates);
       default:
         throw new Exception(pht('Unsupported repository type "%s"!', $type));
     }
   }
 
 
 /* -(  Herald  )------------------------------------------------------------- */
 
   private function applyHeraldRefRules(
     array $ref_updates,
     array $all_updates) {
     $this->applyHeraldRules(
       $ref_updates,
       new HeraldPreCommitRefAdapter(),
       $all_updates);
   }
 
   private function applyHeraldContentRules(
     array $content_updates,
     array $all_updates) {
     $this->applyHeraldRules(
       $content_updates,
       new HeraldPreCommitContentAdapter(),
       $all_updates);
   }
 
   private function applyHeraldRules(
     array $updates,
     HeraldAdapter $adapter_template,
     array $all_updates) {
 
     if (!$updates) {
       return;
     }
 
     $adapter_template->setHookEngine($this);
 
     $engine = new HeraldEngine();
     $rules = null;
     $blocking_effect = null;
     $blocked_update = null;
     foreach ($updates as $update) {
       $adapter = id(clone $adapter_template)
         ->setPushLog($update);
 
       if ($rules === null) {
         $rules = $engine->loadRulesForAdapter($adapter);
       }
 
       $effects = $engine->applyRules($rules, $adapter);
       $engine->applyEffects($effects, $adapter, $rules);
       $xscript = $engine->getTranscript();
 
       if ($blocking_effect === null) {
         foreach ($effects as $effect) {
           if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) {
             $blocking_effect = $effect;
             $blocked_update = $update;
             break;
           }
         }
       }
     }
 
     if ($blocking_effect) {
       foreach ($all_updates as $update) {
         $update->setRejectCode(PhabricatorRepositoryPushLog::REJECT_HERALD);
         $update->setRejectDetails($blocking_effect->getRulePHID());
       }
 
       $message = $blocking_effect->getTarget();
       if (!strlen($message)) {
         $message = pht('(None.)');
       }
 
       $rules = mpull($rules, null, 'getID');
       $rule = idx($rules, $effect->getRuleID());
       if ($rule && strlen($rule->getName())) {
         $rule_name = $rule->getName();
       } else {
         $rule_name = pht('Unnamed Herald Rule');
       }
 
       $blocked_ref_name = coalesce(
         $blocked_update->getRefName(),
         $blocked_update->getRefNewShort());
       $blocked_name = $blocked_update->getRefType().'/'.$blocked_ref_name;
 
       throw new DiffusionCommitHookRejectException(
         pht(
           "This push was rejected by Herald push rule %s.\n".
           "Change: %s\n".
           "  Rule: %s\n".
           "Reason: %s",
           'H'.$blocking_effect->getRuleID(),
           $blocked_name,
           $rule_name,
           $message));
     }
   }
 
   public function loadViewerProjectPHIDsForHerald() {
     // This just caches the viewer's projects so we don't need to load them
     // over and over again when applying Herald rules.
     if ($this->heraldViewerProjects === null) {
       $this->heraldViewerProjects = id(new PhabricatorProjectQuery())
         ->setViewer($this->getViewer())
         ->withMemberPHIDs(array($this->getViewer()->getPHID()))
         ->execute();
     }
 
     return mpull($this->heraldViewerProjects, 'getPHID');
   }
 
 
 /* -(  Git  )---------------------------------------------------------------- */
 
 
   private function findGitRefUpdates() {
     $ref_updates = array();
 
     // First, parse stdin, which lists all the ref changes. The input looks
     // like this:
     //
     //   <old hash> <new hash> <ref>
 
     $stdin = $this->getStdin();
     $lines = phutil_split_lines($stdin, $retain_endings = false);
     foreach ($lines as $line) {
       $parts = explode(' ', $line, 3);
       if (count($parts) != 3) {
         throw new Exception(pht('Expected "old new ref", got "%s".', $line));
       }
 
       $ref_old = $parts[0];
       $ref_new = $parts[1];
       $ref_raw = $parts[2];
 
       if (preg_match('(^refs/heads/)', $ref_raw)) {
         $ref_type = PhabricatorRepositoryPushLog::REFTYPE_BRANCH;
         $ref_raw = substr($ref_raw, strlen('refs/heads/'));
       } else if (preg_match('(^refs/tags/)', $ref_raw)) {
         $ref_type = PhabricatorRepositoryPushLog::REFTYPE_TAG;
         $ref_raw = substr($ref_raw, strlen('refs/tags/'));
       } else {
         throw new Exception(
           pht(
             "Unable to identify the reftype of '%s'. Rejecting push.",
             $ref_raw));
       }
 
       $ref_update = $this->newPushLog()
         ->setRefType($ref_type)
         ->setRefName($ref_raw)
         ->setRefOld($ref_old)
         ->setRefNew($ref_new);
 
       $ref_updates[] = $ref_update;
     }
 
     $this->findGitMergeBases($ref_updates);
     $this->findGitChangeFlags($ref_updates);
 
     return $ref_updates;
   }
 
 
   private function findGitMergeBases(array $ref_updates) {
     assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
 
     $futures = array();
     foreach ($ref_updates as $key => $ref_update) {
       // If the old hash is "00000...", the ref is being created (either a new
       // branch, or a new tag). If the new hash is "00000...", the ref is being
       // deleted. If both are nonempty, the ref is being updated. For updates,
       // we'll figure out the `merge-base` of the old and new objects here. This
       // lets us reject non-FF changes cheaply; later, we'll figure out exactly
       // which commits are new.
       $ref_old = $ref_update->getRefOld();
       $ref_new = $ref_update->getRefNew();
 
       if (($ref_old === self::EMPTY_HASH) ||
           ($ref_new === self::EMPTY_HASH)) {
         continue;
       }
 
       $futures[$key] = $this->getRepository()->getLocalCommandFuture(
         'merge-base %s %s',
         $ref_old,
         $ref_new);
     }
 
     foreach (Futures($futures)->limit(8) as $key => $future) {
 
       // If 'old' and 'new' have no common ancestors (for example, a force push
       // which completely rewrites a ref), `git merge-base` will exit with
       // an error and no output. It would be nice to find a positive test
       // for this instead, but I couldn't immediately come up with one. See
       // T4224. Assume this means there are no ancestors.
 
       list($err, $stdout) = $future->resolve();
 
       if ($err) {
         $merge_base = null;
       } else {
         $merge_base = rtrim($stdout, "\n");
       }
 
+      $ref_update = $ref_updates[$key];
       $ref_update->setMergeBase($merge_base);
     }
 
     return $ref_updates;
   }
 
 
   private function findGitChangeFlags(array $ref_updates) {
     assert_instances_of($ref_updates, 'PhabricatorRepositoryPushLog');
 
     foreach ($ref_updates as $key => $ref_update) {
       $ref_old = $ref_update->getRefOld();
       $ref_new = $ref_update->getRefNew();
       $ref_type = $ref_update->getRefType();
 
       $ref_flags = 0;
       $dangerous = null;
 
       if ($ref_old === self::EMPTY_HASH) {
         $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
       } else if ($ref_new === self::EMPTY_HASH) {
         $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
         if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) {
           $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
           $dangerous = pht(
             "The change you're attempting to push deletes the branch '%s'.",
             $ref_update->getRefName());
         }
       } else {
         $merge_base = $ref_update->getMergeBase();
         if ($merge_base == $ref_old) {
           // This is a fast-forward update to an existing branch.
           // These are safe.
           $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
         } else {
           $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE;
 
           // For now, we don't consider deleting or moving tags to be a
           // "dangerous" update. It's way harder to get wrong and should be easy
           // to recover from once we have better logging. Only add the dangerous
           // flag if this ref is a branch.
 
           if ($ref_type == PhabricatorRepositoryPushLog::REFTYPE_BRANCH) {
             $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
 
             $dangerous = pht(
               "The change you're attempting to push updates the branch '%s' ".
               "from '%s' to '%s', but this is not a fast-forward. Pushes ".
               "which rewrite published branch history are dangerous.",
               $ref_update->getRefName(),
               $ref_update->getRefOldShort(),
               $ref_update->getRefNewShort());
           }
         }
       }
 
       $ref_update->setChangeFlags($ref_flags);
       if ($dangerous !== null) {
         $ref_update->attachDangerousChangeDescription($dangerous);
       }
     }
 
     return $ref_updates;
   }
 
 
   private function findGitContentUpdates(array $ref_updates) {
     $flag_delete = PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
 
     $futures = array();
     foreach ($ref_updates as $key => $ref_update) {
       if ($ref_update->hasChangeFlags($flag_delete)) {
         // Deleting a branch or tag can never create any new commits.
         continue;
       }
 
       // NOTE: This piece of magic finds all new commits, by walking backward
       // from the new value to the value of *any* existing ref in the
       // repository. Particularly, this will cover the cases of a new branch, a
       // completely moved tag, etc.
       $futures[$key] = $this->getRepository()->getLocalCommandFuture(
         'log --format=%s %s --not --all',
         '%H',
         $ref_update->getRefNew());
     }
 
     $content_updates = array();
     foreach (Futures($futures)->limit(8) as $key => $future) {
       list($stdout) = $future->resolvex();
 
       if (!strlen(trim($stdout))) {
         // This change doesn't have any new commits. One common case of this
         // is creating a new tag which points at an existing commit.
         continue;
       }
 
       $commits = phutil_split_lines($stdout, $retain_newlines = false);
 
+      // If we're looking at a branch, mark all of the new commits as on that
+      // branch. It's only possible for these commits to be on updated branches,
+      // since any other branch heads are necessarily behind them.
+      $branch_name = null;
+      $ref_update = $ref_updates[$key];
+      $type_branch = PhabricatorRepositoryPushLog::REFTYPE_BRANCH;
+      if ($ref_update->getRefType() == $type_branch) {
+        $branch_name = $ref_update->getRefName();
+      }
+
       foreach ($commits as $commit) {
+        if ($branch_name) {
+          $this->gitCommits[$commit][] = $branch_name;
+        }
         $content_updates[$commit] = $this->newPushLog()
           ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
           ->setRefNew($commit)
           ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD);
       }
     }
 
     return $content_updates;
   }
 
 
 /* -(  Mercurial  )---------------------------------------------------------- */
 
 
   private function findMercurialRefUpdates() {
     $hook = $this->getMercurialHook();
     switch ($hook) {
       case 'pretxnchangegroup':
         return $this->findMercurialChangegroupRefUpdates();
       case 'prepushkey':
         return $this->findMercurialPushKeyRefUpdates();
       default:
         throw new Exception(pht('Unrecognized hook "%s"!', $hook));
     }
   }
 
   private function findMercurialChangegroupRefUpdates() {
     $hg_node = getenv('HG_NODE');
     if (!$hg_node) {
       throw new Exception(pht('Expected HG_NODE in environment!'));
     }
 
     // NOTE: We need to make sure this is passed to subprocesses, or they won't
     // be able to see new commits. Mercurial uses this as a marker to determine
     // whether the pending changes are visible or not.
     $_ENV['HG_PENDING'] = getenv('HG_PENDING');
     $repository = $this->getRepository();
 
     $futures = array();
 
     foreach (array('old', 'new') as $key) {
       $futures[$key] = $repository->getLocalCommandFuture(
         'heads --template %s',
         '{node}\1{branches}\2');
     }
     // Wipe HG_PENDING out of the old environment so we see the pre-commit
     // state of the repository.
     $futures['old']->updateEnv('HG_PENDING', null);
 
     $futures['commits'] = $repository->getLocalCommandFuture(
       "log --rev %s --rev tip --template %s",
       hgsprintf('%s', $hg_node),
       '{node}\1{branches}\2');
 
     // Resolve all of the futures now. We don't need the 'commits' future yet,
     // but it simplifies the logic to just get it out of the way.
     foreach (Futures($futures) as $future) {
       $future->resolve();
     }
 
     list($commit_raw) = $futures['commits']->resolvex();
     $commit_map = $this->parseMercurialCommits($commit_raw);
     $this->mercurialCommits = $commit_map;
 
     // NOTE: `hg heads` exits with an error code and no output if the repository
     // has no heads. Most commonly this happens on a new repository. We know
     // we can run `hg` successfully since the `hg log` above didn't error, so
     // just ignore the error code.
 
     list($err, $old_raw) = $futures['old']->resolve();
     $old_refs = $this->parseMercurialHeads($old_raw);
 
     list($err, $new_raw) = $futures['new']->resolve();
     $new_refs = $this->parseMercurialHeads($new_raw);
 
     $all_refs = array_keys($old_refs + $new_refs);
 
     $ref_updates = array();
     foreach ($all_refs as $ref) {
       $old_heads = idx($old_refs, $ref, array());
       $new_heads = idx($new_refs, $ref, array());
 
       sort($old_heads);
       sort($new_heads);
 
       if ($old_heads === $new_heads) {
         // No changes to this branch, so skip it.
         continue;
       }
 
       if (!$new_heads) {
         if ($old_heads) {
           // It looks like this push deletes a branch, but that isn't possible
           // in Mercurial, so something is going wrong here. Bail out.
           throw new Exception(
             pht(
               'Mercurial repository has no new head for branch "%s" after '.
               'push. This is unexpected; rejecting change.'));
         } else {
           // Obviously, this should never be possible either, as it makes
           // no sense. Explode.
           throw new Exception(
             pht(
               'Mercurial repository has no new or old heads for branch "%s" '.
               'after push. This makes no sense; rejecting change.'));
         }
       }
 
       $stray_heads = array();
       if (count($old_heads) > 1) {
         // HORRIBLE: In Mercurial, branches can have multiple heads. If the
         // old branch had multiple heads, we need to figure out which new
         // heads descend from which old heads, so we can tell whether you're
         // actively creating new heads (dangerous) or just working in a
         // repository that's already full of garbage (strongly discouraged but
         // not as inherently dangerous). These cases should be very uncommon.
 
         $dfutures = array();
         foreach ($old_heads as $old_head) {
           $dfutures[$old_head] = $repository->getLocalCommandFuture(
             'log --rev %s --template %s',
             hgsprintf('(descendants(%s) and head())', $old_head),
             '{node}\1');
         }
 
         $head_map = array();
         foreach (Futures($dfutures) as $future_head => $dfuture) {
           list($stdout) = $dfuture->resolvex();
           $head_map[$future_head] = array_filter(explode("\1", $stdout));
         }
 
         // Now, find all the new stray heads this push creates, if any. These
         // are new heads which do not descend from the old heads.
         $seen = array_fuse(array_mergev($head_map));
         foreach ($new_heads as $new_head) {
           if (empty($seen[$new_head])) {
             $head_map[self::EMPTY_HASH][] = $new_head;
           }
         }
       } else if ($old_heads) {
         $head_map[head($old_heads)] = $new_heads;
       } else {
         $head_map[self::EMPTY_HASH] = $new_heads;
       }
 
       foreach ($head_map as $old_head => $child_heads) {
         foreach ($child_heads as $new_head) {
           if ($new_head === $old_head) {
             continue;
           }
 
           $ref_flags = 0;
           $dangerous = null;
           if ($old_head == self::EMPTY_HASH) {
             $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
           } else {
             $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
           }
 
           $splits_existing_head = (count($child_heads) > 1);
           $creates_duplicate_head = ($old_head == self::EMPTY_HASH) &&
                                     (count($head_map) > 1);
 
           if ($splits_existing_head || $creates_duplicate_head) {
             $readable_child_heads = array();
             foreach ($child_heads as $child_head) {
               $readable_child_heads[] = substr($child_head, 0, 12);
             }
 
             $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS;
 
             if ($splits_existing_head) {
               // We're splitting an existing head into two or more heads.
               // This is dangerous, and a super bad idea. Note that we're only
               // raising this if you're actively splitting a branch head. If a
               // head split in the past, we don't consider appends to it
               // to be dangerous.
               $dangerous = pht(
                 "The change you're attempting to push splits the head of ".
                 "branch '%s' into multiple heads: %s. This is inadvisable ".
                 "and dangerous.",
                 $ref,
                 implode(', ', $readable_child_heads));
             } else {
               // We're adding a second (or more) head to a branch. The new
               // head is not a descendant of any old head.
               $dangerous = pht(
                 "The change you're attempting to push creates new, divergent ".
                 "heads for the branch '%s': %s. This is inadvisable and ".
                 "dangerous.",
                 $ref,
                 implode(', ', $readable_child_heads));
             }
           }
 
           $ref_update = $this->newPushLog()
             ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BRANCH)
             ->setRefName($ref)
             ->setRefOld($old_head)
             ->setRefNew($new_head)
             ->setChangeFlags($ref_flags);
 
           if ($dangerous !== null) {
             $ref_update->attachDangerousChangeDescription($dangerous);
           }
 
           $ref_updates[] = $ref_update;
         }
       }
     }
 
     return $ref_updates;
   }
 
   private function findMercurialPushKeyRefUpdates() {
     $key_namespace = getenv('HG_NAMESPACE');
 
     if ($key_namespace === 'phases') {
       // Mercurial changes commit phases as part of normal push operations. We
       // just ignore these, as they don't seem to represent anything
       // interesting.
       return array();
     }
 
     $key_name = getenv('HG_KEY');
 
     $key_old = getenv('HG_OLD');
     if (!strlen($key_old)) {
       $key_old = null;
     }
 
     $key_new = getenv('HG_NEW');
     if (!strlen($key_new)) {
       $key_new = null;
     }
 
     if ($key_namespace !== 'bookmarks') {
       throw new Exception(
         pht(
           "Unknown Mercurial key namespace '%s', with key '%s' (%s -> %s). ".
           "Rejecting push.",
           $key_namespace,
           $key_name,
           coalesce($key_old, pht('null')),
           coalesce($key_new, pht('null'))));
     }
 
     if ($key_old === $key_new) {
       // We get a callback when the bookmark doesn't change. Just ignore this,
       // as it's a no-op.
       return array();
     }
 
     $ref_flags = 0;
     $merge_base = null;
     if ($key_old === null) {
       $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
     } else if ($key_new === null) {
       $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
     } else {
       list($merge_base_raw) = $this->getRepository()->execxLocalCommand(
         'log --template %s --rev %s',
         '{node}',
         hgsprintf('ancestor(%s, %s)', $key_old, $key_new));
 
       if (strlen(trim($merge_base_raw))) {
         $merge_base = trim($merge_base_raw);
       }
 
       if ($merge_base && ($merge_base === $key_old)) {
         $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
       } else {
         $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE;
       }
     }
 
     $ref_update = $this->newPushLog()
       ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK)
       ->setRefName($key_name)
       ->setRefOld(coalesce($key_old, self::EMPTY_HASH))
       ->setRefNew(coalesce($key_new, self::EMPTY_HASH))
       ->setChangeFlags($ref_flags);
 
     return array($ref_update);
   }
 
   private function findMercurialContentUpdates(array $ref_updates) {
     $content_updates = array();
 
     foreach ($this->mercurialCommits as $commit => $branches) {
       $content_updates[$commit] = $this->newPushLog()
         ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
         ->setRefNew($commit)
         ->setChangeFlags(PhabricatorRepositoryPushLog::CHANGEFLAG_ADD);
     }
 
     return $content_updates;
   }
 
   private function parseMercurialCommits($raw) {
     $commits_lines = explode("\2", $raw);
     $commits_lines = array_filter($commits_lines);
     $commit_map = array();
     foreach ($commits_lines as $commit_line) {
       list($node, $branches_raw) = explode("\1", $commit_line);
 
       if (!strlen($branches_raw)) {
         $branches = array('default');
       } else {
         $branches = explode(' ', $branches_raw);
       }
 
       $commit_map[$node] = $branches;
     }
 
     return $commit_map;
   }
 
   private function parseMercurialHeads($raw) {
     $heads_map = $this->parseMercurialCommits($raw);
 
     $heads = array();
     foreach ($heads_map as $commit => $branches) {
       foreach ($branches as $branch) {
         $heads[$branch][] = $commit;
       }
     }
 
     return $heads;
   }
 
 
 /* -(  Subversion  )--------------------------------------------------------- */
 
 
   private function findSubversionRefUpdates() {
     // Subversion doesn't have any kind of mutable ref metadata.
     return array();
   }
 
   private function findSubversionContentUpdates(array $ref_updates) {
     list($youngest) = execx(
       'svnlook youngest %s',
       $this->subversionRepository);
     $ref_new = (int)$youngest + 1;
 
     $ref_flags = 0;
     $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_ADD;
     $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND;
 
     $ref_content = $this->newPushLog()
       ->setRefType(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)
       ->setRefNew($ref_new)
       ->setChangeFlags($ref_flags);
 
     return array($ref_content);
   }
 
 
 /* -(  Internals  )---------------------------------------------------------- */
 
 
   private function newPushLog() {
     // NOTE: By default, we create these with REJECT_BROKEN as the reject
     // code. This indicates a broken hook, and covers the case where we
     // encounter some unexpected exception and consequently reject the changes.
 
     // NOTE: We generate PHIDs up front so the Herald transcripts can pick them
     // up.
     $phid = id(new PhabricatorRepositoryPushLog())->generatePHID();
 
     return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer())
       ->setPHID($phid)
       ->attachRepository($this->getRepository())
       ->setRepositoryPHID($this->getRepository()->getPHID())
       ->setEpoch(time())
       ->setRemoteAddress($this->getRemoteAddressForLog())
       ->setRemoteProtocol($this->getRemoteProtocol())
       ->setTransactionKey($this->getTransactionKey())
       ->setRejectCode(PhabricatorRepositoryPushLog::REJECT_BROKEN)
       ->setRejectDetails(null);
   }
 
   public function loadChangesetsForCommit($identifier) {
     $vcs = $this->getRepository()->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         // For git and hg, we can use normal commands.
         $drequest = DiffusionRequest::newFromDictionary(
           array(
             'repository' => $this->getRepository(),
             'user' => $this->getViewer(),
             'commit' => $identifier,
           ));
         $raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest)
           ->setTimeout(5 * 60)
           ->setLinesOfContext(0)
           ->loadRawDiff();
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         // TODO: This diff has 3 lines of context, which produces slightly
         // incorrect "added file content" and "removed file content" results.
         // This may also choke on binaries, but "svnlook diff" does not support
         // the "--diff-cmd" flag.
 
         // For subversion, we need to use `svnlook`.
         list($raw_diff) = execx(
           'svnlook diff -t %s %s',
           $this->subversionTransaction,
           $this->subversionRepository);
         break;
       default:
         throw new Exception(pht("Unknown VCS '%s!'", $vcs));
     }
 
     $parser = new ArcanistDiffParser();
     $changes = $parser->parseDiff($raw_diff);
     $diff = DifferentialDiff::newFromRawChanges($changes);
     return $diff->getChangesets();
   }
 
   public function loadCommitRefForCommit($identifier) {
     $repository = $this->getRepository();
     $vcs = $repository->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         return id(new DiffusionLowLevelCommitQuery())
           ->setRepository($repository)
           ->withIdentifier($identifier)
           ->execute();
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         // For subversion, we need to use `svnlook`.
         list($message) = execx(
           'svnlook log -t %s %s',
           $this->subversionTransaction,
           $this->subversionRepository);
 
         return id(new DiffusionCommitRef())
           ->setMessage($message);
         break;
       default:
         throw new Exception(pht("Unknown VCS '%s!'", $vcs));
     }
   }
 
+  public function loadBranches($identifier) {
+    $repository = $this->getRepository();
+    $vcs = $repository->getVersionControlSystem();
+    switch ($vcs) {
+      case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
+        return idx($this->gitCommits, $identifier, array());
+      case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
+        return idx($this->mercurialCommits, $identifier, array());
+      case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
+        // Subversion doesn't have branches.
+        return array();
+    }
+  }
+
 
 }
diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
index aee68f126d..95cd112fad 100644
--- a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
+++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php
@@ -1,332 +1,339 @@
 <?php
 
 final class HeraldPreCommitContentAdapter extends HeraldAdapter {
 
   private $log;
   private $hookEngine;
   private $changesets;
   private $commitRef;
   private $fields;
   private $revision = false;
 
   public function setPushLog(PhabricatorRepositoryPushLog $log) {
     $this->log = $log;
     return $this;
   }
 
   public function setHookEngine(DiffusionCommitHookEngine $engine) {
     $this->hookEngine = $engine;
     return $this;
   }
 
   public function getAdapterApplicationClass() {
     return 'PhabricatorApplicationDiffusion';
   }
 
   public function getObject() {
     return $this->log;
   }
 
   public function getAdapterContentName() {
     return pht('Commit Hook: Commit Content');
   }
 
   public function getFieldNameMap() {
     return array(
     ) + parent::getFieldNameMap();
   }
 
   public function getFields() {
     return array_merge(
       array(
         self::FIELD_BODY,
         self::FIELD_AUTHOR,
         self::FIELD_COMMITTER,
+        self::FIELD_BRANCHES,
         self::FIELD_DIFF_FILE,
         self::FIELD_DIFF_CONTENT,
         self::FIELD_DIFF_ADDED_CONTENT,
         self::FIELD_DIFF_REMOVED_CONTENT,
         self::FIELD_REPOSITORY,
         self::FIELD_PUSHER,
         self::FIELD_PUSHER_PROJECTS,
         self::FIELD_DIFFERENTIAL_REVISION,
         self::FIELD_DIFFERENTIAL_ACCEPTED,
         self::FIELD_DIFFERENTIAL_REVIEWERS,
         self::FIELD_DIFFERENTIAL_CCS,
         self::FIELD_IS_MERGE_COMMIT,
         self::FIELD_RULE,
       ),
       parent::getFields());
   }
 
   public function getConditionsForField($field) {
     switch ($field) {
     }
     return parent::getConditionsForField($field);
   }
 
   public function getActions($rule_type) {
     switch ($rule_type) {
       case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
         return array(
           self::ACTION_BLOCK,
           self::ACTION_NOTHING
         );
       case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
         return array(
           self::ACTION_NOTHING,
         );
     }
   }
 
   public function getValueTypeForFieldAndCondition($field, $condition) {
     return parent::getValueTypeForFieldAndCondition($field, $condition);
   }
 
   public function getPHID() {
     return $this->getObject()->getPHID();
   }
 
   public function getHeraldName() {
     return pht('Push Log');
   }
 
   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_COMMITTER:
         return $this->getCommitterPHID();
+      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_REPOSITORY:
         return $this->hookEngine->getRepository()->getPHID();
       case self::FIELD_PUSHER:
         return $this->hookEngine->getViewer()->getPHID();
       case self::FIELD_PUSHER_PROJECTS:
         return $this->hookEngine->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();
     }
 
     return parent::getHeraldField($field);
   }
 
   public function applyHeraldEffects(array $effects) {
     assert_instances_of($effects, 'HeraldEffect');
 
     $result = array();
     foreach ($effects as $effect) {
       $action = $effect->getAction();
       switch ($action) {
         case self::ACTION_NOTHING:
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Did nothing.'));
           break;
         case self::ACTION_BLOCK:
           $result[] = new HeraldApplyTranscript(
             $effect,
             true,
             pht('Blocked push.'));
           break;
         default:
           throw new Exception(pht('No rules to handle action "%s"!', $action));
       }
     }
 
     return $result;
   }
 
   private function getDiffContent($type) {
     if ($this->changesets === null) {
       try {
         $this->changesets = $this->hookEngine->loadChangesetsForCommit(
           $this->log->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->hookEngine->loadCommitRefForCommit(
         $this->log->getRefNew());
     }
     return $this->commitRef;
   }
 
   private function getAuthorPHID() {
     $repository = $this->hookEngine->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->hookEngine->getViewer()->getPHID();
     }
   }
 
   private function getCommitterPHID() {
     $repository = $this->hookEngine->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 $this->getAuthorPHID();
         }
         $phid = $this->lookupUser($committer);
         if (!$phid) {
           return $this->getAuthorPHID();
         }
         return $phid;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         // In Subversion, the pusher is always the committer.
         return $this->hookEngine->getViewer()->getPHID();
     }
   }
 
   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->hookEngine->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->hookEngine->getRepository();
     $vcs = $repository->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         $parents = id(new DiffusionLowLevelParentsQuery())
           ->setRepository($repository)
           ->withIdentifier($this->log->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->hookEngine->loadBranches($this->log->getRefNew());
+  }
+
 }
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index e05b57f562..e71f60d96c 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,984 +1,987 @@
 <?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_REPOSITORY             = 'repository';
   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 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';
 
   private $contentSource;
 
   public function setContentSource(PhabricatorContentSource $content_source) {
     $this->contentSource = $content_source;
     return $this;
   }
   public function getContentSource() {
     return $this->contentSource;
   }
 
   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;
       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 getAdapterApplicationClass();
   abstract public function getObject();
 
 
 /* -(  Fields  )------------------------------------------------------------- */
 
 
   public function getFields() {
     return array(
       self::FIELD_ALWAYS,
     );
   }
 
   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_REPOSITORY => pht('Repository'),
       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'),
     );
   }
 
 
 /* -(  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:
         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:
         return array(
           self::CONDITION_IS_ANY,
           self::CONDITION_IS_NOT_ANY,
         );
       case self::FIELD_REPOSITORY:
       case self::FIELD_ASSIGNEE:
         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:
         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:
         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:
         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('Apply 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;
           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:
             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();
     }
     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;
     }
 
     asort($map);
     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);
     }
     foreach ($value as $index => $val) {
       $handle = idx($handles, $val);
       if ($handle) {
         $value[$index] = $handle->renderLink();
       }
     }
     $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) {
       $handle = idx($handles, $val);
       if ($handle) {
         $target[$index] = $handle->renderLink();
       }
     }
     $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;
         }
       }
     }
     return $phids;
   }
 
 }