diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py --- a/rhodecode/apps/home/views.py +++ b/rhodecode/apps/home/views.py @@ -33,7 +33,7 @@ from rhodecode.lib.index import searcher from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int from rhodecode.lib.ext_json import json from rhodecode.model.db import ( - func, true, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup) + func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup) from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.scm import RepoGroupList, RepoList @@ -105,21 +105,27 @@ class HomeView(BaseAppView): return {'suggestions': _user_groups} - def _get_repo_list(self, name_contains=None, repo_type=None, limit=20): + def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20): org_query = name_contains allowed_ids = self._rhodecode_user.repo_acl_ids( ['repository.read', 'repository.write', 'repository.admin'], cache=False, name_filter=name_contains) or [-1] query = Repository.query()\ - .order_by(func.length(Repository.repo_name))\ - .order_by(Repository.repo_name)\ .filter(Repository.archived.isnot(true()))\ .filter(or_( # generate multiple IN to fix limitation problems *in_filter_generator(Repository.repo_id, allowed_ids) )) + query = query.order_by(case( + [ + (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'), + ], + )) + query = query.order_by(func.length(Repository.repo_name)) + query = query.order_by(Repository.repo_name) + if repo_type: query = query.filter(Repository.repo_type == repo_type) @@ -145,20 +151,26 @@ class HomeView(BaseAppView): } for obj in acl_iter] - def _get_repo_group_list(self, name_contains=None, limit=20): + def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20): org_query = name_contains allowed_ids = self._rhodecode_user.repo_group_acl_ids( ['group.read', 'group.write', 'group.admin'], cache=False, name_filter=name_contains) or [-1] query = RepoGroup.query()\ - .order_by(func.length(RepoGroup.group_name))\ - .order_by(RepoGroup.group_name) \ .filter(or_( # generate multiple IN to fix limitation problems *in_filter_generator(RepoGroup.group_id, allowed_ids) )) + query = query.order_by(case( + [ + (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'), + ], + )) + query = query.order_by(func.length(RepoGroup.group_name)) + query = query.order_by(RepoGroup.group_name) + if name_contains: ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) query = query.filter( @@ -183,11 +195,17 @@ class HomeView(BaseAppView): def _get_user_list(self, name_contains=None, limit=20): org_query = name_contains if not name_contains: - return [] + return [], False - name_contains = re.compile('(?:user:)(.+)').findall(name_contains) + # TODO(marcink): should all logged in users be allowed to search others? + allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER + if not allowed_user_search: + return [], False + + name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains) if len(name_contains) != 1: - return [] + return [], False + name_contains = name_contains[0] query = User.query()\ @@ -207,22 +225,28 @@ class HomeView(BaseAppView): { 'id': obj.user_id, 'value': org_query, - 'value_display': obj.username, + 'value_display': 'user: `{}`'.format(obj.username), 'type': 'user', 'icon_link': h.gravatar_url(obj.email, 30), 'url': h.route_path( 'user_profile', username=obj.username) } - for obj in acl_iter] + for obj in acl_iter], True def _get_user_groups_list(self, name_contains=None, limit=20): org_query = name_contains if not name_contains: - return [] + return [], False - name_contains = re.compile('(?:user_group:)(.+)').findall(name_contains) + # TODO(marcink): should all logged in users be allowed to search others? + allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER + if not allowed_user_search: + return [], False + + name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains) if len(name_contains) != 1: - return [] + return [], False + name_contains = name_contains[0] query = UserGroup.query()\ @@ -241,27 +265,34 @@ class HomeView(BaseAppView): { 'id': obj.users_group_id, 'value': org_query, - 'value_display': obj.users_group_name, + 'value_display': 'user_group: `{}`'.format(obj.users_group_name), 'type': 'user_group', 'url': h.route_path( 'user_group_profile', user_group_name=obj.users_group_name) } - for obj in acl_iter] + for obj in acl_iter], True - def _get_hash_commit_list(self, auth_user, searcher, query): + def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None): + repo_name = repo_group_name = None + if repo: + repo_name = repo.repo_name + if repo_group: + repo_group_name = repo_group.group_name + org_query = query if not query or len(query) < 3 or not searcher: - return [] + return [], False - commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query) + commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query) if len(commit_hashes) != 1: - return [] + return [], False + commit_hash = commit_hashes[0] result = searcher.search( 'commit_id:{}*'.format(commit_hash), 'commit', auth_user, - raise_on_exc=False) + repo_name, repo_group_name, raise_on_exc=False) commits = [] for entry in result['results']: @@ -286,22 +317,29 @@ class HomeView(BaseAppView): } commits.append(commit_entry) - return commits + return commits, True - def _get_path_list(self, auth_user, searcher, query): + def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None): + repo_name = repo_group_name = None + if repo: + repo_name = repo.repo_name + if repo_group: + repo_group_name = repo_group.group_name + org_query = query if not query or len(query) < 3 or not searcher: - return [] + return [], False - paths_re = re.compile('(?:file:)(.{1,})').findall(query) + paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query) if len(paths_re) != 1: - return [] + return [], False + file_path = paths_re[0] search_path = searcher.escape_specials(file_path) result = searcher.search( 'file.raw:*{}*'.format(search_path), 'path', auth_user, - raise_on_exc=False) + repo_name, repo_group_name, raise_on_exc=False) files = [] for entry in result['results']: @@ -327,7 +365,7 @@ class HomeView(BaseAppView): } files.append(file_entry) - return files + return files, True @LoginRequired() @view_config( @@ -405,17 +443,15 @@ class HomeView(BaseAppView): qry = query return {'q': qry, 'type': 'content'} label = u'File search for `{}` in this repository.'.format(query) - queries.append( - { - 'id': -10, - 'value': query, - 'value_display': label, - 'type': 'search', - 'url': h.route_path('search_repo', - repo_name=repo_name, - _query=query_modifier()) + file_qry = { + 'id': -10, + 'value': query, + 'value_display': label, + 'type': 'search', + 'url': h.route_path('search_repo', + repo_name=repo_name, + _query=query_modifier()) } - ) # commits def query_modifier(): @@ -423,17 +459,22 @@ class HomeView(BaseAppView): return {'q': qry, 'type': 'commit'} label = u'Commit search for `{}` in this repository.'.format(query) - queries.append( - { - 'id': -20, - 'value': query, - 'value_display': label, - 'type': 'search', - 'url': h.route_path('search_repo', - repo_name=repo_name, - _query=query_modifier()) + commit_qry = { + 'id': -20, + 'value': query, + 'value_display': label, + 'type': 'search', + 'url': h.route_path('search_repo', + repo_name=repo_name, + _query=query_modifier()) } - ) + + if repo_context in ['commit', 'changelog']: + queries.extend([commit_qry, file_qry]) + elif repo_context in ['files', 'summary']: + queries.extend([file_qry, commit_qry]) + else: + queries.extend([commit_qry, file_qry]) elif is_es_6 and repo_group_name: # files @@ -442,17 +483,15 @@ class HomeView(BaseAppView): return {'q': qry, 'type': 'content'} label = u'File search for `{}` in this repository group'.format(query) - queries.append( - { - 'id': -30, - 'value': query, - 'value_display': label, - 'type': 'search', - 'url': h.route_path('search_repo_group', - repo_group_name=repo_group_name, - _query=query_modifier()) + file_qry = { + 'id': -30, + 'value': query, + 'value_display': label, + 'type': 'search', + 'url': h.route_path('search_repo_group', + repo_group_name=repo_group_name, + _query=query_modifier()) } - ) # commits def query_modifier(): @@ -460,18 +499,24 @@ class HomeView(BaseAppView): return {'q': qry, 'type': 'commit'} label = u'Commit search for `{}` in this repository group'.format(query) - queries.append( - { - 'id': -40, - 'value': query, - 'value_display': label, - 'type': 'search', - 'url': h.route_path('search_repo_group', - repo_group_name=repo_group_name, - _query=query_modifier()) + commit_qry = { + 'id': -40, + 'value': query, + 'value_display': label, + 'type': 'search', + 'url': h.route_path('search_repo_group', + repo_group_name=repo_group_name, + _query=query_modifier()) } - ) + if repo_context in ['commit', 'changelog']: + queries.extend([commit_qry, file_qry]) + elif repo_context in ['files', 'summary']: + queries.extend([file_qry, commit_qry]) + else: + queries.extend([commit_qry, file_qry]) + + # Global, not scoped if not queries: queries.append( { @@ -510,65 +555,107 @@ class HomeView(BaseAppView): if not query: return {'suggestions': res} + def no_match(name): + return { + 'id': -1, + 'value': "", + 'value_display': name, + 'type': 'text', + 'url': "" + } searcher = searcher_from_config(self.request.registry.settings) - for _q in self._get_default_search_queries(self.request.GET, searcher, query): - res.append(_q) + has_specialized_search = False + # set repo context + repo = None + repo_id = safe_int(self.request.GET.get('search_context[repo_id]')) + if repo_id: + repo = Repository.get(repo_id) + + # set group context + repo_group = None repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]')) if repo_group_id: repo_group = RepoGroup.get(repo_group_id) - composed_hint = '{}/{}'.format(repo_group.group_name, query) - show_hint = not query.startswith(repo_group.group_name) - if repo_group and show_hint: - hint = u'Repository search inside: `{}`'.format(composed_hint) - res.append({ - 'id': -1, - 'value': composed_hint, - 'value_display': hint, - 'type': 'hint', - 'url': "" - }) + prefix_match = False + + # user: type search + if not prefix_match: + users, prefix_match = self._get_user_list(query) + if users: + has_specialized_search = True + for serialized_user in users: + res.append(serialized_user) + elif prefix_match: + has_specialized_search = True + res.append(no_match('No matching users found')) - repo_groups = self._get_repo_group_list(query) - for serialized_repo_group in repo_groups: - res.append(serialized_repo_group) + # user_group: type search + if not prefix_match: + user_groups, prefix_match = self._get_user_groups_list(query) + if user_groups: + has_specialized_search = True + for serialized_user_group in user_groups: + res.append(serialized_user_group) + elif prefix_match: + has_specialized_search = True + res.append(no_match('No matching user groups found')) - repos = self._get_repo_list(query) - for serialized_repo in repos: - res.append(serialized_repo) + # FTS commit: type search + if not prefix_match: + commits, prefix_match = self._get_hash_commit_list( + c.auth_user, searcher, query, repo, repo_group) + if commits: + has_specialized_search = True + unique_repos = collections.OrderedDict() + for commit in commits: + repo_name = commit['repo'] + unique_repos.setdefault(repo_name, []).append(commit) - # TODO(marcink): should all logged in users be allowed to search others? - allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER - if allowed_user_search: - users = self._get_user_list(query) - for serialized_user in users: - res.append(serialized_user) + for _repo, commits in unique_repos.items(): + for commit in commits: + res.append(commit) + elif prefix_match: + has_specialized_search = True + res.append(no_match('No matching commits found')) - user_groups = self._get_user_groups_list(query) - for serialized_user_group in user_groups: - res.append(serialized_user_group) + # FTS file: type search + if not prefix_match: + paths, prefix_match = self._get_path_list( + c.auth_user, searcher, query, repo, repo_group) + if paths: + has_specialized_search = True + unique_repos = collections.OrderedDict() + for path in paths: + repo_name = path['repo'] + unique_repos.setdefault(repo_name, []).append(path) - commits = self._get_hash_commit_list(c.auth_user, searcher, query) - if commits: - unique_repos = collections.OrderedDict() - for commit in commits: - repo_name = commit['repo'] - unique_repos.setdefault(repo_name, []).append(commit) + for repo, paths in unique_repos.items(): + for path in paths: + res.append(path) + elif prefix_match: + has_specialized_search = True + res.append(no_match('No matching files found')) - for repo, commits in unique_repos.items(): - for commit in commits: - res.append(commit) + # main suggestions + if not has_specialized_search: + repo_group_name = '' + if repo_group: + repo_group_name = repo_group.group_name - paths = self._get_path_list(c.auth_user, searcher, query) - if paths: - unique_repos = collections.OrderedDict() - for path in paths: - repo_name = path['repo'] - unique_repos.setdefault(repo_name, []).append(path) + for _q in self._get_default_search_queries(self.request.GET, searcher, query): + res.append(_q) + + repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name) + for serialized_repo_group in repo_groups: + res.append(serialized_repo_group) - for repo, paths in unique_repos.items(): - for path in paths: - res.append(path) + repos = self._get_repo_list(query, repo_group_name=repo_group_name) + for serialized_repo in repos: + res.append(serialized_repo) + + if not repos and not repo_groups: + res.append(no_match('No matches found')) return {'suggestions': res} diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -2037,6 +2037,6 @@ def get_repo_view_type(request): 'repo_files': 'files', 'repo_summary': 'summary', 'repo_commit': 'commit' + } - } return route_to_view_type.get(route_name) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -39,7 +39,7 @@ from sqlalchemy import ( Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column, Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary, Text, Float, PickleType) -from sqlalchemy.sql.expression import true, false +from sqlalchemy.sql.expression import true, false, case from sqlalchemy.sql.functions import coalesce, count # pragma: no cover from sqlalchemy.orm import ( relationship, joinedload, class_mapper, validates, aliased) diff --git a/rhodecode/public/css/navigation.less b/rhodecode/public/css/navigation.less --- a/rhodecode/public/css/navigation.less +++ b/rhodecode/public/css/navigation.less @@ -644,6 +644,40 @@ ul#context-pages { .main_filter_input_box { display: inline-block; + + .searchItems { + display:flex; + background: #666666; + padding: 0px; + + a { + border: none !important; + } + } + + .searchTag { + line-height: 28px; + padding: 0px 4px; + + .tag { + color: @nav-grey; + border-color: @nav-grey; + } + } + + .searchTagFilter { + background-color: @grey3 !important; + } + + .searchTagHelp { + background-color: @grey2 !important; + } + .searchTagHelp:hover { + background-color: @grey2 !important; + } + .searchTagInput { + background-color: @grey3 !important; + } } .main_filter_box { @@ -667,7 +701,8 @@ ul#context-pages { color: @nav-grey; background: @grey3; min-height: 18px; - + border:none; + border-radius: 0; &:active { color: @grey2 !important; diff --git a/rhodecode/public/js/src/plugins/jquery.autocomplete.js b/rhodecode/public/js/src/plugins/jquery.autocomplete.js --- a/rhodecode/public/js/src/plugins/jquery.autocomplete.js +++ b/rhodecode/public/js/src/plugins/jquery.autocomplete.js @@ -528,7 +528,19 @@ if ($.isFunction(serviceUrl)) { serviceUrl = serviceUrl.call(that.element, query); } - cacheKey = serviceUrl + '?' + $.param(params || {}); + + var callParams = {}; + //make an evaluated copy of params + $.each(params, function(index, value) { + if($.isFunction(value)){ + callParams[index] = value(); + } + else { + callParams[index] = value; + } + }); + + cacheKey = serviceUrl + '?' + $.param(callParams); response = that.cachedResponse[cacheKey]; } @@ -536,7 +548,7 @@ that.suggestions = response.suggestions; that.suggest(); } else if (!that.isBadQuery(query)) { - if (options.onSearchStart.call(that.element, options.params) === false) { + if (options.onSearchStart.call(that.element, params) === false) { return; } if (that.currentRequest) { diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -233,7 +233,6 @@