diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php index 9a056b202a..7595cd0c8e 100644 --- a/src/applications/repository/query/PhabricatorRepositoryQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php @@ -1,551 +1,544 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withCallsigns(array $callsigns) { $this->callsigns = $callsigns; return $this; } public function withIdentifiers(array $identifiers) { $ids = array(); $callsigns = array(); $phids = array(); foreach ($identifiers as $identifier) { if (ctype_digit($identifier)) { $ids[$identifier] = $identifier; } else { $repository_type = PhabricatorRepositoryRepositoryPHIDType::TYPECONST; if (phid_get_type($identifier) === $repository_type) { $phids[$identifier] = $identifier; } else { $callsigns[$identifier] = $identifier; } } } $this->numericIdentifiers = $ids; $this->callsignIdentifiers = $callsigns; $this->phidIdentifiers = $phids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withHosted($hosted) { $this->hosted = $hosted; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } public function withUUIDs(array $uuids) { $this->uuids = $uuids; return $this; } public function withNameContains($contains) { $this->nameContains = $contains; return $this; } public function withRemoteURIs(array $uris) { $this->remoteURIs = $uris; return $this; } public function withDatasourceQuery($query) { $this->datasourceQuery = $query; return $this; } public function needCommitCounts($need_counts) { $this->needCommitCounts = $need_counts; return $this; } public function needMostRecentCommits($need_commits) { $this->needMostRecentCommits = $need_commits; return $this; } public function needProjectPHIDs($need_phids) { $this->needProjectPHIDs = $need_phids; return $this; } public function getBuiltinOrders() { return array( 'committed' => array( 'vector' => array('committed', 'id'), 'name' => pht('Most Recent Commit'), ), 'name' => array( 'vector' => array('name', 'id'), 'name' => pht('Name'), ), 'callsign' => array( 'vector' => array('callsign'), 'name' => pht('Callsign'), ), 'size' => array( 'vector' => array('size', 'id'), 'name' => pht('Size'), ), ) + parent::getBuiltinOrders(); } public function getIdentifierMap() { if ($this->identifierMap === null) { throw new PhutilInvalidStateException('execute'); } return $this->identifierMap; } protected function willExecute() { $this->identifierMap = array(); } - protected function loadPage() { - $table = new PhabricatorRepository(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - '%Q FROM %T r %Q %Q %Q %Q %Q %Q', - $this->buildSelectClause($conn_r), - $table->getTableName(), - $this->buildJoinClause($conn_r), - $this->buildWhereClause($conn_r), - $this->buildGroupClause($conn_r), - $this->buildHavingClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); + public function newResultObject() { + return new PhabricatorRepository(); + } + protected function loadPage() { + $table = $this->newResultObject(); + $data = $this->loadStandardPageRows($table); $repositories = $table->loadAllFromArray($data); if ($this->needCommitCounts) { $sizes = ipull($data, 'size', 'id'); foreach ($repositories as $id => $repository) { $repository->attachCommitCount(nonempty($sizes[$id], 0)); } } if ($this->needMostRecentCommits) { $commit_ids = ipull($data, 'lastCommitID', 'id'); $commit_ids = array_filter($commit_ids); if ($commit_ids) { $commits = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withIDs($commit_ids) ->execute(); } else { $commits = array(); } foreach ($repositories as $id => $repository) { $commit = null; if (idx($commit_ids, $id)) { $commit = idx($commits, $commit_ids[$id]); } $repository->attachMostRecentCommit($commit); } } return $repositories; } protected function willFilterPage(array $repositories) { assert_instances_of($repositories, 'PhabricatorRepository'); // TODO: Denormalize repository status into the PhabricatorRepository // table so we can do this filtering in the database. foreach ($repositories as $key => $repo) { $status = $this->status; switch ($status) { case self::STATUS_OPEN: if (!$repo->isTracked()) { unset($repositories[$key]); } break; case self::STATUS_CLOSED: if ($repo->isTracked()) { unset($repositories[$key]); } break; case self::STATUS_ALL: break; default: throw new Exception("Unknown status '{$status}'!"); } // TODO: This should also be denormalized. $hosted = $this->hosted; switch ($hosted) { case self::HOSTED_PHABRICATOR: if (!$repo->isHosted()) { unset($repositories[$key]); } break; case self::HOSTED_REMOTE: if ($repo->isHosted()) { unset($repositories[$key]); } break; case self::HOSTED_ALL: break; default: throw new Exception(pht("Unknown hosted failed '%s'!", $hosted)); } } // TODO: Denormalize this, too. if ($this->remoteURIs) { $try_uris = $this->getNormalizedPaths(); $try_uris = array_fuse($try_uris); foreach ($repositories as $key => $repository) { if (!isset($try_uris[$repository->getNormalizedPath()])) { unset($repositories[$key]); } } } // Build the identifierMap if ($this->numericIdentifiers) { foreach ($this->numericIdentifiers as $id) { if (isset($repositories[$id])) { $this->identifierMap[$id] = $repositories[$id]; } } } if ($this->callsignIdentifiers) { $repository_callsigns = mpull($repositories, null, 'getCallsign'); foreach ($this->callsignIdentifiers as $callsign) { if (isset($repository_callsigns[$callsign])) { $this->identifierMap[$callsign] = $repository_callsigns[$callsign]; } } } if ($this->phidIdentifiers) { $repository_phids = mpull($repositories, null, 'getPHID'); foreach ($this->phidIdentifiers as $phid) { if (isset($repository_phids[$phid])) { $this->identifierMap[$phid] = $repository_phids[$phid]; } } } return $repositories; } protected function didFilterPage(array $repositories) { if ($this->needProjectPHIDs) { $type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($repositories, 'getPHID')) ->withEdgeTypes(array($type_project)); $edge_query->execute(); foreach ($repositories as $repository) { $project_phids = $edge_query->getDestinationPHIDs( array( $repository->getPHID(), )); $repository->attachProjectPHIDs($project_phids); } } return $repositories; } protected function getPrimaryTableAlias() { return 'r'; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'committed' => array( 'table' => 's', 'column' => 'epoch', 'type' => 'int', 'null' => 'tail', ), 'callsign' => array( 'table' => 'r', 'column' => 'callsign', 'type' => 'string', 'unique' => true, 'reverse' => true, ), 'name' => array( 'table' => 'r', 'column' => 'name', 'type' => 'string', 'reverse' => true, ), 'size' => array( 'table' => 's', 'column' => 'size', 'type' => 'int', 'null' => 'tail', ), ); } protected function willExecuteCursorQuery( PhabricatorCursorPagedPolicyAwareQuery $query) { $vector = $this->getOrderVector(); if ($vector->containsKey('committed')) { $query->needMostRecentCommits(true); } if ($vector->containsKey('size')) { $query->needCommitCounts(true); } } protected function getPagingValueMap($cursor, array $keys) { $repository = $this->loadCursorObject($cursor); $map = array( 'id' => $repository->getID(), 'callsign' => $repository->getCallsign(), 'name' => $repository->getName(), ); foreach ($keys as $key) { switch ($key) { case 'committed': $commit = $repository->getMostRecentCommit(); if ($commit) { $map[$key] = $commit->getEpoch(); } else { $map[$key] = null; } break; case 'size': $count = $repository->getCommitCount(); if ($count) { $map[$key] = $count; } else { $map[$key] = null; } break; } } return $map; } - protected function buildSelectClause(AphrontDatabaseConnection $conn) { - $parts = $this->buildSelectClauseParts($conn); + protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) { + $parts = parent::buildSelectClauseParts($conn); + if ($this->shouldJoinSummaryTable()) { $parts[] = 's.*'; } - return $this->formatSelectClause($parts); + + return $parts; } - protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { - $joins = $this->buildJoinClauseParts($conn_r); + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); if ($this->shouldJoinSummaryTable()) { $joins[] = qsprintf( - $conn_r, + $conn, 'LEFT JOIN %T s ON r.id = s.repositoryID', PhabricatorRepository::TABLE_SUMMARY); } - return $this->formatJoinClause($joins); + return $joins; } private function shouldJoinSummaryTable() { if ($this->needCommitCounts) { return true; } if ($this->needMostRecentCommits) { return true; } $vector = $this->getOrderVector(); if ($vector->containsKey('committed')) { return true; } if ($vector->containsKey('size')) { return true; } return false; } - protected function buildWhereClauseParts(AphrontDatabaseConnection $conn_r) { - $where = parent::buildWhereClauseParts($conn_r); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'r.id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'r.phid IN (%Ls)', $this->phids); } - if ($this->callsigns) { + if ($this->callsigns !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'r.callsign IN (%Ls)', $this->callsigns); } if ($this->numericIdentifiers || $this->callsignIdentifiers || $this->phidIdentifiers) { $identifier_clause = array(); if ($this->numericIdentifiers) { $identifier_clause[] = qsprintf( - $conn_r, + $conn, 'r.id IN (%Ld)', $this->numericIdentifiers); } if ($this->callsignIdentifiers) { $identifier_clause[] = qsprintf( - $conn_r, + $conn, 'r.callsign IN (%Ls)', $this->callsignIdentifiers); } if ($this->phidIdentifiers) { $identifier_clause[] = qsprintf( - $conn_r, + $conn, 'r.phid IN (%Ls)', $this->phidIdentifiers); } $where = array('('.implode(' OR ', $identifier_clause).')'); } if ($this->types) { $where[] = qsprintf( - $conn_r, + $conn, 'r.versionControlSystem IN (%Ls)', $this->types); } if ($this->uuids) { $where[] = qsprintf( - $conn_r, + $conn, 'r.uuid IN (%Ls)', $this->uuids); } if (strlen($this->nameContains)) { $where[] = qsprintf( - $conn_r, + $conn, 'name LIKE %~', $this->nameContains); } if (strlen($this->datasourceQuery)) { // This handles having "rP" match callsigns starting with "P...". $query = trim($this->datasourceQuery); if (preg_match('/^r/', $query)) { $callsign = substr($query, 1); } else { $callsign = $query; } $where[] = qsprintf( - $conn_r, + $conn, 'r.name LIKE %> OR r.callsign LIKE %>', $query, $callsign); } return $where; } public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } private function getNormalizedPaths() { $normalized_uris = array(); // Since we don't know which type of repository this URI is in the general // case, just generate all the normalizations. We could refine this in some // cases: if the query specifies VCS types, or the URI is a git-style URI // or an `svn+ssh` URI, we could deduce how to normalize it. However, this // would be more complicated and it's not clear if it matters in practice. foreach ($this->remoteURIs as $uri) { $normalized_uris[] = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_GIT, $uri); $normalized_uris[] = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_SVN, $uri); $normalized_uris[] = new PhabricatorRepositoryURINormalizer( PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL, $uri); } return array_unique(mpull($normalized_uris, 'getNormalizedPath')); } } diff --git a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php index 21c7fe38ab..e4c1eb14c6 100644 --- a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php +++ b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php @@ -1,282 +1,231 @@ setParameter('callsigns', $request->getStrList('callsigns')); - $saved->setParameter('status', $request->getStr('status')); - $saved->setParameter('order', $request->getStr('order')); - $saved->setParameter('hosted', $request->getStr('hosted')); - $saved->setParameter('types', $request->getArr('types')); - $saved->setParameter('name', $request->getStr('name')); - - $saved->setParameter( - 'projects', - $this->readProjectsFromRequest($request, 'projects')); - - return $saved; - } - - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new PhabricatorRepositoryQuery()) + public function newQuery() { + return id(new PhabricatorRepositoryQuery()) ->needProjectPHIDs(true) ->needCommitCounts(true) ->needMostRecentCommits(true); + } - $callsigns = $saved->getParameter('callsigns'); - if ($callsigns) { - $query->withCallsigns($callsigns); - } - - $status = $saved->getParameter('status'); - $status = idx($this->getStatusValues(), $status); - if ($status) { - $query->withStatus($status); - } + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchStringListField()) + ->setLabel(pht('Callsigns')) + ->setKey('callsigns'), + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Name Contains')) + ->setKey('name'), + id(new PhabricatorSearchSelectField()) + ->setLabel(pht('Status')) + ->setKey('status') + ->setOptions($this->getStatusOptions()), + id(new PhabricatorSearchSelectField()) + ->setLabel(pht('Hosted')) + ->setKey('hosted') + ->setOptions($this->getHostedOptions()), + id(new PhabricatorSearchCheckboxesField()) + ->setLabel(pht('Types')) + ->setKey('types') + ->setOptions(PhabricatorRepositoryType::getAllRepositoryTypes()), + ); + } - $this->setQueryOrder($query, $saved); + public function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); - $hosted = $saved->getParameter('hosted'); - $hosted = idx($this->getHostedValues(), $hosted); - if ($hosted) { - $query->withHosted($hosted); + if ($map['callsigns']) { + $query->withCallsigns($map['callsigns']); } - $types = $saved->getParameter('types'); - if ($types) { - $query->withTypes($types); + if ($map['status']) { + $status = idx($this->getStatusValues(), $map['status']); + if ($status) { + $query->withStatus($status); + } } - $name = $saved->getParameter('name'); - if (strlen($name)) { - $query->withNameContains($name); + if ($map['hosted']) { + $hosted = idx($this->getHostedValues(), $map['hosted']); + if ($hosted) { + $query->withHosted($hosted); + } } - $adjusted = clone $saved; - $adjusted->setParameter('projects', $this->readProjectTokens($saved)); - $this->setQueryProjects($query, $adjusted); - - return $query; - } + if ($map['types']) { + $query->withTypes($map['types']); + } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved_query) { - - $callsigns = $saved_query->getParameter('callsigns', array()); - $types = $saved_query->getParameter('types', array()); - $types = array_fuse($types); - $name = $saved_query->getParameter('name'); - $projects = $this->readProjectTokens($saved_query); - - $form - ->appendChild( - id(new AphrontFormTextControl()) - ->setName('callsigns') - ->setLabel(pht('Callsigns')) - ->setValue(implode(', ', $callsigns))) - ->appendChild( - id(new AphrontFormTextControl()) - ->setName('name') - ->setLabel(pht('Name Contains')) - ->setValue($name)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorProjectLogicalDatasource()) - ->setName('projects') - ->setLabel(pht('Projects')) - ->setValue($projects)) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setName('status') - ->setLabel(pht('Status')) - ->setValue($saved_query->getParameter('status')) - ->setOptions($this->getStatusOptions())) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setName('hosted') - ->setLabel(pht('Hosted')) - ->setValue($saved_query->getParameter('hosted')) - ->setOptions($this->getHostedOptions())); - - $type_control = id(new AphrontFormCheckboxControl()) - ->setLabel(pht('Types')); - - $all_types = PhabricatorRepositoryType::getAllRepositoryTypes(); - foreach ($all_types as $key => $name) { - $type_control->addCheckbox( - 'types[]', - $key, - $name, - isset($types[$key])); + if (strlen($map['name'])) { + $query->withNameContains($map['name']); } - $form->appendChild($type_control); - $this->appendOrderFieldsToForm( - $form, - $saved_query, - new PhabricatorRepositoryQuery()); + return $query; } protected function getURI($path) { return '/diffusion/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'active' => pht('Active Repositories'), 'all' => pht('All Repositories'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'active': return $query->setParameter('status', 'open'); case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } private function getStatusOptions() { return array( '' => pht('Active and Inactive Repositories'), 'open' => pht('Active Repositories'), 'closed' => pht('Inactive Repositories'), ); } private function getStatusValues() { return array( '' => PhabricatorRepositoryQuery::STATUS_ALL, 'open' => PhabricatorRepositoryQuery::STATUS_OPEN, 'closed' => PhabricatorRepositoryQuery::STATUS_CLOSED, ); } private function getHostedOptions() { return array( '' => pht('Hosted and Remote Repositories'), 'phabricator' => pht('Hosted Repositories'), 'remote' => pht('Remote Repositories'), ); } private function getHostedValues() { return array( '' => PhabricatorRepositoryQuery::HOSTED_ALL, 'phabricator' => PhabricatorRepositoryQuery::HOSTED_PHABRICATOR, 'remote' => PhabricatorRepositoryQuery::HOSTED_REMOTE, ); } protected function getRequiredHandlePHIDsForResultList( array $repositories, PhabricatorSavedQuery $query) { return array_mergev(mpull($repositories, 'getProjectPHIDs')); } protected function renderResultList( array $repositories, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($repositories, 'PhabricatorRepository'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); foreach ($repositories as $repository) { $id = $repository->getID(); $item = id(new PHUIObjectItemView()) ->setUser($viewer) ->setHeader($repository->getName()) ->setObjectName('r'.$repository->getCallsign()) ->setHref($this->getApplicationURI($repository->getCallsign().'/')); $commit = $repository->getMostRecentCommit(); if ($commit) { $commit_link = DiffusionView::linkCommit( $repository, $commit->getCommitIdentifier(), $commit->getSummary()); $item->setSubhead($commit_link); $item->setEpoch($commit->getEpoch()); } $item->addIcon( 'none', PhabricatorRepositoryType::getNameForRepositoryType( $repository->getVersionControlSystem())); $size = $repository->getCommitCount(); if ($size) { $history_uri = DiffusionRequest::generateDiffusionURI( array( 'callsign' => $repository->getCallsign(), 'action' => 'history', )); $item->addAttribute( phutil_tag( 'a', array( 'href' => $history_uri, ), pht('%s Commit(s)', new PhutilNumber($size)))); } else { $item->addAttribute(pht('No Commits')); } $project_handles = array_select_keys( $handles, $repository->getProjectPHIDs()); if ($project_handles) { $item->addAttribute( id(new PHUIHandleTagListView()) ->setSlim(true) ->setHandles($project_handles)); } if (!$repository->isTracked()) { $item->setDisabled(true); $item->addIcon('disable-grey', pht('Inactive')); } $list->addItem($item); } return $list; } - private function readProjectTokens(PhabricatorSavedQuery $saved) { - $projects = $saved->getParameter('projects', array()); + protected function willUseSavedQuery(PhabricatorSavedQuery $saved) { + $project_phids = $saved->getParameter('projectPHIDs', array()); + + $old = $saved->getParameter('projects', array()); + foreach ($old as $phid) { + $project_phids[] = $phid; + } $any = $saved->getParameter('anyProjectPHIDs', array()); foreach ($any as $project) { - $projects[] = 'any('.$project.')'; + $project_phids[] = 'any('.$project.')'; } - return $projects; + $saved->setParameter('projectPHIDs', $project_phids); } } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index e1e094a8ff..419447497c 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1,1231 +1,1218 @@ 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 canUseInPanelContext() { return true; } public function saveQuery(PhabricatorSavedQuery $query) { $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 The result of the query. */ public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { + $saved = clone $saved; + $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; } $query = $this->buildQueryFromParameters($map); $object = $this->newResultObject(); if (!$object) { return $query; } if ($object instanceof PhabricatorSubscribableInterface) { if (!empty($map['subscriberPHIDs'])) { $query->withEdgeLogicPHIDs( PhabricatorObjectHasSubscriberEdgeType::EDGECONST, PhabricatorQueryConstraint::OPERATOR_OR, $map['subscriberPHIDs']); } } if ($object instanceof PhabricatorProjectInterface) { if (!empty($map['projectPHIDs'])) { $query->withEdgeLogicConstraints( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $map['projectPHIDs']); } } if ($object instanceof PhabricatorSpacesInterface) { if (!empty($map['spacePHIDs'])) { $query->withSpacePHIDs($map['spacePHIDs']); } } if ($object instanceof PhabricatorCustomFieldInterface) { $this->applyCustomFieldsToQuery($query, $saved); } - $this->setQueryOrder($query, $saved); + $order = $saved->getParameter('order'); + $builtin = $query->getBuiltinOrders(); + if (strlen($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(); $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) { if ($object instanceof PhabricatorSubscribableInterface) { $fields[] = id(new PhabricatorSearchSubscribersField()) ->setLabel(pht('Subscribers')) ->setKey('subscriberPHIDs') ->setAliases(array('subscriber', 'subscribers')); } if ($object instanceof PhabricatorProjectInterface) { $fields[] = id(new PhabricatorSearchProjectsField()) ->setKey('projectPHIDs') ->setAliases(array('project', 'projects')) ->setLabel(pht('Projects')); } if ($object instanceof PhabricatorSpacesInterface) { if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) { $fields[] = id(new PhabricatorSearchSpacesField()) ->setKey('spacePHIDs') ->setAliases(array('space', 'spaces')) ->setLabel(pht('Spaces')); } } } foreach ($this->buildCustomFieldSearchFields() as $custom_field) { $fields[] = $custom_field; } $query = $this->newQuery(); if ($query) { $orders = $query->getBuiltinOrders(); $orders = ipull($orders, 'name'); $fields[] = id(new PhabricatorSearchOrderField()) ->setLabel(pht('Order')) ->setKey('order') ->setOptions($orders); } $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 $this->adjustFieldsForDisplay($field_map); } 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); return $head + $body + $tail; } 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 infrastruture) will be put in the middle. * * @return list Default ordering for field keys. */ protected function getDefaultFieldOrder() { 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/'); } /** * 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'); return $this; } public function loadAllNamedQueries() { $viewer = $this->requireViewer(); $named_queries = id(new PhabricatorNamedQueryQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withEngineClassNames(array(get_class($this))) ->execute(); $named_queries = mpull($named_queries, null, 'getQueryKey'); $builtin = $this->getBuiltinQueries($viewer); $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 = msort($named_queries, 'getSortKey'); return $named_queries + $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; } 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); } } /* -( 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 All available engines. * @task construct */ public static function getAllEngines() { $engines = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); return $engines; } /** * 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($this->requireViewer()->getPHID()) ->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 Other permitted PHID types. * @return list 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 project PHIDs from a request in a flexible way. * * @param AphrontRequest Request to read user PHIDs from. * @param string Key to read in the request. * @return list List of projet PHIDs and selector functions. * @task read */ protected function readProjectsFromRequest(AphrontRequest $request, $key) { $list = $this->readListFromRequest($request, $key); $phids = array(); $slugs = array(); $project_type = PhabricatorProjectProjectPHIDType::TYPECONST; foreach ($list as $item) { $type = phid_get_type($item); if ($type == $project_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 { $slugs[] = $item; } } } if ($slugs) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->requireViewer()) ->withSlugs($slugs) ->execute(); foreach ($projects as $project) { $phids[] = $project->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 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 Optional, list of permitted PHID types. * @return list 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 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 readDateFromRequest( AphrontRequest $request, $key) { $value = AphrontFormDateControlValue::newFromRequest($request, $key); if ($value->isEmpty()) { return null; } return $value->getDictionary(); } protected function readBoolFromRequest( AphrontRequest $request, $key) { if (!strlen($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)); } -/* -( Result Ordering )---------------------------------------------------- */ - - /** - * Set query ordering from a saved value. - */ - protected function setQueryOrder( - PhabricatorCursorPagedPolicyAwareQuery $query, - PhabricatorSavedQuery $saved) { - - $order = $saved->getParameter('order'); - $builtin = $query->getBuiltinOrders(); - - if (strlen($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 $this; - } - - - - protected function appendOrderFieldsToForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved, - PhabricatorCursorPagedPolicyAwareQuery $query) { - - $orders = $query->getBuiltinOrders(); - $orders = ipull($orders, 'name'); - - $form->appendControl( - id(new AphrontFormSelectControl()) - ->setLabel(pht('Order')) - ->setName('order') - ->setOptions($orders) - ->setValue($saved->getParameter('order'))); - } - /* -( Paging and Executing Queries )--------------------------------------- */ public function getPageSize(PhabricatorSavedQuery $saved) { return $saved->getParameter('limit', 100); } public function shouldUseOffsetPaging() { return false; } public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) { if ($this->shouldUseOffsetPaging()) { $pager = new AphrontPagerView(); } 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); } return $objects; } /* -( 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(); } protected function renderResultList( array $objects, PhabricatorSavedQuery $query, array $handles) { throw new Exception(pht('Not supported here yet!')); } /* -( Application Search )------------------------------------------------- */ /** * Retrieve an object to use to define custom fields for this search. * * To integrate with custom fields, subclasses should override this method * and return an instance of the application object which implements * @{interface:PhabricatorCustomFieldInterface}. * * @return PhabricatorCustomFieldInterface|null Object with custom fields. * @task appsearch */ public function getCustomFieldObject() { $object = $this->newResultObject(); if ($object instanceof PhabricatorCustomFieldInterface) { return $object; } return null; } /** * Get the custom fields for this search. * * @return PhabricatorCustomFieldList|null Custom fields, if this search * supports custom fields. * @task appsearch */ public function getCustomFieldList() { if ($this->customFields === false) { $object = $this->getCustomFieldObject(); if ($object) { $fields = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); $fields->setViewer($this->requireViewer()); } else { $fields = null; } $this->customFields = $fields; } return $this->customFields; } /** * Moves data from the request into a saved query. * * @param AphrontRequest Request to read. * @param PhabricatorSavedQuery Query to write to. * @return void * @task appsearch */ protected function readCustomFieldsFromRequest( AphrontRequest $request, PhabricatorSavedQuery $saved) { $list = $this->getCustomFieldList(); if (!$list) { return; } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); $value = $field->readApplicationSearchValueFromRequest( $this, $request); $saved->setParameter($key, $value); } } /** * Applies data from a saved query to an executable query. * * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. * @param PhabricatorSavedQuery Saved query to read. * @return void */ protected function applyCustomFieldsToQuery( PhabricatorCursorPagedPolicyAwareQuery $query, PhabricatorSavedQuery $saved) { $list = $this->getCustomFieldList(); if (!$list) { return; } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); $value = $field->applyApplicationSearchConstraintToQuery( $this, $query, $saved->getParameter($key)); } } protected function applyOrderByToQuery( PhabricatorCursorPagedPolicyAwareQuery $query, array $standard_values, $order) { if (substr($order, 0, 7) === 'custom:') { $list = $this->getCustomFieldList(); if (!$list) { $query->setOrderBy(head($standard_values)); return; } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); if ($key === $order) { $index = $field->buildOrderIndex(); if ($index === null) { $query->setOrderBy(head($standard_values)); return; } $query->withApplicationSearchOrder( $field, $index, false); break; } } } else { $order = idx($standard_values, $order); if ($order) { $query->setOrderBy($order); } else { $query->setOrderBy(head($standard_values)); } } } protected function getCustomFieldOrderOptions() { $list = $this->getCustomFieldList(); if (!$list) { return; } $custom_order = array(); foreach ($list->getFields() as $field) { if ($field->shouldAppearInApplicationSearch()) { if ($field->buildOrderIndex() !== null) { $key = $this->getKeyForCustomField($field); $custom_order[$key] = $field->getFieldName(); } } } return $custom_order; } /** * Get a unique key identifying a field. * * @param PhabricatorCustomField Field to identify. * @return string Unique identifier, suitable for use as an input name. */ public function getKeyForCustomField(PhabricatorCustomField $field) { return 'custom:'.$field->getFieldIndex(); } private function buildCustomFieldSearchFields() { $list = $this->getCustomFieldList(); if (!$list) { return array(); } $fields = array(); foreach ($list->getFields() as $field) { $fields[] = id(new PhabricatorSearchCustomFieldProxyField()) ->setSearchEngine($this) ->setCustomField($field); } return $fields; } // TODO: Remove. protected function appendCustomFieldsToForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $list = $this->getCustomFieldList(); if (!$list) { return; } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); $value = $saved->getParameter($key); $field->appendToApplicationSearchForm($this, $form, $value); } } }