diff --git a/src/applications/audit/application/PhabricatorAuditApplication.php b/src/applications/audit/application/PhabricatorAuditApplication.php
index 7d8ad94aa7..ee5cbe7781 100644
--- a/src/applications/audit/application/PhabricatorAuditApplication.php
+++ b/src/applications/audit/application/PhabricatorAuditApplication.php
@@ -1,37 +1,35 @@
 <?php
 
 final class PhabricatorAuditApplication extends PhabricatorApplication {
 
   public function getBaseURI() {
     return '/diffusion/commit/';
   }
 
   public function getIcon() {
     return 'fa-check-circle-o';
   }
 
   public function getName() {
     return pht('Audit');
   }
 
   public function getShortDescription() {
     return pht('Browse and Audit Commits');
   }
 
   public function canUninstall() {
-    // Audit was once a separate application, but has largely merged with
-    // Diffusion.
-    return false;
+    return true;
   }
 
   public function isPinnedByDefault(PhabricatorUser $viewer) {
     return parent::isClassInstalledForViewer(
       'PhabricatorDiffusionApplication',
       $viewer);
   }
 
   public function getApplicationOrder() {
     return 0.130;
   }
 
 }
diff --git a/src/applications/audit/conduit/AuditConduitAPIMethod.php b/src/applications/audit/conduit/AuditConduitAPIMethod.php
index a5a7957b1f..4dc9eb103e 100644
--- a/src/applications/audit/conduit/AuditConduitAPIMethod.php
+++ b/src/applications/audit/conduit/AuditConduitAPIMethod.php
@@ -1,10 +1,10 @@
 <?php
 
 abstract class AuditConduitAPIMethod extends ConduitAPIMethod {
 
   final public function getApplication() {
     return PhabricatorApplication::getByClass(
-      'PhabricatorDiffusionApplication');
+      'PhabricatorAuditApplication');
   }
 
 }
