diff --git a/resources/sql/autopatches/20181106.repo.02.hook.sql b/resources/sql/autopatches/20181106.repo.02.hook.sql
new file mode 100644
index 0000000000..be06923044
--- /dev/null
+++ b/resources/sql/autopatches/20181106.repo.02.hook.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_repository.repository_pushevent
+  ADD hookWait BIGINT UNSIGNED;
diff --git a/scripts/repository/commit_hook.php b/scripts/repository/commit_hook.php
index 64b7b0ec24..df49aa7b00 100755
--- a/scripts/repository/commit_hook.php
+++ b/scripts/repository/commit_hook.php
@@ -1,234 +1,237 @@
 #!/usr/bin/env php
 <?php
 
 // NOTE: This script will sometimes emit a warning like this on startup:
 //
 //   No entry for terminal type "unknown";
 //   using dumb terminal settings.
 //
 // This can be fixed by adding "TERM=dumb" to the shebang line, but doing so
 // causes some systems to hang mysteriously. See T7119.
 
 // Commit hooks execute in an unusual context where the environment may be
 // unavailable, particularly in SVN. The first parameter to this script is
 // either a bare repository identifier ("X"), or a repository identifier
 // followed by an instance identifier ("X:instance"). If we have an instance
 // identifier, unpack it into the environment before we start up. This allows
 // subclasses of PhabricatorConfigSiteSource to read it and build an instance
 // environment.
 
+$hook_start = microtime(true);
+
 if ($argc > 1) {
   $context = $argv[1];
   $context = explode(':', $context, 2);
   $argv[1] = $context[0];
 
   if (count($context) > 1) {
     $_ENV['PHABRICATOR_INSTANCE'] = $context[1];
     putenv('PHABRICATOR_INSTANCE='.$context[1]);
   }
 }
 
 $root = dirname(dirname(dirname(__FILE__)));
 require_once $root.'/scripts/__init_script__.php';
 
 if ($argc < 2) {
   throw new Exception(pht('usage: commit-hook <repository>'));
 }
 
