Commit 40b38dd3 authored by Terri Chu's avatar Terri Chu Committed by Matthias Käppler

Backend support for multi-select project search

Allow multi select project search for Advanced Search.
Ensure single project search is supported for Basic Search.

Changelog: changed
EE: true
parent 941e229e
......@@ -9,7 +9,8 @@ module SearchHelper
:repository_ref,
:snippets,
:sort,
:force_search_results
:force_search_results,
:project_ids
].freeze
def search_autocomplete_opts(term)
......
......@@ -8,8 +8,8 @@ module Search
attr_accessor :project, :current_user, :params
def initialize(project, user, params)
@project = project
def initialize(project_or_projects, user, params)
@project = project_or_projects
@current_user = user
@params = params.dup
end
......
......@@ -41,6 +41,10 @@ class SearchService
end
# rubocop: enable CodeReuse/ActiveRecord
def projects
# overridden in EE
end
def show_snippets?
return @show_snippets if defined?(@show_snippets)
......
---
name: advanced_search_multi_project_select
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62606
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333011
milestone: '14.0'
type: development
group: group::global search
default_enabled: false
......@@ -293,6 +293,8 @@ module EE
elasticsearch_indexes_namespace?(scope)
when Project
elasticsearch_indexes_project?(scope)
when Array
scope.any? { |project| elasticsearch_indexes_project?(project) }
else
::Feature.enabled?(:advanced_global_search_for_limited_indexing)
end
......
......@@ -10,15 +10,28 @@ module EE
def execute
return super unless use_elasticsearch? && default_branch?
::Gitlab::Elastic::ProjectSearchResults.new(
current_user,
params[:search],
project: project,
repository_ref: repository_ref,
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
if project.is_a?(Array)
project_ids = Array(project).map(&:id)
::Gitlab::Elastic::SearchResults.new(
current_user,
params[:search],
project_ids,
public_and_internal_projects: false,
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
else
::Gitlab::Elastic::ProjectSearchResults.new(
current_user,
params[:search],
project: project,
repository_ref: repository_ref,
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
end
end
def repository_ref
......
......@@ -2,6 +2,9 @@
module EE
module SearchService
include ::Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override
# This is a proper method instead of a `delegate` in order to
# avoid adding unnecessary methods to Search::SnippetService
def use_elasticsearch?
......@@ -27,5 +30,31 @@ module EE
def show_elasticsearch_tabs?
::Gitlab::CurrentSettings.search_using_elasticsearch?(scope: search_service.elasticsearchable_scope)
end
# rubocop: disable CodeReuse/ActiveRecord
# rubocop: disable Gitlab/ModuleWithInstanceVariables
override :projects
def projects
strong_memoize(:projects) do
next unless params[:project_ids].present? && params[:project_ids].is_a?(String)
next unless group.present? && ::Feature.enabled?(:advanced_search_multi_project_select, group)
project_ids = params[:project_ids].split(',')
the_projects = ::Project.where(id: project_ids)
allowed_projects = the_projects.find_all { |p| can?(current_user, :read_project, p) }
allowed_projects.presence
end
end
# rubocop: enable Gitlab/ModuleWithInstanceVariables
# rubocop: enable CodeReuse/ActiveRecord
private
override :search_service
def search_service
return super unless projects
@search_service ||= ::Search::ProjectService.new(projects, current_user, params) # rubocop: disable Gitlab/ModuleWithInstanceVariables
end
end
end
......@@ -37,13 +37,13 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic, :clean_gitlab_re
let_it_be(:private_project) { create(:project, :repository, :wiki_repo) }
before do
[project, private_project].each do |project|
create(:note, note: 'bla-bla term', project: project)
project.wiki.create_page('index_page', 'term')
project.wiki.index_wiki_blobs
[project, private_project].each do |p|
create(:note, note: 'bla-bla term', project: p)
p.wiki.create_page('index_page', 'term')
p.wiki.index_wiki_blobs
p.repository.index_commits_and_blobs
end
project.repository.index_commits_and_blobs
ensure_elasticsearch_index!
end
......@@ -58,8 +58,6 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic, :clean_gitlab_re
end
context 'visibility checks' do
let_it_be(:project) { create(:project, :public, :wiki_repo) }
let(:query) { 'term' }
before do
......
......@@ -546,6 +546,24 @@ RSpec.describe ApplicationSetting do
it { is_expected.to eq(only_when_enabled_globally) }
end
context 'array of projects (all in scope)' do
let(:scope) { [included_project] }
it { is_expected.to eq(indexing && searching) }
end
context 'array of projects (all not in scope)' do
let(:scope) { [excluded_project] }
it { is_expected.to eq(only_when_enabled_globally) }
end
context 'array of projects (some in scope)' do
let(:scope) { [included_project, excluded_project] }
it { is_expected.to eq(indexing && searching) }
end
end
end
......
......@@ -11,10 +11,21 @@ RSpec.describe Search::ProjectService do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
it_behaves_like 'EE search service shared examples', ::Gitlab::ProjectSearchResults, ::Gitlab::Elastic::ProjectSearchResults do
let(:user) { scope.owner }
let(:scope) { create(:project) }
let(:service) { described_class.new(scope, user, params) }
context 'when a single project provided' do
it_behaves_like 'EE search service shared examples', ::Gitlab::ProjectSearchResults, ::Gitlab::Elastic::ProjectSearchResults do
let(:user) { scope.owner }
let(:scope) { create(:project) }
let(:service) { described_class.new(scope, user, params) }
end
end
context 'when a multiple projects provided' do
it_behaves_like 'EE search service shared examples', ::Gitlab::ProjectSearchResults, ::Gitlab::Elastic::SearchResults do
let(:user) { group.owner }
let(:group) { create(:group) }
let(:scope) { create_list(:project, 3, namespace: group) }
let(:service) { described_class.new( scope, user, params) }
end
end
context 'code search' do
......
......@@ -76,7 +76,7 @@ RSpec.describe SearchService do
end
context 'redacting search results' do
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
# Resources the user has access to
let(:project) { create(:project) }
......@@ -218,4 +218,97 @@ RSpec.describe SearchService do
end
end
end
describe '#projects' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:accessible_project) { create(:project, :public, namespace: group) }
let_it_be(:inaccessible_project) { create(:project, :private, namespace: group) }
before do
stub_feature_flags(advanced_search_multi_project_select: group)
end
context 'when all projects are accessible' do
let_it_be(:accessible_project_2) { create(:project, :public, namespace: group) }
it 'returns the project' do
project_ids = [accessible_project.id, accessible_project_2.id].join(',')
projects = described_class.new(user, group_id: group.id, project_ids: project_ids).projects
expect(projects).to match [accessible_project, accessible_project_2]
end
it 'returns the projects for guests' do
search_project = create :project
search_project.add_guest(user)
project_ids = [accessible_project.id, accessible_project_2.id, search_project.id].join(',')
projects = described_class.new(user, group_id: group.id, project_ids: project_ids).projects
expect(projects).to match [accessible_project, accessible_project_2, search_project]
end
it 'handles spaces in the param' do
project_ids = [accessible_project.id, accessible_project_2.id].join(', ')
projects = described_class.new(user, group_id: group.id, project_ids: project_ids).projects
expect(projects).to match [accessible_project, accessible_project_2]
end
it 'returns nil if projects param is not a String' do
project_ids = accessible_project.id
projects = described_class.new(user, group_id: group.id, project_ids: project_ids).projects
expect(projects).to be_nil
end
end
context 'when some projects are accessible' do
it 'returns only accessible projects' do
project_ids = [accessible_project.id, inaccessible_project.id].join(',')
projects = described_class.new(user, group_id: group.id, project_ids: project_ids).projects
expect(projects).to match [accessible_project]
end
end
context 'when no projects are accessible' do
it 'returns nil' do
project_ids = "#{inaccessible_project.id}"
projects = described_class.new(user, group_id: group.id, project_ids: project_ids).projects
expect(projects).to be_nil
end
end
context 'when no project_ids are provided' do
it 'returns nil' do
projects = described_class.new(user).projects
expect(projects).to be_nil
end
end
context 'when no group_id provided' do
it 'returns nil' do
project_ids = "#{accessible_project.id}"
projects = described_class.new(user, project_ids: project_ids).projects
expect(projects).to be_nil
end
end
context 'when the advanced_search_multi_project_select feature is not enabled for the group' do
before do
stub_feature_flags(advanced_search_multi_project_select: false)
end
it 'returns nil' do
project_ids = "#{accessible_project.id}"
projects = described_class.new(user, group_id: group.id, project_ids: project_ids).projects
expect(projects).to be_nil
end
end
end
end
......@@ -43,9 +43,20 @@ module Gitlab
end
end
# rubocop:disable CodeReuse/ActiveRecord
def users
super.where(id: @project.team.members) # rubocop:disable CodeReuse/ActiveRecord
results = super
if @project.is_a?(Array)
team_members_for_projects = User.joins(:project_authorizations).where(project_authorizations: { project_id: @project })
results = results.where(id: team_members_for_projects)
else
results = results.where(id: @project.team.members)
end
results
end
# rubocop:enable CodeReuse/ActiveRecord
def limited_blobs_count
@limited_blobs_count ||= blobs(limit: count_limit).count
......
......@@ -549,30 +549,39 @@ RSpec.describe Gitlab::ProjectSearchResults do
describe 'user search' do
let(:query) { 'gob' }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let_it_be(:user_1) { create(:user, username: 'gob_bluth') }
let_it_be(:user_2) { create(:user, username: 'michael_bluth') }
let_it_be(:user_3) { create(:user, username: 'gob_2018') }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
subject(:objects) { results.objects('users') }
it 'returns the user belonging to the project matching the search query' do
user1 = create(:user, username: 'gob_bluth')
create(:project_member, :developer, user: user1, project: project)
create(:project_member, :developer, user: user_1, project: project)
create(:project_member, :developer, user: user_2, project: project)
user2 = create(:user, username: 'michael_bluth')
create(:project_member, :developer, user: user2, project: project)
expect(objects).to contain_exactly(user_1)
end
create(:user, username: 'gob_2018')
it 'returns the user belonging to the group matching the search query' do
create(:group_member, :developer, user: user_1, group: group)
expect(objects).to contain_exactly(user1)
expect(objects).to contain_exactly(user_1)
end
it 'returns the user belonging to the group matching the search query' do
user1 = create(:user, username: 'gob_bluth')
create(:group_member, :developer, user: user1, group: group)
context 'when multiple projects provided' do
let_it_be(:project_2) { create(:project, namespace: group) }
subject(:results) { described_class.new(user, query, project: [project, project_2], repository_ref: repository_ref, filters: filters) }
create(:user, username: 'gob_2018')
it 'returns users belonging to projects matching the search query' do
create(:project_member, :developer, user: user_1, project: project)
create(:project_member, :developer, user: user_3, project: project_2)
expect(objects).to contain_exactly(user1)
expect(objects).to contain_exactly(user_1, user_3)
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment