diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php index 92d8b95adf..327fe36ba8 100644 --- a/src/applications/repository/query/PhabricatorRepositoryQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php @@ -1,325 +1,338 @@ 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 withStatus($status) { $this->status = $status; 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 needCommitCounts($need_counts) { $this->needCommitCounts = $need_counts; return $this; } public function needMostRecentCommits($need_commits) { $this->needMostRecentCommits = $need_commits; return $this; } public function setOrder($order) { $this->order = $order; return $this; } protected function loadPage() { $table = new PhabricatorRepository(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T r %Q %Q %Q %Q', $table->getTableName(), $this->buildJoinsClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $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; } public 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}'!"); } } return $repositories; } public function getReversePaging() { switch ($this->order) { case self::ORDER_CALLSIGN: case self::ORDER_NAME: return true; } return false; } protected function getPagingColumn() { $order = $this->order; switch ($order) { case self::ORDER_CREATED: return 'r.id'; case self::ORDER_COMMITTED: return 's.epoch'; case self::ORDER_CALLSIGN: return 'r.callsign'; case self::ORDER_NAME: return 'r.name'; default: throw new Exception("Unknown order '{$order}!'"); } } private function loadCursorObject($id) { $query = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getPagingViewer()) ->withIDs(array((int)$id)); if ($this->order == self::ORDER_COMMITTED) { $query->needMostRecentCommits(true); } $results = $query->execute(); return head($results); } protected function buildPagingClause(AphrontDatabaseConnection $conn_r) { $default = parent::buildPagingClause($conn_r); $before_id = $this->getBeforeID(); $after_id = $this->getAfterID(); if (!$before_id && !$after_id) { return $default; } $order = $this->order; if ($order == self::ORDER_CREATED) { return $default; } if ($before_id) { $cursor = $this->loadCursorObject($before_id); } else { $cursor = $this->loadCursorObject($after_id); } if (!$cursor) { return null; } $id_column = array( 'name' => 'r.id', 'type' => 'int', 'value' => $cursor->getID(), ); $columns = array(); switch ($order) { case self::ORDER_COMMITTED: $commit = $cursor->getMostRecentCommit(); if (!$commit) { return null; } $columns[] = array( 'name' => 's.epoch', 'type' => 'int', 'value' => $commit->getEpoch(), ); $columns[] = $id_column; break; case self::ORDER_CALLSIGN: $columns[] = array( 'name' => 'r.callsign', 'type' => 'string', 'value' => $cursor->getCallsign(), 'reverse' => true, ); break; case self::ORDER_NAME: $columns[] = array( 'name' => 'r.name', 'type' => 'string', 'value' => $cursor->getName(), 'reverse' => true, ); $columns[] = $id_column; break; default: throw new Exception("Unknown order '{$order}'!"); } return $this->buildPagingClauseFromMultipleColumns( $conn_r, $columns, array( // TODO: Clean up the column ordering stuff and then make this // depend on getReversePaging(). 'reversed' => (bool)($before_id), )); } private function buildJoinsClause(AphrontDatabaseConnection $conn_r) { $joins = array(); $join_summary_table = $this->needCommitCounts || $this->needMostRecentCommits || ($this->order == self::ORDER_COMMITTED); if ($join_summary_table) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T s ON r.id = s.repositoryID', PhabricatorRepository::TABLE_SUMMARY); } return implode(' ', $joins); } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'r.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'r.phid IN (%Ls)', $this->phids); } if ($this->callsigns) { $where[] = qsprintf( $conn_r, 'r.callsign IN (%Ls)', $this->callsigns); } if ($this->types) { $where[] = qsprintf( $conn_r, 'r.versionControlSystem IN (%Ls)', $this->types); } if ($this->uuids) { $where[] = qsprintf( $conn_r, 'r.uuid IN (%Ls)', $this->uuids); } + if (strlen($this->nameContains)) { + $where[] = qsprintf( + $conn_r, + 'name LIKE %~', + $this->nameContains); + } + $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorApplicationDiffusion'; } } diff --git a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php index 5c6595426a..41dff1056e 100644 --- a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php +++ b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php @@ -1,155 +1,167 @@ setParameter('callsigns', $request->getStrList('callsigns')); $saved->setParameter('status', $request->getStr('status')); $saved->setParameter('order', $request->getStr('order')); $saved->setParameter('types', $request->getArr('types')); + $saved->setParameter('name', $request->getStr('name')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorRepositoryQuery()) ->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); } $order = $saved->getParameter('order'); $order = idx($this->getOrderValues(), $order); if ($order) { $query->setOrder($order); } else { $query->setOrder(head($this->getOrderValues())); } $types = $saved->getParameter('types'); if ($types) { $query->withTypes($types); } + $name = $saved->getParameter('name'); + if (strlen($name)) { + $query->withNameContains($name); + } + return $query; } 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'); $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)) ->appendChild( id(new AphrontFormSelectControl()) ->setName('status') ->setLabel(pht('Status')) ->setValue($saved_query->getParameter('status')) ->setOptions($this->getStatusOptions())); $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])); } $form ->appendChild($type_control) ->appendChild( id(new AphrontFormSelectControl()) ->setName('order') ->setLabel(pht('Order')) ->setValue($saved_query->getParameter('order')) ->setOptions($this->getOrderOptions())); } protected function getURI($path) { return '/diffusion/'.$path; } public 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 getOrderOptions() { return array( 'committed' => pht('Most Recent Commit'), 'name' => pht('Name'), 'callsign' => pht('Callsign'), 'created' => pht('Date Created'), ); } private function getOrderValues() { return array( 'committed' => PhabricatorRepositoryQuery::ORDER_COMMITTED, 'name' => PhabricatorRepositoryQuery::ORDER_NAME, 'callsign' => PhabricatorRepositoryQuery::ORDER_CALLSIGN, 'created' => PhabricatorRepositoryQuery::ORDER_CREATED, ); } } diff --git a/src/applications/search/engine/PhabricatorJumpNavHandler.php b/src/applications/search/engine/PhabricatorJumpNavHandler.php index b0e80962f0..e3081dfebd 100644 --- a/src/applications/search/engine/PhabricatorJumpNavHandler.php +++ b/src/applications/search/engine/PhabricatorJumpNavHandler.php @@ -1,111 +1,126 @@ 'uri:'.$help_href, '/^a$/i' => 'uri:/audit/', '/^f$/i' => 'uri:/feed/', '/^d$/i' => 'uri:/differential/', '/^r$/i' => 'uri:/diffusion/', '/^t$/i' => 'uri:/maniphest/', '/^p$/i' => 'uri:/project/', '/^u$/i' => 'uri:/people/', '/^p\s+(.+)$/i' => 'project', '/^#(.+)$/i' => 'project', '/^u\s+(\S+)$/i' => 'user', '/^@(.+)$/i' => 'user', '/^task:\s*(.+)/i' => 'create-task', '/^(?:s|symbol)\s+(\S+)/i' => 'find-symbol', + '/^r\s+(.+)$/i' => 'find-repository', ); foreach ($patterns as $pattern => $effect) { $matches = null; if (preg_match($pattern, $jump, $matches)) { if (!strncmp($effect, 'uri:', 4)) { return id(new AphrontRedirectResponse()) ->setURI(substr($effect, 4)); } else { switch ($effect) { case 'user': return id(new AphrontRedirectResponse()) ->setURI('/p/'.$matches[1].'/'); case 'project': $project = self::findCloselyNamedProject($matches[1]); if ($project) { return id(new AphrontRedirectResponse()) ->setURI('/project/view/'.$project->getID().'/'); } else { $jump = $matches[1]; } break; case 'find-symbol': $context = ''; $symbol = $matches[1]; $parts = array(); if (preg_match('/(.*)(?:\\.|::|->)(.*)/', $symbol, $parts)) { $context = '&context='.phutil_escape_uri($parts[1]); $symbol = $parts[2]; } return id(new AphrontRedirectResponse()) ->setURI("/diffusion/symbol/$symbol/?jump=true$context"); + case 'find-repository': + $name = $matches[1]; + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withNameContains($name) + ->execute(); + if (count($repositories) == 1) { + // Just one match, jump to repository. + $uri = '/diffusion/'.head($repositories)->getCallsign().'/'; + } else { + // More than one match, jump to search. + $uri = urisprintf('/diffusion/?order=name&name=%s', $name); + } + return id(new AphrontRedirectResponse())->setURI($uri); case 'create-task': return id(new AphrontRedirectResponse()) ->setURI('/maniphest/task/create/?title=' .phutil_escape_uri($matches[1])); default: throw new Exception("Unknown jump effect '{$effect}'!"); } } } } // If none of the patterns matched, look for an object by name. $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withNames(array($jump)) ->execute(); if (count($objects) == 1) { $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(mpull($objects, 'getPHID')) ->executeOne(); return id(new AphrontRedirectResponse())->setURI($handle->getURI()); } return null; } private static function findCloselyNamedProject($name) { $project = id(new PhabricatorProject())->loadOneWhere( 'name = %s', $name); if ($project) { return $project; } else { // no exact match, try a fuzzy match $projects = id(new PhabricatorProject())->loadAllWhere( 'name LIKE %~', $name); if ($projects) { $min_name_length = PHP_INT_MAX; $best_project = null; foreach ($projects as $project) { $name_length = strlen($project->getName()); if ($name_length <= $min_name_length) { $min_name_length = $name_length; $best_project = $project; } } return $best_project; } else { return null; } } } } diff --git a/src/docs/user/userguide/jump.diviner b/src/docs/user/userguide/jump.diviner index 06b1e4fa9d..5910484760 100644 --- a/src/docs/user/userguide/jump.diviner +++ b/src/docs/user/userguide/jump.diviner @@ -1,28 +1,29 @@ @title Jump Nav User Guide @group userguide Command reference for the jump nav. = Overview = The jump nav provides a quick way to navigate to tools and objects: just type a navigational command into the box and press return. = Supported Commands = - **help** - Jump to this document. - **T** - Jump to Maniphest. - **T123** - Jump to Maniphest Task 123. - **D** - Jump to Differential. - **D123** - Jump to Differential Revision 123. - **r** - Jump to Diffusion. - **rXYZ** - Jump to Diffusion Repository XYZ. - **rXYZabcdef** - Jump to Diffusion Commit rXYZabcdef. + - **r ** - Search for repositories by name. - **u** - Jump to People - **u username** - Jump to username's Profile - **p** - Jump to Project - **p Some Project** - Jump to Project: Some Project - **s SymbolName** - Jump to Symbol SymbolName - **task: (new title)** - Jumps to Task Creation Page with pre-filled title. - **(default)** - Search for input.