-$engine = new DiffusionCommitHookEngine();
+$engine = id(new DiffusionCommitHookEngine())
+  ->setStartTime($hook_start);
 
 $repository = id(new PhabricatorRepositoryQuery())
   ->setViewer(PhabricatorUser::getOmnipotentUser())
   ->withIdentifiers(array($argv[1]))
   ->needProjectPHIDs(true)
   ->executeOne();
 
 if (!$repository) {
   throw new Exception(pht('No such repository "%s"!', $argv[1]));
 }
 
 if (!$repository->isHosted()) {
   // In Mercurial, the "pretxnchangegroup" hook fires for both pulls and
   // pushes. Normally we only install the hook for hosted repositories, but
   // if a hosted repository is later converted into an observed repository we
   // can end up with an observed repository that has the hook installed.
   // If we're running hooks from an observed repository, just exit without
   // taking action. For more discussion, see PHI24.
   return 0;
 }
 
 $engine->setRepository($repository);
 
 $args = new PhutilArgumentParser($argv);
 $args->parsePartial(
   array(
     array(
       'name' => 'hook-mode',
       'param' => 'mode',
       'help' => pht('Hook execution mode.'),
     ),
   ));
 
 $argv = array_merge(
   array($argv[0]),
   $args->getUnconsumedArgumentVector());
 
 // Figure out which user is writing the commit.
 $hook_mode = $args->getArg('hook-mode');
 if ($hook_mode !== null) {
   $known_modes = array(
     'svn-revprop' => true,
   );
 
   if (empty($known_modes[$hook_mode])) {
     throw new Exception(
       pht(
         'Invalid Hook Mode: This hook was invoked in "%s" mode, but this '.
         'is not a recognized hook mode. Valid modes are: %s.',
         $hook_mode,
         implode(', ', array_keys($known_modes))));
   }
 }
 
 $is_svnrevprop = ($hook_mode == 'svn-revprop');
 
 if ($is_svnrevprop) {
   // For now, we let these through if the repository allows dangerous changes
   // and prevent them if it doesn't. See T11208 for discussion.
 
   $revprop_key = $argv[5];
 
   if ($repository->shouldAllowDangerousChanges()) {
     $err = 0;
   } else {
     $err = 1;
 
     $console = PhutilConsole::getConsole();
     $console->writeErr(
       pht(
         "DANGEROUS CHANGE: Dangerous change protection is enabled for this ".
         "repository, so you can not change revision properties (you are ".
         "attempting to edit \"%s\").\n".
         "Edit the repository configuration before making dangerous changes.",
         $revprop_key));
   }
 
   exit($err);
 } else if ($repository->isGit() || $repository->isHg()) {
   $username = getenv(DiffusionCommitHookEngine::ENV_USER);
   if (!strlen($username)) {
     throw new Exception(
       pht(
         'No Direct Pushes: You are pushing directly to a repository hosted '.
         'by Phabricator. This will not work. See "No Direct Pushes" in the '.
         'documentation for more information.'));
   }
 
   if ($repository->isHg()) {
     // We respond to several different hooks in Mercurial.
     $engine->setMercurialHook($argv[2]);
   }
 
 } else if ($repository->isSVN()) {
   // NOTE: In Subversion, the entire environment gets wiped so we can't read
   // DiffusionCommitHookEngine::ENV_USER. Instead, we've set "--tunnel-user" to
   // specify the correct user; read this user out of the commit log.
 
   if ($argc < 4) {
     throw new Exception(pht('usage: commit-hook <repository> <repo> <txn>'));
   }
 
   $svn_repo = $argv[2];
   $svn_txn = $argv[3];
   list($username) = execx('svnlook author -t %s %s', $svn_txn, $svn_repo);
   $username = rtrim($username, "\n");
 
   $engine->setSubversionTransactionInfo($svn_txn, $svn_repo);
 } else {
   throw new Exception(pht('Unknown repository type.'));
 }
 
 $user = id(new PhabricatorPeopleQuery())
   ->setViewer(PhabricatorUser::getOmnipotentUser())
   ->withUsernames(array($username))
   ->executeOne();
 
 if (!$user) {
   throw new Exception(pht('No such user "%s"!', $username));
 }
 
 $engine->setViewer($user);
 
 
 // Read stdin for the hook engine.
 
 if ($repository->isHg()) {
   // Mercurial leaves stdin open, so we can't just read it until EOF.
   $stdin = '';
 } else {
   // Git and Subversion write data into stdin and then close it. Read the
   // data.
   $stdin = @file_get_contents('php://stdin');
   if ($stdin === false) {
     throw new Exception(pht('Failed to read stdin!'));
   }
 }
 
 $engine->setStdin($stdin);
 $engine->setOriginalArgv(array_slice($argv, 2));
 
 $remote_address = getenv(DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS);
 if (strlen($remote_address)) {
   $engine->setRemoteAddress($remote_address);
 }
 
 $remote_protocol = getenv(DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL);
 if (strlen($remote_protocol)) {
   $engine->setRemoteProtocol($remote_protocol);
 }
 
 $request_identifier = getenv(DiffusionCommitHookEngine::ENV_REQUEST);
 if (strlen($request_identifier)) {
   $engine->setRequestIdentifier($request_identifier);
 }
 
 try {
   $err = $engine->execute();
 } catch (DiffusionCommitHookRejectException $ex) {
   $console = PhutilConsole::getConsole();
 
   if (PhabricatorEnv::getEnvConfig('phabricator.serious-business')) {
     $preamble = pht('*** PUSH REJECTED BY COMMIT HOOK ***');
   } else {
     $preamble = pht(<<<EOTXT
 +---------------------------------------------------------------+
 |      * * * PUSH REJECTED BY EVIL DRAGON BUREAUCRATS * * *     |
 +---------------------------------------------------------------+
              \
               \                    ^    /^
                \                  / \  // \
                 \   |\___/|      /   \//  .\
                  \  /V  V  \__  /    //  | \ \           *----*
                    /     /  \/_/    //   |  \  \          \   |
                    @___@`    \/_   //    |   \   \         \/\ \
                   0/0/|       \/_ //     |    \    \         \  \
               0/0/0/0/|        \///      |     \     \       |  |
            0/0/0/0/0/_|_ /   (  //       |      \     _\     |  /
         0/0/0/0/0/0/`/,_ _ _/  ) ; -.    |    _ _\.-~       /   /
                     ,-}        _      *-.|.-~-.           .~    ~
   *     \__/         `/\      /                 ~-. _ .-~      /
    \____(Oo)            *.   }            {                   /
    (    (..)           .----~-.\        \-`                 .~
    //___\\\\  \ DENIED!  ///.----..<        \             _ -~
   //     \\\\                ///-._ _ _ _ _ _ _{^ - - - - ~
 
 EOTXT
 );
   }
 
   $console->writeErr("%s\n\n", $preamble);
   $console->writeErr("%s\n\n", $ex->getMessage());
   $err = 1;
 }
 
 exit($err);