diff --git a/src/applications/audit/query/PhabricatorCommitSearchEngine.php b/src/applications/audit/query/PhabricatorCommitSearchEngine.php
index a6a2ee071e..dfc038c197 100644
--- a/src/applications/audit/query/PhabricatorCommitSearchEngine.php
+++ b/src/applications/audit/query/PhabricatorCommitSearchEngine.php
@@ -1,280 +1,289 @@
 <?php
 
 final class PhabricatorCommitSearchEngine
   extends PhabricatorApplicationSearchEngine {
 
   public function getResultTypeDescription() {
     return pht('Diffusion Commits');
   }
 
   public function getApplicationClassName() {
     return PhabricatorDiffusionApplication::class;
   }
 
   public function newQuery() {
     return id(new DiffusionCommitQuery())
       ->needAuditRequests(true)
       ->needCommitData(true)
       ->needIdentities(true)
       ->needDrafts(true);
   }
 
   protected function newResultBuckets() {
     return DiffusionCommitResultBucket::getAllResultBuckets();
   }
 
   protected function buildQueryFromParameters(array $map) {
     $query = $this->newQuery();
 
     if ($map['responsiblePHIDs']) {
       $query->withResponsiblePHIDs($map['responsiblePHIDs']);
     }
 
     if ($map['auditorPHIDs']) {
       $query->withAuditorPHIDs($map['auditorPHIDs']);
     }
 
     if ($map['authorPHIDs']) {
       $query->withAuthorPHIDs($map['authorPHIDs']);
     }
 
     if ($map['statuses']) {
       $query->withStatuses($map['statuses']);
     }
 
     if ($map['repositoryPHIDs']) {
       $query->withRepositoryPHIDs($map['repositoryPHIDs']);
     }
 
     if ($map['packagePHIDs']) {
       $query->withPackagePHIDs($map['packagePHIDs']);
     }
 
     if ($map['unreachable'] !== null) {
       $query->withUnreachable($map['unreachable']);
     }
 
     if ($map['permanent'] !== null) {
       $query->withPermanent($map['permanent']);
     }
 
     if ($map['ancestorsOf']) {
       $query->withAncestorsOf($map['ancestorsOf']);
     }
 
     if ($map['identifiers']) {
       $query->withIdentifiers($map['identifiers']);
     }
 
     return $query;
   }
 
   protected function buildCustomSearchFields() {
+    $show_audit_fields = (id(new PhabricatorAuditApplication())->isInstalled());
     $show_packages = PhabricatorApplication::isClassInstalled(
       'PhabricatorPackagesApplication');
     return array(
       id(new PhabricatorSearchDatasourceField())
         ->setLabel(pht('Responsible Users'))
         ->setKey('responsiblePHIDs')
         ->setConduitKey('responsible')
         ->setAliases(array('responsible', 'responsibles', 'responsiblePHID'))
         ->setDatasource(new DifferentialResponsibleDatasource())
         ->setDescription(
           pht(
             'Find commits where given users, projects, or packages are '.
             'responsible for the next steps in the audit workflow.')),
       id(new PhabricatorUsersSearchField())
         ->setLabel(pht('Authors'))
         ->setKey('authorPHIDs')
         ->setConduitKey('authors')
         ->setAliases(array('author', 'authors', 'authorPHID'))
         ->setDescription(pht('Find commits authored by particular users.')),
       id(new PhabricatorSearchDatasourceField())
         ->setLabel(pht('Auditors'))
         ->setKey('auditorPHIDs')
         ->setConduitKey('auditors')
         ->setAliases(array('auditor', 'auditors', 'auditorPHID'))
         ->setDatasource(new DiffusionAuditorFunctionDatasource())
+        ->setIsHidden(!$show_audit_fields)
         ->setDescription(
           pht(
             'Find commits where given users, projects, or packages are '.
             'auditors.')),
       id(new PhabricatorSearchCheckboxesField())
         ->setLabel(pht('Audit Status'))
         ->setKey('statuses')
         ->setAliases(array('status'))
         ->setOptions(DiffusionCommitAuditStatus::newOptions())
         ->setDeprecatedOptions(
           DiffusionCommitAuditStatus::newDeprecatedOptions())
+        ->setIsHidden(!$show_audit_fields)
         ->setDescription(pht('Find commits with given audit statuses.')),
       id(new PhabricatorSearchDatasourceField())
         ->setLabel(pht('Repositories'))
         ->setKey('repositoryPHIDs')
         ->setConduitKey('repositories')
         ->setAliases(array('repository', 'repositories', 'repositoryPHID'))
         ->setDatasource(new DiffusionRepositoryFunctionDatasource())
         ->setDescription(pht('Find commits in particular repositories.')),
       id(new PhabricatorSearchDatasourceField())
         ->setLabel(pht('Packages'))
         ->setKey('packagePHIDs')
         ->setConduitKey('packages')
         ->setAliases(array('package', 'packages', 'packagePHID'))
         ->setDatasource(new PhabricatorOwnersPackageDatasource())
         ->setIsHidden(!$show_packages)
         ->setDescription(
           pht('Find commits which affect given packages.')),
       id(new PhabricatorSearchThreeStateField())
         ->setLabel(pht('Unreachable'))
         ->setKey('unreachable')
         ->setOptions(
           pht('(Show All)'),
           pht('Show Only Unreachable Commits'),
           pht('Hide Unreachable Commits'))
         ->setDescription(
           pht(
             'Find or exclude unreachable commits which are not ancestors of '.
             'any branch, tag, or ref.')),
       id(new PhabricatorSearchThreeStateField())
         ->setLabel(pht('Permanent'))
         ->setKey('permanent')
         ->setOptions(
           pht('(Show All)'),
           pht('Show Only Permanent Commits'),
           pht('Hide Permanent Commits'))
         ->setDescription(
           pht(
             'Find or exclude permanent commits which are ancestors of '.
             'any permanent branch, tag, or ref.')),
       id(new PhabricatorSearchStringListField())
         ->setLabel(pht('Ancestors Of'))
         ->setKey('ancestorsOf')
         ->setDescription(
           pht(
             'Find commits which are ancestors of a particular ref, '.
             'like "master".')),
       id(new PhabricatorSearchStringListField())
         ->setLabel(pht('Identifiers'))
         ->setKey('identifiers')
         ->setDescription(
           pht(
             'Find commits with particular identifiers (usually, hashes). '.
             'Supports full or partial identifiers (like "abcd12340987..." or '.
             '"abcd1234") and qualified or unqualified identifiers (like '.
             '"rXabcd1234" or "abcd1234").')),
     );
   }
 
   protected function getURI($path) {
     return '/diffusion/commit/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array();
 
     if ($this->requireViewer()->isLoggedIn()) {
-      $names['active'] = pht('Active Audits');
+      if (id(new PhabricatorAuditApplication())->isInstalled()) {
+        $names['active'] = pht('Active Audits');
+      }
       $names['authored'] = pht('Authored');
-      $names['audited'] = pht('Audited');
+      if (id(new PhabricatorAuditApplication())->isInstalled()) {
+        $names['audited'] = pht('Audited');
+      }
     }
 
     $names['all'] = pht('All Commits');
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
     $viewer = $this->requireViewer();
 
     $viewer_phid = $viewer->getPHID();
     switch ($query_key) {
       case 'all':
         return $query;
       case 'active':
         $bucket_key = DiffusionCommitRequiredActionResultBucket::BUCKETKEY;
 
         $open = DiffusionCommitAuditStatus::getOpenStatusConstants();
 
         $query
           ->setParameter('responsiblePHIDs', array($viewer_phid))
           ->setParameter('statuses', $open)
           ->setParameter('bucket', $bucket_key)
           ->setParameter('unreachable', false);
         return $query;
       case 'authored':
         $query
           ->setParameter('authorPHIDs', array($viewer_phid));
         return $query;
       case 'audited':
         $query
           ->setParameter('auditorPHIDs', array($viewer_phid));
         return $query;
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   protected function renderResultList(
     array $commits,
     PhabricatorSavedQuery $query,
     array $handles) {
     assert_instances_of($commits, 'PhabricatorRepositoryCommit');
     $viewer = $this->requireViewer();
 
     $bucket = $this->getResultBucket($query);
 
+    // hide "Auditors" on /diffusion/commit/query/all/ if Audit not installed
+    $show_auditors = id(new PhabricatorAuditApplication())->isInstalled();
     $template = id(new DiffusionCommitGraphView())
       ->setViewer($viewer)
-      ->setShowAuditors(true);
+      ->setShowAuditors($show_auditors);
 
     $views = array();
     if ($bucket) {
       $bucket->setViewer($viewer);
 
       try {
         $groups = $bucket->newResultGroups($query, $commits);
 
         foreach ($groups as $group) {
           // Don't show groups in Dashboard Panels
           if ($group->getObjects() || !$this->isPanelContext()) {
             $item_list = id(clone $template)
               ->setCommits($group->getObjects())
               ->newObjectItemListView();
 
             $views[] = $item_list
               ->setHeader($group->getName())
               ->setNoDataString($group->getNoDataString());
           }
         }
       } catch (Exception $ex) {
         $this->addError($ex->getMessage());
       }
     }
 
     if (!$views) {
       $item_list = id(clone $template)
         ->setCommits($commits)
         ->newObjectItemListView();
 
       $views[] = $item_list
         ->setNoDataString(pht('No commits found.'));
     }
 
     return id(new PhabricatorApplicationSearchResultView())
       ->setContent($views);
   }
 
   protected function getNewUserBody() {
 
     $view = id(new PHUIBigInfoView())
       ->setIcon('fa-check-circle-o')
       ->setTitle(pht('Welcome to Audit'))
       ->setDescription(
         pht('Post-commit code review and auditing. Audits you are assigned '.
             'to will appear here.'));
 
       return $view;
   }
 
 }
diff --git a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php
index 02213cceee..c807d4e8c4 100644
--- a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php
+++ b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php
@@ -1,169 +1,172 @@
 <?php
 
 final class DiffusionCommitEditEngine
   extends PhabricatorEditEngine {
 
   const ENGINECONST = 'diffusion.commit';
 
   const ACTIONGROUP_AUDIT = 'audit';
   const ACTIONGROUP_COMMIT = 'commit';
 
   public function isEngineConfigurable() {
     return false;
   }
 
   public function getEngineName() {
     return pht('Commits');
   }
 
   public function getSummaryHeader() {
     return pht('Edit Commits');
   }
 
   public function getSummaryText() {
     return pht('Edit commits.');
   }
 
   public function getEngineApplicationClass() {
     return PhabricatorDiffusionApplication::class;
   }
 
   protected function newEditableObject() {
     // NOTE: We must return a valid object here so that things like Conduit
     // documentation generation work. You can't actually create commits via
     // EditEngine. This is enforced with a "No One" creation policy.
 
     $repository = new PhabricatorRepository();
     $data = new PhabricatorRepositoryCommitData();
 
     return id(new PhabricatorRepositoryCommit())
       ->attachRepository($repository)
       ->attachCommitData($data)
       ->attachAudits(array());
   }
 
   protected function newObjectQuery() {
     $viewer = $this->getViewer();
 
     return id(new DiffusionCommitQuery())
       ->needCommitData(true)
       ->needAuditRequests(true)
       ->needAuditAuthority(array($viewer))
       ->needIdentities(true);
   }
 
   protected function getEditorURI() {
     return $this->getApplication()->getApplicationURI('commit/edit/');
   }
 
   protected function newCommentActionGroups() {
     return array(
       id(new PhabricatorEditEngineCommentActionGroup())
         ->setKey(self::ACTIONGROUP_AUDIT)
         ->setLabel(pht('Audit Actions')),
       id(new PhabricatorEditEngineCommentActionGroup())
         ->setKey(self::ACTIONGROUP_COMMIT)
         ->setLabel(pht('Commit Actions')),
     );
   }
 
   protected function getObjectCreateTitleText($object) {
     return pht('Create Commit');
   }
 
   protected function getObjectCreateShortText() {
     return pht('Create Commit');
   }
 
   protected function getObjectEditTitleText($object) {
     return pht('Edit Commit: %s', $object->getDisplayName());
   }
 
   protected function getObjectEditShortText($object) {
     return $object->getDisplayName();
   }
 
   protected function getObjectName() {
     return pht('Commit');
   }
 
   protected function getObjectViewURI($object) {
     return $object->getURI();
   }
 
   protected function getCreateNewObjectPolicy() {
     return PhabricatorPolicies::POLICY_NOONE;
   }
 
   protected function buildCustomEditFields($object) {
     $viewer = $this->getViewer();
     $data = $object->getCommitData();
 
     $fields = array();
-
-    $fields[] = id(new PhabricatorDatasourceEditField())
-      ->setKey('auditors')
-      ->setLabel(pht('Auditors'))
-      ->setDatasource(new DiffusionAuditorDatasource())
-      ->setUseEdgeTransactions(true)
-      ->setTransactionType(
-        DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE)
-      ->setCommentActionLabel(pht('Change Auditors'))
-      ->setDescription(pht('Auditors for this commit.'))
-      ->setConduitDescription(pht('Change the auditors for this commit.'))
-      ->setConduitTypeDescription(pht('New auditors.'))
-      ->setValue($object->getAuditorPHIDsForEdit());
+    // remove "Change Auditors" from "Add Action" dropdown etc
+    // if Audit is not installed
+    if (id(new PhabricatorAuditApplication())->isInstalled()) {
+      $fields[] = id(new PhabricatorDatasourceEditField())
+        ->setKey('auditors')
+        ->setLabel(pht('Auditors'))
+          ->setDatasource(new DiffusionAuditorDatasource())
+        ->setUseEdgeTransactions(true)
+        ->setTransactionType(
+          DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE)
+        ->setCommentActionLabel(pht('Change Auditors'))
+        ->setDescription(pht('Auditors for this commit.'))
+        ->setConduitDescription(pht('Change the auditors for this commit.'))
+        ->setConduitTypeDescription(pht('New auditors.'))
+        ->setValue($object->getAuditorPHIDsForEdit());
+    }
 
     $actions = DiffusionCommitActionTransaction::loadAllActions();
     $actions = msortv($actions, 'getCommitActionOrderVector');
 
     foreach ($actions as $key => $action) {
       $fields[] = $action->newEditField($object, $viewer);
     }
 
     return $fields;
   }
 
   protected function newAutomaticCommentTransactions($object) {
     $viewer = $this->getViewer();
 
     $editor = $object->getApplicationTransactionEditor()
       ->setActor($viewer);
 
     $xactions = $editor->newAutomaticInlineTransactions(
       $object,
       PhabricatorAuditActionConstants::INLINE,
       new DiffusionDiffInlineCommentQuery());
 
     return $xactions;
   }
 
   protected function newCommentPreviewContent($object, array $xactions) {
     $viewer = $this->getViewer();
     $type_inline = PhabricatorAuditActionConstants::INLINE;
 
     $inlines = array();
     foreach ($xactions as $xaction) {
       if ($xaction->getTransactionType() === $type_inline) {
         $inlines[] = $xaction->getComment();
       }
     }
 
     $content = array();
 
     if ($inlines) {
       $inline_preview = id(new PHUIDiffInlineCommentPreviewListView())
         ->setViewer($viewer)
         ->setInlineComments($inlines);
 
       $content[] = phutil_tag(
         'div',
         array(
           'id' => 'inline-comment-preview',
         ),
         $inline_preview);
     }
 
     return $content;
   }
 }
diff --git a/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php b/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php
index f895847d4a..f3ae92248c 100644
--- a/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php
+++ b/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php
@@ -1,37 +1,42 @@
 <?php
 
 final class DiffusionAuditorsAddAuditorsHeraldAction
   extends DiffusionAuditorsHeraldAction {
 
   const ACTIONCONST = 'diffusion.auditors.add';
 
   public function getHeraldActionName() {
     return pht('Add auditors');
   }
 
+  // hide "Add auditors" Herald action if Audit not installed
   public function supportsRuleType($rule_type) {
-    return ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
+    if (id(new PhabricatorAuditApplication())->isInstalled()) {
+      return ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
+    } else {
+      return false;
+    }
   }
 
   public function applyEffect($object, HeraldEffect $effect) {
     $rule = $effect->getRule();
     return $this->applyAuditors($effect->getTarget(), $rule);
   }
 
   public function getHeraldActionStandardType() {
     return self::STANDARD_PHID_LIST;
   }
 
   protected function getDatasource() {
     return new DiffusionAuditorDatasource();
   }
 
   public function renderActionDescription($value) {
     return pht('Add auditors: %s.', $this->renderHandleList($value));
   }
 
   public function getPHIDsAffectedByAction(HeraldActionRecord $record) {
     return $record->getTarget();
   }
 
 }
diff --git a/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php b/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php
index d27876d40e..b1ab98db77 100644
--- a/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php
+++ b/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php
@@ -1,30 +1,35 @@
 <?php
 
 final class DiffusionAuditorsAddSelfHeraldAction
   extends DiffusionAuditorsHeraldAction {
 
   const ACTIONCONST = 'diffusion.auditors.self.add';
 
   public function getHeraldActionName() {
     return pht('Add me as an auditor');
   }
 
+  // hide "Add me as an auditor" Herald action if Audit not installed
   public function supportsRuleType($rule_type) {
-    return ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
+    if (id(new PhabricatorAuditApplication())->isInstalled()) {
+      return ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
+    } else {
+      return false;
+    }
   }
 
   public function applyEffect($object, HeraldEffect $effect) {
     $rule = $effect->getRule();
     $phid = $rule->getAuthorPHID();
     return $this->applyAuditors(array($phid), $rule);
   }
 
   public function getHeraldActionStandardType() {
     return self::STANDARD_NONE;
   }
 
   public function renderActionDescription($value) {
     return pht('Add rule author as auditor.');
   }
 
 }
diff --git a/src/applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php
index 5f0da133f8..7afd0f6705 100644
--- a/src/applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php
+++ b/src/applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php
@@ -1,43 +1,52 @@
 <?php
 
 final class DiffusionCommitAuditorsHeraldField
   extends DiffusionCommitHeraldField {
 
   const FIELDCONST = 'diffusion.commit.auditors';
 
+  // hide "Auditors" Herald condition if Audit not installed
+  public function supportsObject($object) {
+    if (id(new PhabricatorAuditApplication())->isInstalled()) {
+      return ($object instanceof PhabricatorRepositoryCommit);
+    } else {
+      return false;
+    }
+  }
+
   public function getHeraldFieldName() {
     return pht('Auditors');
   }
 
   public function getHeraldFieldValue($object) {
     $viewer = PhabricatorUser::getOmnipotentUser();
 
     $commit = id(new DiffusionCommitQuery())
       ->setViewer($viewer)
       ->withPHIDs(array($object->getPHID()))
       ->needAuditRequests(true)
       ->executeOne();
 
     $audits = $commit->getAudits();
 
     $phids = array();
     foreach ($audits as $audit) {
       if ($audit->isResigned()) {
         continue;
       }
 
       $phids[] = $audit->getAuditorPHID();
     }
 
     return $phids;
   }
 
   protected function getHeraldFieldStandardType() {
     return self::STANDARD_PHID_LIST;
   }
 
   protected function getDatasource() {
     return new DiffusionAuditorDatasource();
   }
 
 }
diff --git a/src/applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php
index 587c0b58d3..9fdd785fcb 100644
--- a/src/applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php
+++ b/src/applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php
@@ -1,33 +1,43 @@
 <?php
 
 final class DiffusionCommitPackageAuditHeraldField
   extends DiffusionCommitHeraldField {
 
   const FIELDCONST = 'diffusion.commit.package.audit';
 
+  // hide "Affected packages that need audit" Herald condition
+  // if Audit not installed
+  public function supportsObject($object) {
+    if (id(new PhabricatorAuditApplication())->isInstalled()) {
+      return ($object instanceof PhabricatorRepositoryCommit);
+    } else {
+      return false;
+    }
+  }
+
   public function getHeraldFieldName() {
     return pht('Affected packages that need audit');
   }
 
   public function getFieldGroupKey() {
     return HeraldRelatedFieldGroup::FIELDGROUPKEY;
   }
 
   public function getHeraldFieldValue($object) {
     $packages = $this->getAdapter()->loadAuditNeededPackages();
     if (!$packages) {
       return array();
     }
 
     return mpull($packages, 'getPHID');
   }
 
   protected function getHeraldFieldStandardType() {
     return self::STANDARD_PHID_LIST;
   }
 
   protected function getDatasource() {
     return new PhabricatorOwnersPackageDatasource();
   }
 
 }
diff --git a/src/applications/diffusion/view/DiffusionCommitGraphView.php b/src/applications/diffusion/view/DiffusionCommitGraphView.php
index 30c252712c..7e70a4e655 100644
--- a/src/applications/diffusion/view/DiffusionCommitGraphView.php
+++ b/src/applications/diffusion/view/DiffusionCommitGraphView.php
@@ -1,642 +1,645 @@
 <?php
 
 final class DiffusionCommitGraphView
   extends DiffusionView {
 
   private $history;
   private $commits;
   private $isHead;
   private $isTail;
   private $parents;
   private $filterParents;
 
   private $commitMap;
   private $buildableMap;
   private $revisionMap;
 
   private $showAuditors;
 
   public function setHistory(array $history) {
     assert_instances_of($history, 'DiffusionPathChange');
     $this->history = $history;
     return $this;
   }
 
   public function getHistory() {
     return $this->history;
   }
 
   public function setCommits(array $commits) {
     assert_instances_of($commits, 'PhabricatorRepositoryCommit');
     $this->commits = $commits;
     return $this;
   }
 
   public function getCommits() {
     return $this->commits;
   }
 
   public function setShowAuditors($show_auditors) {
     $this->showAuditors = $show_auditors;
     return $this;
   }
 
   public function getShowAuditors() {
     return $this->showAuditors;
   }
 
   public function setParents(array $parents) {
     $this->parents = $parents;
     return $this;
   }
 
   public function getParents() {
     return $this->parents;
   }
 
   public function setIsHead($is_head) {
     $this->isHead = $is_head;
     return $this;
   }
 
   public function getIsHead() {
     return $this->isHead;
   }
 
   public function setIsTail($is_tail) {
     $this->isTail = $is_tail;
     return $this;
   }
 
   public function getIsTail() {
     return $this->isTail;
   }
 
   public function setFilterParents($filter_parents) {
     $this->filterParents = $filter_parents;
     return $this;
   }
 
   public function getFilterParents() {
     return $this->filterParents;
   }
 
   private function getRepository() {
     $drequest = $this->getDiffusionRequest();
 
     if (!$drequest) {
       return null;
     }
 
     return $drequest->getRepository();
   }
 
   public function newObjectItemListView() {
     $list_view = id(new PHUIObjectItemListView());
 
     $item_views = $this->newObjectItemViews();
     foreach ($item_views as $item_view) {
       $list_view->addItem($item_view);
     }
 
     return $list_view;
   }
 
   private function newObjectItemViews() {
     $viewer = $this->getViewer();
 
     require_celerity_resource('diffusion-css');
 
     $show_builds = $this->shouldShowBuilds();
     $show_revisions = $this->shouldShowRevisions();
     $show_auditors = $this->shouldShowAuditors();
 
     $phids = array();
 
     if ($show_revisions) {
       $revision_map = $this->getRevisionMap();
       foreach ($revision_map as $revisions) {
         foreach ($revisions as $revision) {
           $phids[] = $revision->getPHID();
         }
       }
     }
 
     $commits = $this->getCommitMap();
 
     foreach ($commits as $commit) {
       $author_phid = $commit->getAuthorDisplayPHID();
       if ($author_phid !== null) {
         $phids[] = $author_phid;
       }
     }
 
     if ($show_auditors) {
       foreach ($commits as $commit) {
         $audits = $commit->getAudits();
         foreach ($audits as $auditor) {
           $phids[] = $auditor->getAuditorPHID();
         }
       }
     }
 
     $handles = $viewer->loadHandles($phids);
 
     $views = array();
 
     $items = $this->newHistoryItems();
     foreach ($items as $hash => $item) {
       $content = array();
 
       $commit = $item['commit'];
 
       $commit_description = $this->getCommitDescription($commit);
       $commit_link = $this->getCommitURI($hash);
 
       $short_hash = $this->getCommitObjectName($hash);
       $is_disabled = $this->getCommitIsDisabled($commit);
 
       $item_view = id(new PHUIObjectItemView())
         ->setViewer($viewer)
         ->setHeader($commit_description)
         ->setObjectName($short_hash)
         ->setHref($commit_link)
         ->setDisabled($is_disabled);
 
       $this->addBrowseAction($item_view, $hash);
 
       if ($show_builds) {
         $this->addBuildAction($item_view, $hash);
       }
 
-      $this->addAuditAction($item_view, $hash);
+      // hide Audit entry on /diffusion/commit/query/all if Audit not installed
+      if (id(new PhabricatorAuditApplication())->isInstalled()) {
+        $this->addAuditAction($item_view, $hash);
+      }
 
       if ($show_auditors) {
         $auditor_list = $item_view->newMapView();
         if ($commit) {
           $auditors = $this->newAuditorList($commit, $handles);
           $auditor_list->newItem()
             ->setName(pht('Auditors'))
             ->setValue($auditors);
         }
       }
 
       $property_list = $item_view->newMapView();
 
       if ($commit) {
         $author_view = $this->getCommitAuthorView($commit);
         if ($author_view) {
           $property_list->newItem()
             ->setName(pht('Author'))
             ->setValue($author_view);
         }
       }
 
       if ($show_revisions) {
         if ($commit) {
           $revisions = $this->getRevisions($commit);
           if ($revisions) {
             $list_view = $handles->newSublist(mpull($revisions, 'getPHID'))
               ->newListView();
 
             $property_list->newItem()
               ->setName(pht('Revisions'))
               ->setValue($list_view);
           }
         }
       }
 
       $views[$hash] = $item_view;
     }
 
     return $views;
   }
 
   private function newObjectItemRows() {
     $viewer = $this->getViewer();
 
     $items = $this->newHistoryItems();
     $views = $this->newObjectItemViews();
 
     $last_date = null;
     $rows = array();
     foreach ($items as $hash => $item) {
       $item_epoch = $item['epoch'];
       $item_date = phabricator_date($item_epoch, $viewer);
       if ($item_date !== $last_date) {
         $last_date = $item_date;
         $header = $item_date;
       } else {
         $header = null;
       }
 
       $item_view = $views[$hash];
 
       $list_view = id(new PHUIObjectItemListView())
         ->setViewer($viewer)
         ->setFlush(true)
         ->addItem($item_view);
 
       if ($header !== null) {
         $list_view->setHeader($header);
       }
 
       $rows[] = $list_view;
     }
 
     return $rows;
   }
 
   public function render() {
     $rows = $this->newObjectItemRows();
 
     $graph = $this->newGraphView();
     foreach ($rows as $idx => $row) {
       $cells = array();
 
       if ($graph) {
         $cells[] = phutil_tag(
           'td',
           array(
             'class' => 'diffusion-commit-graph-path-cell',
           ),
           $graph[$idx]);
       }
 
       $cells[] = phutil_tag(
         'td',
         array(
           'class' => 'diffusion-commit-graph-content-cell',
         ),
         $row);
 
       $rows[$idx] = phutil_tag('tr', array(), $cells);
     }
 
     $table = phutil_tag(
       'table',
       array(
         'class' => 'diffusion-commit-graph-table',
       ),
       $rows);
 
     return $table;
   }
 
   private function newGraphView() {
     if (!$this->getParents()) {
       return null;
     }
 
     $parents = $this->getParents();
 
     // If we're filtering parents, remove relationships which point to
     // commits that are not part of the visible graph. Otherwise, we get
     // a big tree of nonsense when viewing release branches like "stable"
     // versus "master".
     if ($this->getFilterParents()) {
       foreach ($parents as $key => $nodes) {
         foreach ($nodes as $nkey => $node) {
           if (empty($parents[$node])) {
             unset($parents[$key][$nkey]);
           }
         }
       }
     }
 
     return id(new PHUIDiffGraphView())
       ->setIsHead($this->getIsHead())
       ->setIsTail($this->getIsTail())
       ->renderGraph($parents);
   }
 
   private function shouldShowBuilds() {
     $viewer = $this->getViewer();
 
     $show_builds = PhabricatorApplication::isClassInstalledForViewer(
       'PhabricatorHarbormasterApplication',
       $this->getUser());
 
     return $show_builds;
   }
 
   private function shouldShowRevisions() {
     $viewer = $this->getViewer();
 
     $show_revisions = PhabricatorApplication::isClassInstalledForViewer(
       'PhabricatorDifferentialApplication',
       $viewer);
 
     return $show_revisions;
   }
 
   private function shouldShowAuditors() {
     return $this->getShowAuditors();
   }
 
   private function newHistoryItems() {
     $items = array();
 
     $history = $this->getHistory();
     if ($history !== null) {
       foreach ($history as $history_item) {
         $commit_hash = $history_item->getCommitIdentifier();
 
         $items[$commit_hash] = array(
           'epoch' => $history_item->getEpoch(),
           'hash' => $commit_hash,
           'commit' => $this->getCommit($commit_hash),
         );
       }
     } else {
       $commits = $this->getCommitMap();
       foreach ($commits as $commit) {
         $commit_hash = $commit->getCommitIdentifier();
 
         $items[$commit_hash] = array(
           'epoch' => $commit->getEpoch(),
           'hash' => $commit_hash,
           'commit' => $commit,
         );
       }
     }
 
     return $items;
   }
 
   private function getCommitDescription($commit) {
     if (!$commit) {
       return phutil_tag('em', array(), pht("Discovering\xE2\x80\xA6"));
     }
 
     // We can show details once the message and change have been imported.
     $partial_import = PhabricatorRepositoryCommit::IMPORTED_MESSAGE |
                       PhabricatorRepositoryCommit::IMPORTED_CHANGE;
     if (!$commit->isPartiallyImported($partial_import)) {
       return phutil_tag('em', array(), pht("Importing\xE2\x80\xA6"));
     }
 
     return $commit->getCommitData()->getSummary();
   }
 
   private function getCommitURI($hash) {
     $repository = $this->getRepository();
 
     if ($repository) {
       return $repository->getCommitURI($hash);
     }
 
     $commit = $this->getCommit($hash);
     if ($commit) {
       return $commit->getURI();
     }
 
     return null;
   }
 
   private function getCommitObjectName($hash) {
     $repository = $this->getRepository();
 
     if ($repository) {
       return $repository->formatCommitName(
         $hash,
         $is_local = true);
     }
 
     $commit = $this->getCommit($hash);
     if ($commit) {
       return $commit->getDisplayName();
     }
 
     return null;
   }
 
   private function getCommitIsDisabled($commit) {
     if (!$commit) {
       return true;
     }
 
     if ($commit->isUnreachable()) {
       return true;
     }
 
     return false;
   }
 
   private function getCommitAuthorView($commit) {
     if (!$commit) {
       return null;
     }
 
     $viewer = $this->getViewer();
 
     $author_phid = $commit->getAuthorDisplayPHID();
     if ($author_phid) {
       return $viewer->loadHandles(array($author_phid))
         ->newListView();
     }
 
     return $commit->newCommitAuthorView($viewer);
   }
 
   private function getCommit($hash) {
     $commit_map = $this->getCommitMap();
     return idx($commit_map, $hash);
   }
 
   private function getCommitMap() {
     if ($this->commitMap === null) {
       $commit_list = $this->newCommitList();
       $this->commitMap = mpull($commit_list, null, 'getCommitIdentifier');
     }
 
     return $this->commitMap;
   }
 
   private function addBrowseAction(PHUIObjectItemView $item, $hash) {
     $repository = $this->getRepository();
 
     if (!$repository) {
       return;
     }
 
     $drequest = $this->getDiffusionRequest();
     $path = $drequest->getPath();
 
     $uri = $drequest->generateURI(
       array(
         'action' => 'browse',
         'path' => $path,
         'commit' => $hash,
       ));
 
     $menu_item = $item->newMenuItem()
       ->setName(pht('Browse Repository'))
       ->setURI($uri);
 
     $menu_item->newIcon()
       ->setIcon('fa-folder-open-o')
       ->setColor('bluegrey');
   }
 
   private function addBuildAction(PHUIObjectItemView $item, $hash) {
     $is_disabled = true;
 
     $buildable = null;
 
     $commit = $this->getCommit($hash);
     if ($commit) {
       $buildable = $this->getBuildable($commit);
     }
 
     if ($buildable) {
       $icon = $buildable->getStatusIcon();
       $color = $buildable->getStatusColor();
       $name = $buildable->getStatusDisplayName();
       $uri = $buildable->getURI();
     } else {
       $icon = 'fa-times';
       $color = 'grey';
       $name = pht('No Builds');
       $uri = null;
     }
 
     $menu_item = $item->newMenuItem()
       ->setName($name)
       ->setURI($uri)
       ->setDisabled(($uri === null));
 
     $menu_item->newIcon()
       ->setIcon($icon)
       ->setColor($color);
   }
 
   private function addAuditAction(PHUIObjectItemView $item_view, $hash) {
     $commit = $this->getCommit($hash);
 
     if ($commit) {
       $status = $commit->getAuditStatusObject();
 
       $text = $status->getName();
       $icon = $status->getIcon();
 
       $is_disabled = $status->isNoAudit();
       if ($is_disabled) {
         $uri = null;
         $color = 'grey';
       } else {
         $color = $status->getColor();
         $uri = $commit->getURI();
       }
     } else {
       $text = pht('No Audit');
       $color = 'grey';
       $icon = 'fa-times';
       $uri = null;
 
       $is_disabled = true;
     }
 
     $menu_item = $item_view->newMenuItem()
       ->setName($text)
       ->setURI($uri)
       ->setBackgroundColor($color)
       ->setDisabled($is_disabled);
 
     $menu_item->newIcon()
       ->setIcon($icon)
       ->setColor($color);
   }
 
   private function getBuildable(PhabricatorRepositoryCommit $commit) {
     $buildable_map = $this->getBuildableMap();
     return idx($buildable_map, $commit->getPHID());
   }
 
   private function getBuildableMap() {
     if ($this->buildableMap === null) {
       $commits = $this->getCommitMap();
       $buildables = $this->loadBuildables($commits);
       $this->buildableMap = $buildables;
     }
 
     return $this->buildableMap;
   }
 
   private function getRevisions(PhabricatorRepositoryCommit $commit) {
     $revision_map = $this->getRevisionMap();
     return idx($revision_map, $commit->getPHID(), array());
   }
 
   private function getRevisionMap() {
     if ($this->revisionMap === null) {
       $this->revisionMap = $this->newRevisionMap();
     }
 
     return $this->revisionMap;
   }
 
   private function newRevisionMap() {
     $viewer = $this->getViewer();
     $commits = $this->getCommitMap();
 
     return DiffusionCommitRevisionQuery::loadRevisionMapForCommits(
       $viewer,
       $commits);
   }
 
   private function newCommitList() {
     $commits = $this->getCommits();
     if ($commits !== null) {
       return $commits;
     }
 
     $repository = $this->getRepository();
     if (!$repository) {
       return array();
     }
 
     $history = $this->getHistory();
     if ($history === null) {
       return array();
     }
 
     $identifiers = array();
     foreach ($history as $item) {
       $identifiers[] = $item->getCommitIdentifier();
     }
 
     if (!$identifiers) {
       return array();
     }
 
     $viewer = $this->getViewer();
 
     $commits = id(new DiffusionCommitQuery())
       ->setViewer($viewer)
       ->withRepository($repository)
       ->withIdentifiers($identifiers)
       ->needCommitData(true)
       ->needIdentities(true)
       ->execute();
 
     return $commits;
   }
 
   private function newAuditorList(
     PhabricatorRepositoryCommit $commit,
     $handles) {
 
     $auditors = $commit->getAudits();
     if (!$auditors) {
       return phutil_tag('em', array(), pht('None'));
     }
 
     $auditor_phids = mpull($auditors, 'getAuditorPHID');
     $auditor_list = $handles->newSublist($auditor_phids)
       ->newListView();
 
     return $auditor_list;
   }
 
 }
diff --git a/src/applications/diffusion/xaction/DiffusionCommitActionTransaction.php b/src/applications/diffusion/xaction/DiffusionCommitActionTransaction.php
index 1d351ffa5d..d381602766 100644
--- a/src/applications/diffusion/xaction/DiffusionCommitActionTransaction.php
+++ b/src/applications/diffusion/xaction/DiffusionCommitActionTransaction.php
@@ -1,124 +1,127 @@
 <?php
 
 abstract class DiffusionCommitActionTransaction
   extends DiffusionCommitTransactionType {
 
   final public function getCommitActionKey() {
     return $this->getPhobjectClassConstant('ACTIONKEY', 32);
   }
 
   public function isActionAvailable($object, PhabricatorUser $viewer) {
+    if (!id(new PhabricatorAuditApplication())->isInstalled()) {
+      return false;
+    }
     try {
       $this->validateAction($object, $viewer);
       return true;
     } catch (Exception $ex) {
       return false;
     }
   }
 
   abstract protected function validateAction($object, PhabricatorUser $viewer);
   abstract protected function getCommitActionLabel();
 
   public function getCommandKeyword() {
     return null;
   }
 
   public function getCommandAliases() {
     return array();
   }
 
   public function getCommandSummary() {
     return null;
   }
 
   protected function getCommitActionOrder() {
     return 1000;
   }
 
   public function getCommitActionOrderVector() {
     return id(new PhutilSortVector())
       ->addInt($this->getCommitActionOrder());
   }
 
   protected function getCommitActionGroupKey() {
     return DiffusionCommitEditEngine::ACTIONGROUP_COMMIT;
   }
 
   protected function getCommitActionDescription() {
     return null;
   }
 
   public static function loadAllActions() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getCommitActionKey')
       ->execute();
   }
 
   protected function isViewerCommitAuthor(
     PhabricatorRepositoryCommit $commit,
     PhabricatorUser $viewer) {
 
     if (!$viewer->getPHID()) {
       return false;
     }
 
     return ($viewer->getPHID() === $commit->getEffectiveAuthorPHID());
   }
 
   public function newEditField(
     PhabricatorRepositoryCommit $commit,
     PhabricatorUser $viewer) {
 
     // Actions in the "audit" group, like "Accept Commit", do not require
     // that the actor be able to edit the commit.
     $group_audit = DiffusionCommitEditEngine::ACTIONGROUP_AUDIT;
     $is_audit = ($this->getCommitActionGroupKey() == $group_audit);
 
     $field = id(new PhabricatorApplyEditField())
       ->setKey($this->getCommitActionKey())
       ->setTransactionType($this->getTransactionTypeConstant())
       ->setCanApplyWithoutEditCapability($is_audit)
       ->setValue(true);
 
     if ($this->isActionAvailable($commit, $viewer)) {
       $label = $this->getCommitActionLabel();
       if ($label !== null) {
         $field->setCommentActionLabel($label);
 
         $description = $this->getCommitActionDescription();
         $field->setActionDescription($description);
 
         $group_key = $this->getCommitActionGroupKey();
         $field->setCommentActionGroupKey($group_key);
 
         $field->setActionConflictKey('commit.action');
       }
     }
 
     return $field;
   }
 
   public function validateTransactions($object, array $xactions) {
     $errors = array();
     $actor = $this->getActor();
 
     $action_exception = null;
     try {
       $this->validateAction($object, $actor);
     } catch (Exception $ex) {
       $action_exception = $ex;
     }
 
     foreach ($xactions as $xaction) {
       if ($action_exception) {
         $errors[] = $this->newInvalidError(
           $action_exception->getMessage(),
           $xaction);
       }
     }
 
     return $errors;
   }
 
 }
diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
index 076a9a1e52..cad397b0ac 100644
--- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
@@ -1,1627 +1,1629 @@
 <?php
 
 /**
  * Represents an abstract search engine for an application. It supports
  * creating and storing saved queries.
  *
  * @task construct  Constructing Engines
  * @task app        Applications
  * @task builtin    Builtin Queries
  * @task uri        Query URIs
  * @task dates      Date Filters
  * @task order      Result Ordering
  * @task read       Reading Utilities
  * @task exec       Paging and Executing Queries
  * @task render     Rendering Results
  */
 abstract class PhabricatorApplicationSearchEngine extends Phobject {
 
   private $application;
   private $viewer;
   private $errors = array();
   private $request;
   private $context;
   private $controller;
   private $namedQueries;
   private $navigationItems = array();
 
   const CONTEXT_LIST  = 'list';
   const CONTEXT_PANEL = 'panel';
 
   const BUCKET_NONE = 'none';
 
   public function setController(PhabricatorController $controller) {
     $this->controller = $controller;
     return $this;
   }
 
   public function getController() {
     return $this->controller;
   }
 
   public function buildResponse() {
     $controller = $this->getController();
     $request = $controller->getRequest();
 
     $search = id(new PhabricatorApplicationSearchController())
       ->setQueryKey($request->getURIData('queryKey'))
       ->setSearchEngine($this);
 
     return $controller->delegateToController($search);
   }
 
   public function newResultObject() {
     // We may be able to get this automatically if newQuery() is implemented.
     $query = $this->newQuery();
     if ($query) {
       $object = $query->newResultObject();
       if ($object) {
         return $object;
       }
     }
 
     return null;
   }
 
   public function newQuery() {
     return null;
   }
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   protected function requireViewer() {
     if (!$this->viewer) {
       throw new PhutilInvalidStateException('setViewer');
     }
     return $this->viewer;
   }
 
   public function setContext($context) {
     $this->context = $context;
     return $this;
   }
 
   public function isPanelContext() {
     return ($this->context == self::CONTEXT_PANEL);
   }
 
   public function setNavigationItems(array $navigation_items) {
     assert_instances_of($navigation_items, 'PHUIListItemView');
     $this->navigationItems = $navigation_items;
     return $this;
   }
 
   public function getNavigationItems() {
     return $this->navigationItems;
   }
 
   public function canUseInPanelContext() {
     return true;
   }
 
   public function saveQuery(PhabricatorSavedQuery $query) {
     if ($query->getID()) {
       throw new Exception(
         pht(
           'Query (with ID "%s") has already been saved. Queries are '.
           'immutable once saved.',
           $query->getID()));
     }
 
     $query->setEngineClassName(get_class($this));
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
     try {
       $query->save();
     } catch (AphrontDuplicateKeyQueryException $ex) {
       // Ignore, this is just a repeated search.
     }
     unset($unguarded);
   }
 
   /**
    * Create a saved query object from the request.
    *
    * @param AphrontRequest The search request.
    * @return PhabricatorSavedQuery
    */
   public function buildSavedQueryFromRequest(AphrontRequest $request) {
     $fields = $this->buildSearchFields();
     $viewer = $this->requireViewer();
 
     $saved = new PhabricatorSavedQuery();
     foreach ($fields as $field) {
       $field->setViewer($viewer);
 
       $value = $field->readValueFromRequest($request);
       $saved->setParameter($field->getKey(), $value);
     }
 
     return $saved;
   }
 
   /**
    * Executes the saved query.
    *
    * @param PhabricatorSavedQuery The saved query to operate on.
    * @return PhabricatorQuery The result of the query.
    */
   public function buildQueryFromSavedQuery(PhabricatorSavedQuery $original) {
     $saved = clone $original;
     $this->willUseSavedQuery($saved);
 
     $fields = $this->buildSearchFields();
     $viewer = $this->requireViewer();
 
     $map = array();
     foreach ($fields as $field) {
       $field->setViewer($viewer);
       $field->readValueFromSavedQuery($saved);
       $value = $field->getValueForQuery($field->getValue());
       $map[$field->getKey()] = $value;
     }
 
     $original->attachParameterMap($map);
     $query = $this->buildQueryFromParameters($map);
 
     $object = $this->newResultObject();
     if (!$object) {
       return $query;
     }
 
     $extensions = $this->getEngineExtensions();
     foreach ($extensions as $extension) {
       $extension->applyConstraintsToQuery($object, $query, $saved, $map);
     }
 
     $order = $saved->getParameter('order');
     $builtin = $query->getBuiltinOrderAliasMap();
     if (phutil_nonempty_string($order) && isset($builtin[$order])) {
       $query->setOrder($order);
     } else {
       // If the order is invalid or not available, we choose the first
       // builtin order. This isn't always the default order for the query,
       // but is the first value in the "Order" dropdown, and makes the query
       // behavior more consistent with the UI. In queries where the two
       // orders differ, this order is the preferred order for humans.
       $query->setOrder(head_key($builtin));
     }
 
     return $query;
   }
 
   /**
    * Hook for subclasses to adjust saved queries prior to use.
    *
    * If an application changes how queries are saved, it can implement this
    * hook to keep old queries working the way users expect, by reading,
    * adjusting, and overwriting parameters.
    *
    * @param PhabricatorSavedQuery Saved query which will be executed.
    * @return void
    */
   protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
     return;
   }
 
   protected function buildQueryFromParameters(array $parameters) {
     throw new PhutilMethodNotImplementedException();
   }
 
   /**
    * Builds the search form using the request.
    *
    * @param AphrontFormView       Form to populate.
    * @param PhabricatorSavedQuery The query from which to build the form.
    * @return void
    */
   public function buildSearchForm(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved) {
 
     $saved = clone $saved;
     $this->willUseSavedQuery($saved);
 
     $fields = $this->buildSearchFields();
     $fields = $this->adjustFieldsForDisplay($fields);
     $viewer = $this->requireViewer();
 
     foreach ($fields as $field) {
       $field->setViewer($viewer);
       $field->readValueFromSavedQuery($saved);
     }
 
     foreach ($fields as $field) {
       foreach ($field->getErrors() as $error) {
         $this->addError(last($error));
       }
     }
 
     foreach ($fields as $field) {
       $field->appendToForm($form);
     }
   }
 
   protected function buildSearchFields() {
     $fields = array();
 
     foreach ($this->buildCustomSearchFields() as $field) {
       $fields[] = $field;
     }
 
     $object = $this->newResultObject();
     if ($object) {
       $extensions = $this->getEngineExtensions();
       foreach ($extensions as $extension) {
         $extension_fields = $extension->getSearchFields($object);
         foreach ($extension_fields as $extension_field) {
           $fields[] = $extension_field;
         }
       }
     }
 
     $query = $this->newQuery();
     if ($query && $this->shouldShowOrderField()) {
       $orders = $query->getBuiltinOrders();
       $orders = ipull($orders, 'name');
 
       $fields[] = id(new PhabricatorSearchOrderField())
         ->setLabel(pht('Order By'))
         ->setKey('order')
         ->setOrderAliases($query->getBuiltinOrderAliasMap())
         ->setOptions($orders);
     }
 
-    $buckets = $this->newResultBuckets();
-    if ($query && $buckets) {
-      $bucket_options = array(
-        self::BUCKET_NONE => pht('No Bucketing'),
-      ) + mpull($buckets, 'getResultBucketName');
-
-      $fields[] = id(new PhabricatorSearchSelectField())
-        ->setLabel(pht('Bucket'))
-        ->setKey('bucket')
-        ->setOptions($bucket_options);
+    if (id(new PhabricatorAuditApplication())->isInstalled()) {
+      $buckets = $this->newResultBuckets();
+      if ($query && $buckets) {
+        $bucket_options = array(
+          self::BUCKET_NONE => pht('No Bucketing'),
+        ) + mpull($buckets, 'getResultBucketName');
+
+        $fields[] = id(new PhabricatorSearchSelectField())
+          ->setLabel(pht('Bucket'))
+          ->setKey('bucket')
+          ->setOptions($bucket_options);
+      }
     }
 
     $field_map = array();
     foreach ($fields as $field) {
       $key = $field->getKey();
       if (isset($field_map[$key])) {
         throw new Exception(
           pht(
             'Two fields in this SearchEngine use the same key ("%s"), but '.
             'each field must use a unique key.',
             $key));
       }
       $field_map[$key] = $field;
     }
 
     return $field_map;
   }
 
   protected function shouldShowOrderField() {
     return true;
   }
 
   private function adjustFieldsForDisplay(array $field_map) {
     $order = $this->getDefaultFieldOrder();
 
     $head_keys = array();
     $tail_keys = array();
     $seen_tail = false;
     foreach ($order as $order_key) {
       if ($order_key === '...') {
         $seen_tail = true;
         continue;
       }
 
       if (!$seen_tail) {
         $head_keys[] = $order_key;
       } else {
         $tail_keys[] = $order_key;
       }
     }
 
     $head = array_select_keys($field_map, $head_keys);
     $body = array_diff_key($field_map, array_fuse($tail_keys));
     $tail = array_select_keys($field_map, $tail_keys);
 
     $result = $head + $body + $tail;
 
     // Force the fulltext "query" field to the top unconditionally.
     $result = array_select_keys($result, array('query')) + $result;
 
     foreach ($this->getHiddenFields() as $hidden_key) {
       unset($result[$hidden_key]);
     }
 
     return $result;
   }
 
   protected function buildCustomSearchFields() {
     throw new PhutilMethodNotImplementedException();
   }
 
 
   /**
    * Define the default display order for fields by returning a list of
    * field keys.
    *
    * You can use the special key `...` to mean "all unspecified fields go
    * here". This lets you easily put important fields at the top of the form,
    * standard fields in the middle of the form, and less important fields at
    * the bottom.
    *
    * For example, you might return a list like this:
    *
    *   return array(
    *     'authorPHIDs',
    *     'reviewerPHIDs',
    *     '...',
    *     'createdAfter',
    *     'createdBefore',
    *   );
    *
    * Any unspecified fields (including custom fields and fields added
    * automatically by infrastructure) will be put in the middle.
    *
    * @return list<string> Default ordering for field keys.
    */
   protected function getDefaultFieldOrder() {
     return array();
   }
 
   /**
    * Return a list of field keys which should be hidden from the viewer.
    *
     * @return list<string> Fields to hide.
    */
   protected function getHiddenFields() {
     return array();
   }
 
   public function getErrors() {
     return $this->errors;
   }
 
   public function addError($error) {
     $this->errors[] = $error;
     return $this;
   }
 
   /**
    * Return an application URI corresponding to the results page of a query.
    * Normally, this is something like `/application/query/QUERYKEY/`.
    *
    * @param   string  The query key to build a URI for.
    * @return  string  URI where the query can be executed.
    * @task uri
    */
   public function getQueryResultsPageURI($query_key) {
     return $this->getURI('query/'.$query_key.'/');
   }
 
 
   /**
    * Return an application URI for query management. This is used when, e.g.,
    * a query deletion operation is cancelled.
    *
    * @return  string  URI where queries can be managed.
    * @task uri
    */
   public function getQueryManagementURI() {
     return $this->getURI('query/edit/');
   }
 
   public function getQueryBaseURI() {
     return $this->getURI('');
   }
 
   public function getExportURI($query_key) {
     return $this->getURI('query/'.$query_key.'/export/');
   }
 
   public function getCustomizeURI($query_key, $object_phid, $context_phid) {
     $params = array(
       'search.objectPHID' => $object_phid,
       'search.contextPHID' => $context_phid,
     );
 
     $uri = $this->getURI('query/'.$query_key.'/customize/');
     $uri = new PhutilURI($uri, $params);
 
     return phutil_string_cast($uri);
   }
 
 
 
   /**
    * Return the URI to a path within the application. Used to construct default
    * URIs for management and results.
    *
    * @return string URI to path.
    * @task uri
    */
   abstract protected function getURI($path);
 
 
   /**
    * Return a human readable description of the type of objects this query
    * searches for.
    *
    * For example, "Tasks" or "Commits".
    *
    * @return string Human-readable description of what this engine is used to
    *   find.
    */
   abstract public function getResultTypeDescription();
 
 
   public function newSavedQuery() {
     return id(new PhabricatorSavedQuery())
       ->setEngineClassName(get_class($this));
   }
 
   public function addNavigationItems(PHUIListView $menu) {
     $viewer = $this->requireViewer();
 
     $menu->newLabel(pht('Queries'));
 
     $named_queries = $this->loadEnabledNamedQueries();
 
     foreach ($named_queries as $query) {
       $key = $query->getQueryKey();
       $uri = $this->getQueryResultsPageURI($key);
       $menu->newLink($query->getQueryName(), $uri, 'query/'.$key);
     }
 
     if ($viewer->isLoggedIn()) {
       $manage_uri = $this->getQueryManagementURI();
       $menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit');
     }
 
     $menu->newLabel(pht('Search'));
     $advanced_uri = $this->getQueryResultsPageURI('advanced');
     $menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced');
 
     foreach ($this->navigationItems as $extra_item) {
       $menu->addMenuItem($extra_item);
     }
 
     return $this;
   }
 
   public function loadAllNamedQueries() {
     $viewer = $this->requireViewer();
     $builtin = $this->getBuiltinQueries();
 
     if ($this->namedQueries === null) {
       $named_queries = id(new PhabricatorNamedQueryQuery())
         ->setViewer($viewer)
         ->withEngineClassNames(array(get_class($this)))
         ->withUserPHIDs(
           array(
             $viewer->getPHID(),
             PhabricatorNamedQuery::SCOPE_GLOBAL,
           ))
         ->execute();
       $named_queries = mpull($named_queries, null, 'getQueryKey');
 
       $builtin = mpull($builtin, null, 'getQueryKey');
 
       foreach ($named_queries as $key => $named_query) {
         if ($named_query->getIsBuiltin()) {
           if (isset($builtin[$key])) {
             $named_queries[$key]->setQueryName($builtin[$key]->getQueryName());
             unset($builtin[$key]);
           } else {
             unset($named_queries[$key]);
           }
         }
 
         unset($builtin[$key]);
       }
 
       $named_queries = msortv($named_queries, 'getNamedQuerySortVector');
       $this->namedQueries = $named_queries;
     }
 
     return $this->namedQueries + $builtin;
   }
 
   public function loadEnabledNamedQueries() {
     $named_queries = $this->loadAllNamedQueries();
     foreach ($named_queries as $key => $named_query) {
       if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
         unset($named_queries[$key]);
       }
     }
     return $named_queries;
   }
 
   public function getDefaultQueryKey() {
     $viewer = $this->requireViewer();
 
     $configs = id(new PhabricatorNamedQueryConfigQuery())
       ->setViewer($viewer)
       ->withEngineClassNames(array(get_class($this)))
       ->withScopePHIDs(
         array(
           $viewer->getPHID(),
           PhabricatorNamedQueryConfig::SCOPE_GLOBAL,
         ))
       ->execute();
     $configs = msortv($configs, 'getStrengthSortVector');
 
     $key_pinned = PhabricatorNamedQueryConfig::PROPERTY_PINNED;
     $map = $this->loadEnabledNamedQueries();
     foreach ($configs as $config) {
       $pinned = $config->getConfigProperty($key_pinned);
       if (!isset($map[$pinned])) {
         continue;
       }
 
       return $pinned;
     }
 
     return head_key($map);
   }
 
   protected function setQueryProjects(
     PhabricatorCursorPagedPolicyAwareQuery $query,
     PhabricatorSavedQuery $saved) {
 
     $datasource = id(new PhabricatorProjectLogicalDatasource())
       ->setViewer($this->requireViewer());
 
     $projects = $saved->getParameter('projects', array());
     $constraints = $datasource->evaluateTokens($projects);
 
     if ($constraints) {
       $query->withEdgeLogicConstraints(
         PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
         $constraints);
     }
 
     return $this;
   }
 
 
 /* -(  Applications  )------------------------------------------------------- */
 
 
   protected function getApplicationURI($path = '') {
     return $this->getApplication()->getApplicationURI($path);
   }
 
   protected function getApplication() {
     if (!$this->application) {
       $class = $this->getApplicationClassName();
 
       $this->application = id(new PhabricatorApplicationQuery())
         ->setViewer($this->requireViewer())
         ->withClasses(array($class))
         ->withInstalled(true)
         ->executeOne();
 
       if (!$this->application) {
         throw new Exception(
           pht(
             'Application "%s" is not installed!',
             $class));
       }
     }
 
     return $this->application;
   }
 
   abstract public function getApplicationClassName();
 
 
 /* -(  Constructing Engines  )----------------------------------------------- */
 
 
   /**
    * Load all available application search engines.
    *
    * @return list<PhabricatorApplicationSearchEngine> All available engines.
    * @task construct
    */
   public static function getAllEngines() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->execute();
   }
 
 
   /**
    * Get an engine by class name, if it exists.
    *
    * @return PhabricatorApplicationSearchEngine|null Engine, or null if it does
    *   not exist.
    * @task construct
    */
   public static function getEngineByClassName($class_name) {
     return idx(self::getAllEngines(), $class_name);
   }
 
 
 /* -(  Builtin Queries  )---------------------------------------------------- */
 
 
   /**
    * @task builtin
    */
   public function getBuiltinQueries() {
     $names = $this->getBuiltinQueryNames();
 
     $queries = array();
     $sequence = 0;
     foreach ($names as $key => $name) {
       $queries[$key] = id(new PhabricatorNamedQuery())
         ->setUserPHID(PhabricatorNamedQuery::SCOPE_GLOBAL)
         ->setEngineClassName(get_class($this))
         ->setQueryName($name)
         ->setQueryKey($key)
         ->setSequence((1 << 24) + $sequence++)
         ->setIsBuiltin(true);
     }
 
     return $queries;
   }
 
 
   /**
    * @task builtin
    */
   public function getBuiltinQuery($query_key) {
     if (!$this->isBuiltinQuery($query_key)) {
       throw new Exception(pht("'%s' is not a builtin!", $query_key));
     }
     return idx($this->getBuiltinQueries(), $query_key);
   }
 
 
   /**
    * @task builtin
    */
   protected function getBuiltinQueryNames() {
     return array();
   }
 
 
   /**
    * @task builtin
    */
   public function isBuiltinQuery($query_key) {
     $builtins = $this->getBuiltinQueries();
     return isset($builtins[$query_key]);
   }
 
 
   /**
    * @task builtin
    */
   public function buildSavedQueryFromBuiltin($query_key) {
     throw new Exception(pht("Builtin '%s' is not supported!", $query_key));
   }
 
 
 /* -(  Reading Utilities )--------------------------------------------------- */
 
 
   /**
    * Read a list of user PHIDs from a request in a flexible way. This method
    * supports either of these forms:
    *
    *   users[]=alincoln&users[]=htaft
    *   users=alincoln,htaft
    *
    * Additionally, users can be specified either by PHID or by name.
    *
    * The main goal of this flexibility is to allow external programs to generate
    * links to pages (like "alincoln's open revisions") without needing to make
    * API calls.
    *
    * @param AphrontRequest  Request to read user PHIDs from.
    * @param string          Key to read in the request.
    * @param list<const>     Other permitted PHID types.
    * @return list<phid>     List of user PHIDs and selector functions.
    * @task read
    */
   protected function readUsersFromRequest(
     AphrontRequest $request,
     $key,
     array $allow_types = array()) {
 
     $list = $this->readListFromRequest($request, $key);
 
     $phids = array();
     $names = array();
     $allow_types = array_fuse($allow_types);
     $user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
     foreach ($list as $item) {
       $type = phid_get_type($item);
       if ($type == $user_type) {
         $phids[] = $item;
       } else if (isset($allow_types[$type])) {
         $phids[] = $item;
       } else {
         if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
           // If this is a function, pass it through unchanged; we'll evaluate
           // it later.
           $phids[] = $item;
         } else {
           $names[] = $item;
         }
       }
     }
 
     if ($names) {
       $users = id(new PhabricatorPeopleQuery())
         ->setViewer($this->requireViewer())
         ->withUsernames($names)
         ->execute();
       foreach ($users as $user) {
         $phids[] = $user->getPHID();
       }
       $phids = array_unique($phids);
     }
 
     return $phids;
   }
 
 
   /**
    * Read a list of subscribers from a request in a flexible way.
    *
    * @param AphrontRequest  Request to read PHIDs from.
    * @param string          Key to read in the request.
    * @return list<phid>     List of object PHIDs.
    * @task read
    */
   protected function readSubscribersFromRequest(
     AphrontRequest $request,
     $key) {
     return $this->readUsersFromRequest(
       $request,
       $key,
       array(
         PhabricatorProjectProjectPHIDType::TYPECONST,
       ));
   }
 
 
   /**
    * Read a list of generic PHIDs from a request in a flexible way. Like
    * @{method:readUsersFromRequest}, this method supports either array or
    * comma-delimited forms. Objects can be specified either by PHID or by
    * object name.
    *
    * @param AphrontRequest  Request to read PHIDs from.
    * @param string          Key to read in the request.
    * @param list<const>     Optional, list of permitted PHID types.
    * @return list<phid>     List of object PHIDs.
    *
    * @task read
    */
   protected function readPHIDsFromRequest(
     AphrontRequest $request,
     $key,
     array $allow_types = array()) {
 
     $list = $this->readListFromRequest($request, $key);
 
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($this->requireViewer())
       ->withNames($list)
       ->execute();
     $list = mpull($objects, 'getPHID');
 
     if (!$list) {
       return array();
     }
 
     // If only certain PHID types are allowed, filter out all the others.
     if ($allow_types) {
       $allow_types = array_fuse($allow_types);
       foreach ($list as $key => $phid) {
         if (empty($allow_types[phid_get_type($phid)])) {
           unset($list[$key]);
         }
       }
     }
 
     return $list;
   }
 
 
   /**
    * Read a list of items from the request, in either array format or string
    * format:
    *
    *   list[]=item1&list[]=item2
    *   list=item1,item2
    *
    * This provides flexibility when constructing URIs, especially from external
    * sources.
    *
    * @param AphrontRequest  Request to read strings from.
    * @param string          Key to read in the request.
    * @return list<string>   List of values.
    */
   protected function readListFromRequest(
     AphrontRequest $request,
     $key) {
     $list = $request->getArr($key, null);
     if ($list === null) {
       $list = $request->getStrList($key);
     }
 
     if (!$list) {
       return array();
     }
 
     return $list;
   }
 
   protected function readBoolFromRequest(
     AphrontRequest $request,
     $key) {
     if (!phutil_nonempty_string($request->getStr($key))) {
       return null;
     }
     return $request->getBool($key);
   }
 
 
   protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) {
     $value = $query->getParameter($key);
     if ($value === null) {
       return $value;
     }
     return $value ? 'true' : 'false';
   }
 
 
 /* -(  Dates  )-------------------------------------------------------------- */
 
 
   /**
    * @task dates
    */
   protected function parseDateTime($date_time) {
     if (!strlen($date_time)) {
       return null;
     }
 
     return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer());
   }
 
 
   /**
    * @task dates
    */
   protected function buildDateRange(
     AphrontFormView $form,
     PhabricatorSavedQuery $saved_query,
     $start_key,
     $start_name,
     $end_key,
     $end_name) {
 
     $start_str = $saved_query->getParameter($start_key);
     $start = null;
     if (strlen($start_str)) {
       $start = $this->parseDateTime($start_str);
       if (!$start) {
         $this->addError(
           pht(
             '"%s" date can not be parsed.',
             $start_name));
       }
     }
 
 
     $end_str = $saved_query->getParameter($end_key);
     $end = null;
     if (strlen($end_str)) {
       $end = $this->parseDateTime($end_str);
       if (!$end) {
         $this->addError(
           pht(
             '"%s" date can not be parsed.',
             $end_name));
       }
     }
 
     if ($start && $end && ($start >= $end)) {
       $this->addError(
         pht(
           '"%s" must be a date before "%s".',
           $start_name,
           $end_name));
     }
 
     $form
       ->appendChild(
         id(new PHUIFormFreeformDateControl())
           ->setName($start_key)
           ->setLabel($start_name)
           ->setValue($start_str))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName($end_key)
           ->setLabel($end_name)
           ->setValue($end_str));
   }
 
 
 /* -(  Paging and Executing Queries  )--------------------------------------- */
 
 
   protected function newResultBuckets() {
     return array();
   }
 
   public function getResultBucket(PhabricatorSavedQuery $saved) {
     $key = $saved->getParameter('bucket');
     if ($key == self::BUCKET_NONE) {
       return null;
     }
 
     $buckets = $this->newResultBuckets();
     return idx($buckets, $key);
   }
 
 
   public function getPageSize(PhabricatorSavedQuery $saved) {
     $bucket = $this->getResultBucket($saved);
 
     $limit = (int)$saved->getParameter('limit');
 
     if ($limit > 0) {
       if ($bucket) {
         $bucket->setPageSize($limit);
       }
       return $limit;
     }
 
     if ($bucket) {
       return $bucket->getPageSize();
     }
 
     return 100;
   }
 
 
   public function shouldUseOffsetPaging() {
     return false;
   }
 
 
   public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) {
     if ($this->shouldUseOffsetPaging()) {
       $pager = new PHUIPagerView();
     } else {
       $pager = new AphrontCursorPagerView();
     }
 
     $page_size = $this->getPageSize($saved);
     if (is_finite($page_size)) {
       $pager->setPageSize($page_size);
     } else {
       // Consider an INF pagesize to mean a large finite pagesize.
 
       // TODO: It would be nice to handle this more gracefully, but math
       // with INF seems to vary across PHP versions, systems, and runtimes.
       $pager->setPageSize(0xFFFF);
     }
 
     return $pager;
   }
 
 
   public function executeQuery(
     PhabricatorPolicyAwareQuery $query,
     AphrontView $pager) {
 
     $query->setViewer($this->requireViewer());
 
     if ($this->shouldUseOffsetPaging()) {
       $objects = $query->executeWithOffsetPager($pager);
     } else {
       $objects = $query->executeWithCursorPager($pager);
     }
 
     $this->didExecuteQuery($query);
 
     return $objects;
   }
 
   protected function didExecuteQuery(PhabricatorPolicyAwareQuery $query) {
     return;
   }
 
 
 /* -(  Rendering  )---------------------------------------------------------- */
 
 
   public function setRequest(AphrontRequest $request) {
     $this->request = $request;
     return $this;
   }
 
   public function getRequest() {
     return $this->request;
   }
 
   public function renderResults(
     array $objects,
     PhabricatorSavedQuery $query) {
 
     $phids = $this->getRequiredHandlePHIDsForResultList($objects, $query);
 
     if ($phids) {
       $handles = id(new PhabricatorHandleQuery())
         ->setViewer($this->requireViewer())
         ->witHPHIDs($phids)
         ->execute();
     } else {
       $handles = array();
     }
 
     return $this->renderResultList($objects, $query, $handles);
   }
 
   protected function getRequiredHandlePHIDsForResultList(
     array $objects,
     PhabricatorSavedQuery $query) {
     return array();
   }
 
   abstract protected function renderResultList(
     array $objects,
     PhabricatorSavedQuery $query,
     array $handles);
 
 
 /* -(  Application Search  )------------------------------------------------- */
 
 
   public function getSearchFieldsForConduit() {
     $standard_fields = $this->buildSearchFields();
 
     $fields = array();
     foreach ($standard_fields as $field_key => $field) {
       $conduit_key = $field->getConduitKey();
 
       if (isset($fields[$conduit_key])) {
         $other = $fields[$conduit_key];
         $other_key = $other->getKey();
 
         throw new Exception(
           pht(
             'SearchFields "%s" (of class "%s") and "%s" (of class "%s") both '.
             'define the same Conduit key ("%s"). Keys must be unique.',
             $field_key,
             get_class($field),
             $other_key,
             get_class($other),
             $conduit_key));
       }
 
       $fields[$conduit_key] = $field;
     }
 
     // These are handled separately for Conduit, so don't show them as
     // supported.
     unset($fields['order']);
     unset($fields['limit']);
 
     $viewer = $this->requireViewer();
     foreach ($fields as $key => $field) {
       $field->setViewer($viewer);
     }
 
     return $fields;
   }
 
   public function buildConduitResponse(
     ConduitAPIRequest $request,
     ConduitAPIMethod $method) {
     $viewer = $this->requireViewer();
 
     $query_key = $request->getValue('queryKey');
     $is_empty_query_key = phutil_string_cast($query_key) === '';
     if ($is_empty_query_key) {
       $saved_query = new PhabricatorSavedQuery();
     } else if ($this->isBuiltinQuery($query_key)) {
       $saved_query = $this->buildSavedQueryFromBuiltin($query_key);
     } else {
       $saved_query = id(new PhabricatorSavedQueryQuery())
         ->setViewer($viewer)
         ->withQueryKeys(array($query_key))
         ->executeOne();
       if (!$saved_query) {
         throw new Exception(
           pht(
             'Query key "%s" does not correspond to a valid query.',
             $query_key));
       }
     }
 
     $constraints = $request->getValue('constraints', array());
     if (!is_array($constraints)) {
       throw new Exception(
         pht(
           'Parameter "constraints" must be a map of constraints, got "%s".',
           phutil_describe_type($constraints)));
     }
 
     $fields = $this->getSearchFieldsForConduit();
 
     foreach ($fields as $key => $field) {
       if (!$field->getConduitParameterType()) {
         unset($fields[$key]);
       }
     }
 
     $valid_constraints = array();
     foreach ($fields as $field) {
       foreach ($field->getValidConstraintKeys() as $key) {
         $valid_constraints[$key] = true;
       }
     }
 
     foreach ($constraints as $key => $constraint) {
       if (empty($valid_constraints[$key])) {
         throw new Exception(
           pht(
             'Constraint "%s" is not a valid constraint for this query.',
             $key));
       }
     }
 
     foreach ($fields as $field) {
       if (!$field->getValueExistsInConduitRequest($constraints)) {
         continue;
       }
 
       $value = $field->readValueFromConduitRequest(
         $constraints,
         $request->getIsStrictlyTyped());
       $saved_query->setParameter($field->getKey(), $value);
     }
 
     // NOTE: Currently, when running an ad-hoc query we never persist it into
     // a saved query. We might want to add an option to do this in the future
     // (for example, to enable a CLI-to-Web workflow where user can view more
     // details about results by following a link), but have no use cases for
     // it today. If we do identify a use case, we could save the query here.
 
     $query = $this->buildQueryFromSavedQuery($saved_query);
     $pager = $this->newPagerForSavedQuery($saved_query);
 
     $attachments = $this->getConduitSearchAttachments();
 
     // TODO: Validate this better.
     $attachment_specs = $request->getValue('attachments', array());
     $attachments = array_select_keys(
       $attachments,
       array_keys($attachment_specs));
 
     foreach ($attachments as $key => $attachment) {
       $attachment->setViewer($viewer);
     }
 
     foreach ($attachments as $key => $attachment) {
       $attachment->willLoadAttachmentData($query, $attachment_specs[$key]);
     }
 
     $this->setQueryOrderForConduit($query, $request);
     $this->setPagerLimitForConduit($pager, $request);
     $this->setPagerOffsetsForConduit($pager, $request);
 
     $objects = $this->executeQuery($query, $pager);
 
     $data = array();
     if ($objects) {
       $field_extensions = $this->getConduitFieldExtensions();
 
       $extension_data = array();
       foreach ($field_extensions as $key => $extension) {
         $extension_data[$key] = $extension->loadExtensionConduitData($objects);
       }
 
       $attachment_data = array();
       foreach ($attachments as $key => $attachment) {
         $attachment_data[$key] = $attachment->loadAttachmentData(
           $objects,
           $attachment_specs[$key]);
       }
 
       foreach ($objects as $object) {
         $field_map = $this->getObjectWireFieldsForConduit(
           $object,
           $field_extensions,
           $extension_data);
 
         $attachment_map = array();
         foreach ($attachments as $key => $attachment) {
           $attachment_map[$key] = $attachment->getAttachmentForObject(
             $object,
             $attachment_data[$key],
             $attachment_specs[$key]);
         }
 
         // If this is empty, we still want to emit a JSON object, not a
         // JSON list.
         if (!$attachment_map) {
           $attachment_map = (object)$attachment_map;
         }
 
         $id = (int)$object->getID();
         $phid = $object->getPHID();
 
         $data[] = array(
           'id' => $id,
           'type' => phid_get_type($phid),
           'phid' => $phid,
           'fields' => $field_map,
           'attachments' => $attachment_map,
         );
       }
     }
 
     return array(
       'data' => $data,
       'maps' => $method->getQueryMaps($query),
       'query' => array(
         // This may be `null` if we have not saved the query.
         'queryKey' => $saved_query->getQueryKey(),
       ),
       'cursor' => array(
         'limit' => $pager->getPageSize(),
         'after' => $pager->getNextPageID(),
         'before' => $pager->getPrevPageID(),
         'order' => $request->getValue('order'),
       ),
     );
   }
 
   public function getAllConduitFieldSpecifications() {
     $extensions = $this->getConduitFieldExtensions();
     $object = $this->newQuery()->newResultObject();
 
     $map = array();
     foreach ($extensions as $extension) {
       $specifications = $extension->getFieldSpecificationsForConduit($object);
       foreach ($specifications as $specification) {
         $key = $specification->getKey();
         if (isset($map[$key])) {
           throw new Exception(
             pht(
               'Two field specifications share the same key ("%s"). Each '.
               'specification must have a unique key.',
               $key));
         }
         $map[$key] = $specification;
       }
     }
 
     return $map;
   }
 
   private function getEngineExtensions() {
     $extensions = PhabricatorSearchEngineExtension::getAllEnabledExtensions();
 
     foreach ($extensions as $key => $extension) {
       $extension
         ->setViewer($this->requireViewer())
         ->setSearchEngine($this);
     }
 
     $object = $this->newResultObject();
     foreach ($extensions as $key => $extension) {
       if (!$extension->supportsObject($object)) {
         unset($extensions[$key]);
       }
     }
 
     return $extensions;
   }
 
 
   private function getConduitFieldExtensions() {
     $extensions = $this->getEngineExtensions();
     $object = $this->newResultObject();
 
     foreach ($extensions as $key => $extension) {
       if (!$extension->getFieldSpecificationsForConduit($object)) {
         unset($extensions[$key]);
       }
     }
 
     return $extensions;
   }
 
   private function setQueryOrderForConduit($query, ConduitAPIRequest $request) {
     $order = $request->getValue('order');
     if ($order === null) {
       return;
     }
 
     if (is_scalar($order)) {
       $query->setOrder($order);
     } else {
       $query->setOrderVector($order);
     }
   }
 
   private function setPagerLimitForConduit($pager, ConduitAPIRequest $request) {
     $limit = $request->getValue('limit');
 
     // If there's no limit specified and the query uses a weird huge page
     // size, just leave it at the default gigantic page size. Otherwise,
     // make sure it's between 1 and 100, inclusive.
 
     if ($limit === null) {
       if ($pager->getPageSize() >= 0xFFFF) {
         return;
       } else {
         $limit = 100;
       }
     }
 
     if ($limit > 100) {
       throw new Exception(
         pht(
           'Maximum page size for Conduit API method calls is 100, but '.
           'this call specified %s.',
           $limit));
     }
 
     if ($limit < 1) {
       throw new Exception(
         pht(
           'Minimum page size for API searches is 1, but this call '.
           'specified %s.',
           $limit));
     }
 
     $pager->setPageSize($limit);
   }
 
   private function setPagerOffsetsForConduit(
     $pager,
     ConduitAPIRequest $request) {
     $before_id = $request->getValue('before');
     if ($before_id !== null) {
       $pager->setBeforeID($before_id);
     }
 
     $after_id = $request->getValue('after');
     if ($after_id !== null) {
       $pager->setAfterID($after_id);
     }
   }
 
   protected function getObjectWireFieldsForConduit(
     $object,
     array $field_extensions,
     array $extension_data) {
 
     $fields = array();
     foreach ($field_extensions as $key => $extension) {
       $data = idx($extension_data, $key, array());
       $fields += $extension->getFieldValuesForConduit($object, $data);
     }
 
     return $fields;
   }
 
   public function getConduitSearchAttachments() {
     $extensions = $this->getEngineExtensions();
     $object = $this->newResultObject();
 
     $attachments = array();
     foreach ($extensions as $extension) {
       $extension_attachments = $extension->getSearchAttachments($object);
       foreach ($extension_attachments as $attachment) {
         $attachment_key = $attachment->getAttachmentKey();
         if (isset($attachments[$attachment_key])) {
           $other = $attachments[$attachment_key];
           throw new Exception(
             pht(
               'Two search engine attachments (of classes "%s" and "%s") '.
               'specify the same attachment key ("%s"); keys must be unique.',
               get_class($attachment),
               get_class($other),
               $attachment_key));
         }
         $attachments[$attachment_key] = $attachment;
       }
     }
 
     return $attachments;
   }
 
   final public function renderNewUserView() {
     $body = $this->getNewUserBody();
 
     if (!$body) {
       return null;
     }
 
     return $body;
   }
 
   protected function getNewUserHeader() {
     return null;
   }
 
   protected function getNewUserBody() {
     return null;
   }
 
   public function newUseResultsActions(PhabricatorSavedQuery $saved) {
     return array();
   }
 
 
 /* -(  Export  )------------------------------------------------------------- */
 
 
   public function canExport() {
     $fields = $this->newExportFields();
     return (bool)$fields;
   }
 
   final public function newExportFieldList() {
     $object = $this->newResultObject();
 
     $builtin_fields = array(
       id(new PhabricatorIDExportField())
         ->setKey('id')
         ->setLabel(pht('ID')),
     );
 
     if ($object->getConfigOption(LiskDAO::CONFIG_AUX_PHID)) {
       $builtin_fields[] = id(new PhabricatorPHIDExportField())
         ->setKey('phid')
         ->setLabel(pht('PHID'));
     }
 
     $fields = mpull($builtin_fields, null, 'getKey');
 
     $export_fields = $this->newExportFields();
     foreach ($export_fields as $export_field) {
       $key = $export_field->getKey();
 
       if (isset($fields[$key])) {
         throw new Exception(
           pht(
             'Search engine ("%s") defines an export field with a key ("%s") '.
             'that collides with another field. Each field must have a '.
             'unique key.',
             get_class($this),
             $key));
       }
 
       $fields[$key] = $export_field;
     }
 
     $extensions = $this->newExportExtensions();
     foreach ($extensions as $extension) {
       $extension_fields = $extension->newExportFields();
       foreach ($extension_fields as $extension_field) {
         $key = $extension_field->getKey();
 
         if (isset($fields[$key])) {
           throw new Exception(
             pht(
               'Export engine extension ("%s") defines an export field with '.
               'a key ("%s") that collides with another field. Each field '.
               'must have a unique key.',
               get_class($extension_field),
               $key));
         }
 
         $fields[$key] = $extension_field;
       }
     }
 
     return $fields;
   }
 
   final public function newExport(array $objects) {
     $object = $this->newResultObject();
     $has_phid = $object->getConfigOption(LiskDAO::CONFIG_AUX_PHID);
 
     $objects = array_values($objects);
     $n = count($objects);
 
     $maps = array();
     foreach ($objects as $object) {
       $map = array(
         'id' => $object->getID(),
       );
 
       if ($has_phid) {
         $map['phid'] = $object->getPHID();
       }
 
       $maps[] = $map;
     }
 
     $export_data = $this->newExportData($objects);
     $export_data = array_values($export_data);
     if (count($export_data) !== count($objects)) {
       throw new Exception(
         pht(
           'Search engine ("%s") exported the wrong number of objects, '.
           'expected %s but got %s.',
           get_class($this),
           phutil_count($objects),
           phutil_count($export_data)));
     }
 
     for ($ii = 0; $ii < $n; $ii++) {
       $maps[$ii] += $export_data[$ii];
     }
 
     $extensions = $this->newExportExtensions();
     foreach ($extensions as $extension) {
       $extension_data = $extension->newExportData($objects);
       $extension_data = array_values($extension_data);
       if (count($export_data) !== count($objects)) {
         throw new Exception(
           pht(
             'Export engine extension ("%s") exported the wrong number of '.
             'objects, expected %s but got %s.',
             get_class($extension),
             phutil_count($objects),
             phutil_count($export_data)));
       }
 
       for ($ii = 0; $ii < $n; $ii++) {
         $maps[$ii] += $extension_data[$ii];
       }
     }
 
     return $maps;
   }
 
   protected function newExportFields() {
     return array();
   }
 
   protected function newExportData(array $objects) {
     throw new PhutilMethodNotImplementedException();
   }
 
   private function newExportExtensions() {
     $object = $this->newResultObject();
     $viewer = $this->requireViewer();
 
     $extensions = PhabricatorExportEngineExtension::getAllExtensions();
 
     $supported = array();
     foreach ($extensions as $extension) {
       $extension = clone $extension;
       $extension->setViewer($viewer);
 
       if ($extension->supportsObject($object)) {
         $supported[] = $extension;
       }
     }
 
     return $supported;
   }
 
 }