diff --git a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
index 59fb4b5e12..d22635e859 100644
--- a/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
+++ b/src/applications/diffusion/engine/DiffusionCommitHookEngine.php
@@ -1,1363 +1,1377 @@
 <?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_REPOSITORY = 'PHABRICATOR_REPOSITORY';
   const ENV_USER = 'PHABRICATOR_USER';
   const ENV_REQUEST = 'PHABRICATOR_REQUEST';
   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 $originalArgv;
   private $subversionTransaction;
   private $subversionRepository;
   private $remoteAddress;
   private $remoteProtocol;
   private $requestIdentifier;
   private $transactionKey;
   private $mercurialHook;
   private $mercurialCommits = array();
   private $gitCommits = array();
+  private $startTime;
 
   private $heraldViewerProjects;
   private $rejectCode = PhabricatorRepositoryPushLog::REJECT_BROKEN;
   private $rejectDetails;
   private $emailPHIDs = array();
   private $changesets = array();
   private $changesetsSize = 0;
 
 
 /* -(  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;
   }
 
   public function setRequestIdentifier($request_identifier) {
     $this->requestIdentifier = $request_identifier;
     return $this;
   }
 
   public function getRequestIdentifier() {
     return $this->requestIdentifier;
   }
 
+  public function setStartTime($start_time) {
+    $this->startTime = $start_time;
+    return $this;
+  }
+
+  public function getStartTime() {
+    return $this->startTime;
+  }
+
   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 setOriginalArgv(array $original_argv) {
     $this->originalArgv = $original_argv;
     return $this;
   }
 
   public function getOriginalArgv() {
     return $this->originalArgv;
   }
 
   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.
         $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_DANGEROUS;
         throw $ex;
       }
 
       $content_updates = $this->findContentUpdates($ref_updates);
       $all_updates = array_merge($ref_updates, $content_updates);
 
       // If this is an "initial import" (a sizable push to a previously empty
       // repository) we'll allow enormous changes and disable Herald rules.
       // These rulesets can consume a large amount of time and memory and are
       // generally not relevant when importing repository history.
       $is_initial_import = $this->isInitialImport($all_updates);
 
       if (!$is_initial_import) {
         $this->applyHeraldRefRules($ref_updates);
       }
 
       try {
         if (!$is_initial_import) {
           $this->rejectEnormousChanges($content_updates);
         }
       } catch (DiffusionCommitHookRejectException $ex) {
         // If we're rejecting enormous changes, flag everything.
         $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_ENORMOUS;
         throw $ex;
       }
 
       if (!$is_initial_import) {
         $this->applyHeraldContentRules($content_updates);
       }
 
       // Run custom scripts in `hook.d/` directories.
       $this->applyCustomHooks($all_updates);
 
       // If we make it this far, we're accepting these changes. Mark all the
       // logs as accepted.
       $this->rejectCode = 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.
     $event = $this->newPushEvent();
 
     $event->setRejectCode($this->rejectCode);
     $event->setRejectDetails($this->rejectDetails);
 
     $event->openTransaction();
       $event->save();
       foreach ($all_updates as $update) {
         $update->setPushEventPHID($event->getPHID());
         $update->save();
       }
     $event->saveTransaction();
 
     if ($caught) {
       throw $caught;
     }
 
     // If this went through cleanly and was an import, set the importing flag
     // on the repository. It will be cleared once we fully process everything.
 
     if ($is_initial_import) {
       $repository = $this->getRepository();
       $repository->markImporting();
     }
 
     if ($this->emailPHIDs) {
       // If Herald rules triggered email to users, queue a worker to send the
       // mail. We do this out-of-process so that we block pushes as briefly
       // as possible.
 
       // (We do need to pull some commit info here because the commit objects
       // may not exist yet when this worker runs, which could be immediately.)
 
       PhabricatorWorker::scheduleTask(
         'PhabricatorRepositoryPushMailWorker',
         array(
           'eventPHID' => $event->getPHID(),
           'emailPHIDs' => array_values($this->emailPHIDs),
           'info' => $this->loadCommitInfoForWorker($all_updates),
         ),
         array(
           'priority' => PhabricatorWorker::PRIORITY_ALERTS,
         ));
     }
 
     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) {
     $this->applyHeraldRules(
       $ref_updates,
       new HeraldPreCommitRefAdapter());
   }
 
   private function applyHeraldContentRules(array $content_updates) {
     $this->applyHeraldRules(
       $content_updates,
       new HeraldPreCommitContentAdapter());
   }
 
   private function applyHeraldRules(
     array $updates,
     HeraldAdapter $adapter_template) {
 
     if (!$updates) {
       return;
     }
 
     $viewer = $this->getViewer();
 
     $adapter_template
       ->setHookEngine($this)
       ->setActingAsPHID($viewer->getPHID());
 
     $engine = new HeraldEngine();
     $rules = null;
     $blocking_effect = null;
     $blocked_update = null;
     $blocking_xscript = 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();
 
       // Store any PHIDs we want to send email to for later.
       foreach ($adapter->getEmailPHIDs() as $email_phid) {
         $this->emailPHIDs[$email_phid] = $email_phid;
       }
 
       $block_action = DiffusionBlockHeraldAction::ACTIONCONST;
 
       if ($blocking_effect === null) {
         foreach ($effects as $effect) {
           if ($effect->getAction() == $block_action) {
             $blocking_effect = $effect;
             $blocked_update = $update;
             $blocking_xscript = $xscript;
             break;
           }
         }
       }
     }
 
     if ($blocking_effect) {
       $rule = $blocking_effect->getRule();
 
       $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_HERALD;
       $this->rejectDetails = $rule->getPHID();
 
       $message = $blocking_effect->getTarget();
       if (!strlen($message)) {
         $message = pht('(None.)');
       }
 
       $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\n".
           "Transcript: %s",
           $rule->getMonogram(),
           $blocked_name,
           $rule->getName(),
           $message,
           PhabricatorEnv::getProductionURI(
             '/herald/transcript/'.$blocking_xscript->getID().'/')));
     }
   }
 
   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);
     }
 
     $futures = id(new FutureIterator($futures))
       ->limit(8);
     foreach ($futures 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_new === self::EMPTY_HASH)) {
         // This happens if you try to delete a tag or branch which does not
         // exist by pushing directly to the ref. Git will warn about it but
         // allow it. Just call it a delete, without flagging it as dangerous.
         $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
       } else 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();
     $futures = id(new FutureIterator($futures))
       ->limit(8);
     foreach ($futures 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;
   }
 
 /* -(  Custom  )------------------------------------------------------------- */
 
   private function applyCustomHooks(array $updates) {
     $args = $this->getOriginalArgv();
     $stdin = $this->getStdin();
     $console = PhutilConsole::getConsole();
 
     $env = array(
       self::ENV_REPOSITORY => $this->getRepository()->getPHID(),
       self::ENV_USER => $this->getViewer()->getUsername(),
       self::ENV_REQUEST => $this->getRequestIdentifier(),
       self::ENV_REMOTE_PROTOCOL => $this->getRemoteProtocol(),
       self::ENV_REMOTE_ADDRESS => $this->getRemoteAddress(),
     );
 
     $repository = $this->getRepository();
 
     $env += $repository->getPassthroughEnvironmentalVariables();
 
     $directories = $repository->getHookDirectories();
     foreach ($directories as $directory) {
       $hooks = $this->getExecutablesInDirectory($directory);
       sort($hooks);
       foreach ($hooks as $hook) {
         // NOTE: We're explicitly running the hooks in sequential order to
         // make this more predictable.
         $future = id(new ExecFuture('%s %Ls', $hook, $args))
           ->setEnv($env, $wipe_process_env = false)
           ->write($stdin);
 
         list($err, $stdout, $stderr) = $future->resolve();
         if (!$err) {
           // This hook ran OK, but echo its output in case there was something
           // informative.
           $console->writeOut('%s', $stdout);
           $console->writeErr('%s', $stderr);
           continue;
         }
 
         $this->rejectCode = PhabricatorRepositoryPushLog::REJECT_EXTERNAL;
         $this->rejectDetails = basename($hook);
 
         throw new DiffusionCommitHookRejectException(
           pht(
             "This push was rejected by custom hook script '%s':\n\n%s%s",
             basename($hook),
             $stdout,
             $stderr));
       }
     }
   }
 
   private function getExecutablesInDirectory($directory) {
     $executables = array();
 
     if (!Filesystem::pathExists($directory)) {
       return $executables;
     }
 
     foreach (Filesystem::listDirectory($directory) as $path) {
       $full_path = $directory.DIRECTORY_SEPARATOR.$path;
       if (!is_executable($full_path)) {
         // Don't include non-executable files.
         continue;
       }
 
       if (basename($full_path) == 'README') {
         // Don't include README, even if it is marked as executable. It almost
         // certainly got caught in the crossfire of a sweeping `chmod`, since
         // users do this with some frequency.
         continue;
       }
 
       $executables[] = $full_path;
     }
 
     return $executables;
   }
 
 
 /* -(  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 %s in environment!',
           'HG_NODE'));
     }
 
     // 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{branch}\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 --template %s',
       hgsprintf('%s:%s', $hg_node, 'tip'),
       '{node}\1{branch}\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 (new FutureIterator($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) {
         // This should never be possible, 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.',
             $ref));
       }
 
       if ($old_heads === $new_heads) {
         // No changes to this branch, so skip it.
         continue;
       }
 
       $stray_heads = array();
       $head_map = array();
 
       if ($old_heads && !$new_heads) {
         // This is a branch deletion with "--close-branch".
         foreach ($old_heads as $old_head) {
           $head_map[$old_head] = array(self::EMPTY_HASH);
         }
       } else 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.
 
         // NOTE: We're only looking for heads on the same branch. The old
         // tip of the branch may be the branchpoint for other branches, but that
         // is OK.
 
         $dfutures = array();
         foreach ($old_heads as $old_head) {
           $dfutures[$old_head] = $repository->getLocalCommandFuture(
             'log --branch %s --rev %s --template %s',
             $ref,
             hgsprintf('(descendants(%s) and head())', $old_head),
             '{node}\1');
         }
 
         foreach (new FutureIterator($dfutures) as $future_head => $dfuture) {
           list($stdout) = $dfuture->resolvex();
           $descendant_heads = array_filter(explode("\1", $stdout));
           if ($descendant_heads) {
             // This old head has at least one descendant in the push.
             $head_map[$future_head] = $descendant_heads;
           } else {
             // This old head has no descendants, so it is being deleted.
             $head_map[$future_head] = array(self::EMPTY_HASH);
           }
         }
 
         // 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 ($new_head === self::EMPTY_HASH) {
             // If a branch head is being deleted, don't insert it as an add.
             continue;
           }
           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;
           }
 
 
           $deletes_existing_head = ($new_head == self::EMPTY_HASH);
           $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));
             }
           }
 
           if ($deletes_existing_head) {
             // TODO: Somewhere in here we should be setting CHANGEFLAG_REWRITE
             // if we are also creating at least one other head to replace
             // this one.
 
             // NOTE: In Git, this is a dangerous change, but it is not dangerous
             // in Mercurial. Mercurial branches are version controlled, and
             // Mercurial does not prompt you for any special flags when pushing
             // a `--close-branch` commit by default.
 
             $ref_flags |= PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE;
           }
 
           $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, $branch) = explode("\1", $commit_line);
       $commit_map[$node] = array($branch);
     }
 
     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: We generate PHIDs up front so the Herald transcripts can pick them
     // up.
     $phid = id(new PhabricatorRepositoryPushLog())->generatePHID();
 
     $device = AlmanacKeys::getLiveDevice();
     if ($device) {
       $device_phid = $device->getPHID();
     } else {
       $device_phid = null;
     }
 
     return PhabricatorRepositoryPushLog::initializeNewLog($this->getViewer())
       ->setPHID($phid)
       ->setDevicePHID($device_phid)
       ->setRepositoryPHID($this->getRepository()->getPHID())
       ->attachRepository($this->getRepository())
       ->setEpoch(PhabricatorTime::getNow());
   }
 
   private function newPushEvent() {
     $viewer = $this->getViewer();
 
+    $hook_start = $this->getStartTime();
+    $hook_end = microtime(true);
+
     $event = PhabricatorRepositoryPushEvent::initializeNewEvent($viewer)
       ->setRepositoryPHID($this->getRepository()->getPHID())
       ->setRemoteAddress($this->getRemoteAddress())
       ->setRemoteProtocol($this->getRemoteProtocol())
-      ->setEpoch(PhabricatorTime::getNow());
+      ->setEpoch(PhabricatorTime::getNow())
+      ->setHookWait((int)(1000000 * ($hook_end - $hook_start)));
 
     $identifier = $this->getRequestIdentifier();
     if (strlen($identifier)) {
       $event->setRequestIdentifier($identifier);
     }
 
     return $event;
   }
 
   private function rejectEnormousChanges(array $content_updates) {
     $repository = $this->getRepository();
     if ($repository->shouldAllowEnormousChanges()) {
       return;
     }
 
     // See T13142. Don't cache more than 64MB of changesets. For normal small
     // pushes, caching everything here can let us hit the cache from Herald if
     // we need to run content rules, which speeds things up a bit. For large
     // pushes, we may not be able to hold everything in memory.
     $cache_limit = 1024 * 1024 * 64;
 
     foreach ($content_updates as $update) {
       $identifier = $update->getRefNew();
       try {
         $info = $this->loadChangesetsForCommit($identifier);
         list($changesets, $size) = $info;
 
         if ($this->changesetsSize + $size <= $cache_limit) {
           $this->changesets[$identifier] = $changesets;
           $this->changesetsSize += $size;
         }
       } catch (Exception $ex) {
         $this->changesets[$identifier] = $ex;
 
         $message = pht(
           'ENORMOUS CHANGE'.
           "\n".
           'Enormous change protection is enabled for this repository, but '.
           'you are pushing an enormous change ("%s"). Edit the repository '.
           'configuration before making enormous changes.'.
           "\n\n".
           "Content Exception: %s",
           $identifier,
           $ex->getMessage());
 
         throw new DiffusionCommitHookRejectException($message);
       }
     }
   }
 
   private function loadChangesetsForCommit($identifier) {
     $byte_limit = HeraldCommitAdapter::getEnormousByteLimit();
     $time_limit = HeraldCommitAdapter::getEnormousTimeLimit();
 
     $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($time_limit)
           ->setByteLimit($byte_limit)
           ->setLinesOfContext(0)
           ->executeInline();
         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`.
         $future = new ExecFuture(
           'svnlook diff -t %s %s',
           $this->subversionTransaction,
           $this->subversionRepository);
 
         $future->setTimeout($time_limit);
         $future->setStdoutSizeLimit($byte_limit);
         $future->setStderrSizeLimit($byte_limit);
 
         list($raw_diff) = $future->resolvex();
         break;
       default:
         throw new Exception(pht("Unknown VCS '%s!'", $vcs));
     }
 
     if (strlen($raw_diff) >= $byte_limit) {
       throw new Exception(
         pht(
           'The raw text of this change ("%s") is enormous (larger than %s '.
           'bytes).',
           $identifier,
           new PhutilNumber($byte_limit)));
     }
 
     if (!strlen($raw_diff)) {
       // If the commit is actually empty, just return no changesets.
       return array(array(), 0);
     }
 
     $parser = new ArcanistDiffParser();
     $changes = $parser->parseDiff($raw_diff);
     $diff = DifferentialDiff::newEphemeralFromRawChanges(
       $changes);
 
     $changesets = $diff->getChangesets();
     $size = strlen($raw_diff);
 
     return array($changesets, $size);
   }
 
   public function getChangesetsForCommit($identifier) {
     if (isset($this->changesets[$identifier])) {
       $cached = $this->changesets[$identifier];
 
       if ($cached instanceof Exception) {
         throw $cached;
       }
 
       return $cached;
     }
 
     $info = $this->loadChangesetsForCommit($identifier);
     list($changesets, $size) = $info;
     return $changesets;
   }
 
   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:
         // NOTE: This will be "the branch the commit was made to", not
         // "a list of all branch heads which descend from the commit".
         // This is consistent with Mercurial, but possibly confusing.
         return idx($this->mercurialCommits, $identifier, array());
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         // Subversion doesn't have branches.
         return array();
     }
   }
 
   private function loadCommitInfoForWorker(array $all_updates) {
     $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT;
 
     $map = array();
     foreach ($all_updates as $update) {
       if ($update->getRefType() != $type_commit) {
         continue;
       }
       $map[$update->getRefNew()] = array();
     }
 
     foreach ($map as $identifier => $info) {
       $ref = $this->loadCommitRefForCommit($identifier);
       $map[$identifier] += array(
         'summary'  => $ref->getSummary(),
         'branches' => $this->loadBranches($identifier),
       );
     }
 
     return $map;
   }
 
   private function isInitialImport(array $all_updates) {
     $repository = $this->getRepository();
 
     $vcs = $repository->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         // There is no meaningful way to import history into Subversion by
         // pushing.
         return false;
       default:
         break;
     }
 
     // Now, apply a heuristic to guess whether this is a normal commit or
     // an initial import. We guess something is an initial import if:
     //
     //   - the repository is currently empty; and
     //   - it pushes more than 7 commits at once.
     //
     // The number "7" is chosen arbitrarily as seeming reasonable. We could
     // also look at author data (do the commits come from multiple different
     // authors?) and commit date data (is the oldest commit more than 48 hours
     // old), but we don't have immediate access to those and this simple
     // heuristic might be good enough.
 
     $commit_count = 0;
     $type_commit = PhabricatorRepositoryPushLog::REFTYPE_COMMIT;
     foreach ($all_updates as $update) {
       if ($update->getRefType() != $type_commit) {
         continue;
       }
       $commit_count++;
     }
 
     if ($commit_count <= PhabricatorRepository::IMPORT_THRESHOLD) {
       // If this pushes a very small number of commits, assume it's an
       // initial commit or stack of a few initial commits.
       return false;
     }
 
     $any_commits = id(new DiffusionCommitQuery())
       ->setViewer($this->getViewer())
       ->withRepository($repository)
       ->setLimit(1)
       ->execute();
 
     if ($any_commits) {
       // If the repository already has commits, this isn't an import.
       return false;
     }
 
     return true;
   }
 
 }
diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php
index 682b367926..ac97aa2bcf 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryPushEvent.php
@@ -1,106 +1,108 @@
 <?php
 
 /**
  * Groups a set of push logs corresponding to changes which were all pushed in
  * the same transaction.
  */
 final class PhabricatorRepositoryPushEvent
   extends PhabricatorRepositoryDAO
   implements PhabricatorPolicyInterface {
 
   protected $repositoryPHID;
   protected $epoch;
   protected $pusherPHID;
   protected $requestIdentifier;
   protected $remoteAddress;
   protected $remoteProtocol;
   protected $rejectCode;
   protected $rejectDetails;
   protected $writeWait;
   protected $readWait;
   protected $hostWait;
+  protected $hookWait;
 
   private $repository = self::ATTACHABLE;
   private $logs = self::ATTACHABLE;
 
   public static function initializeNewEvent(PhabricatorUser $viewer) {
     return id(new PhabricatorRepositoryPushEvent())
       ->setPusherPHID($viewer->getPHID());
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_TIMESTAMPS => false,
       self::CONFIG_COLUMN_SCHEMA => array(
         'requestIdentifier' => 'bytes12?',
         'remoteAddress' => 'ipaddress?',
         'remoteProtocol' => 'text32?',
         'rejectCode' => 'uint32',
         'rejectDetails' => 'text64?',
         'writeWait' => 'uint64?',
         'readWait' => 'uint64?',
         'hostWait' => 'uint64?',
+        'hookWait' => 'uint64?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_repository' => array(
           'columns' => array('repositoryPHID'),
         ),
         'key_identifier' => array(
           'columns' => array('requestIdentifier'),
         ),
         'key_reject' => array(
           'columns' => array('rejectCode', 'rejectDetails'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorRepositoryPushEventPHIDType::TYPECONST);
   }
 
   public function attachRepository(PhabricatorRepository $repository) {
     $this->repository = $repository;
     return $this;
   }
 
   public function getRepository() {
     return $this->assertAttached($this->repository);
   }
 
   public function attachLogs(array $logs) {
     $this->logs = $logs;
     return $this;
   }
 
   public function getLogs() {
     return $this->assertAttached($this->logs);
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     return $this->getRepository()->getPolicy($capability);
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return $this->getRepository()->hasAutomaticCapability($capability, $viewer);
   }
 
   public function describeAutomaticCapability($capability) {
     return pht(
       "A repository's push events are visible to users who can see the ".
       "repository.");
   }
 
 }