diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 2187126272d9db6829fc327a09e51779eac2a2fc..04ab6fe6afb80fd901fbc07c320097ee3da055ab 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -608,7 +608,14 @@ def feature_flag_tab_enabled?(flag) def sanitized_search_params sanitized_params = params.dup - sanitized_params[:confidential] = Gitlab::Utils.to_boolean(sanitized_params[:confidential]) if sanitized_params.key?(:confidential) + + if sanitized_params.key?(:confidential) + sanitized_params[:confidential] = Gitlab::Utils.to_boolean(sanitized_params[:confidential]) + end + + if sanitized_params.key?(:include_archived) + sanitized_params[:include_archived] = Gitlab::Utils.to_boolean(sanitized_params[:include_archived]) + end sanitized_params end diff --git a/app/services/concerns/search/filter.rb b/app/services/concerns/search/filter.rb index c358f49eef151855376e60e822e6b0019d22e127..e234edcfce435be3afea295492154662167eff76 100644 --- a/app/services/concerns/search/filter.rb +++ b/app/services/concerns/search/filter.rb @@ -5,7 +5,7 @@ module Filter private def filters - { state: params[:state], confidential: params[:confidential] } + { state: params[:state], confidential: params[:confidential], include_archived: params[:include_archived] } end end end diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index 2d4952dacfd485bbd2f454bec2e22604eec4f06d..85ca0b850f5ba95cf054c591751c664060bbe74e 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -25,7 +25,7 @@ def execute # rubocop: disable CodeReuse/ActiveRecord def projects - @projects ||= ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute.preload(:topics, :project_topics) + @projects ||= ProjectsFinder.new(current_user: current_user).execute.preload(:topics, :project_topics) end def allowed_scopes diff --git a/doc/user/search/index.md b/doc/user/search/index.md index f6733abd305ad0af388b3edb3c1b039aaba86525..24bc31a103f628c9bcb50aea5ca6155d1179337e 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -96,8 +96,6 @@ To filter code search results by one or more languages: ## Search for projects by full path > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108906) in GitLab 15.9 [with a flag](../../administration/feature_flags.md) named `full_path_project_search`. Disabled by default. -> - [Enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/388473) on GitLab.com in GitLab 15.9. -> - [Enabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111808) on self-managed GitLab 15.10. > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114932) in GitLab 15.11. Feature flag `full_path_project_search` removed. You can search for a project by entering its full path (including the namespace it belongs to) in the search box. @@ -108,6 +106,12 @@ For example, the search query: - `gitlab-org/gitlab` searches for the `gitlab` project in the `gitlab-org` namespace. - `gitlab-org/` displays autocomplete suggestions for projects that belong to the `gitlab-org` namespace. +### Search archived projects + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121981) in GitLab 16.1. + +By default, archived projects are excluded from the search results. To include archived projects, add the parameter `include_archived=true` to the URL. + ## Search for a SHA You can search for a commit SHA. diff --git a/ee/lib/elastic/latest/project_class_proxy.rb b/ee/lib/elastic/latest/project_class_proxy.rb index b22cb80243d4f64737d0f1a75a0c86819e804b56..98071b96f2e496a40dbd3442288c58d64d21337b 100644 --- a/ee/lib/elastic/latest/project_class_proxy.rb +++ b/ee/lib/elastic/latest/project_class_proxy.rb @@ -20,11 +20,11 @@ def elastic_search(query, options: {}) } end - if options[:non_archived] + unless options[:include_archived] filters << { terms: { - _name: context.name(:not_archived), - archived: [!options[:non_archived]].flatten + _name: context.name(:archived, false), + archived: [false] } } end diff --git a/ee/lib/gitlab/elastic/search_results.rb b/ee/lib/gitlab/elastic/search_results.rb index a2238606e648668035746ed34335b9abf07e7c2b..17614606c964d6bed635153f9c49dbfff652d1c9 100644 --- a/ee/lib/gitlab/elastic/search_results.rb +++ b/ee/lib/gitlab/elastic/search_results.rb @@ -269,6 +269,8 @@ def base_options def scope_options(scope) case scope + when :projects + base_options.merge(filters.slice(:include_archived)) when :merge_requests base_options.merge(filters.slice(:order_by, :sort, :state)) when :issues diff --git a/ee/spec/lib/elastic/latest/project_class_proxy_spec.rb b/ee/spec/lib/elastic/latest/project_class_proxy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..360fa85043224f239535eec1543f2fd75a7a8a82 --- /dev/null +++ b/ee/spec/lib/elastic/latest/project_class_proxy_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Elastic::Latest::ProjectClassProxy, feature_category: :global_search do + subject { described_class.new(Project) } + + let(:query) { 'blob' } + let(:options) { {} } + let(:elastic_search) { subject.elastic_search(query, options: options) } + let(:request) { Elasticsearch::Model::Searching::SearchRequest.new(Project, '*') } + let(:response) do + Elasticsearch::Model::Response::Response.new(Project, request) + end + + before do + stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) + end + + describe '#elastic_search' do + describe 'query', :elastic_delete_by_query do + it 'has the correct named queries' do + elastic_search.response + + assert_named_queries( + 'project:match:search_terms', + 'doc:is_a:project', + 'project:archived:false' + ) + end + + context 'when project_ids is set' do + let(:options) { { project_ids: [create(:project).id] } } + + it 'has the correct named queries' do + elastic_search.response + + assert_named_queries( + 'project:match:search_terms', + 'doc:is_a:project', + 'project:membership:id', + 'project:archived:false' + ) + end + end + + context 'when include_archived is set' do + let(:options) { { include_archived: true } } + + it 'does not have a filter for archived' do + elastic_search.response + + assert_named_queries( + 'project:match:search_terms', + 'doc:is_a:project' + ) + end + end + end + end +end diff --git a/ee/spec/lib/gitlab/elastic/group_search_results_spec.rb b/ee/spec/lib/gitlab/elastic/group_search_results_spec.rb index 63458350197a94bdb4dda85d3bb1f6116476e3f6..548b315368270236ee79f11cf056a887a3d6d4ac 100644 --- a/ee/spec/lib/gitlab/elastic/group_search_results_spec.rb +++ b/ee/spec/lib/gitlab/elastic/group_search_results_spec.rb @@ -57,6 +57,19 @@ it_behaves_like 'search results filtered by language' end + context 'for projects' do + let!(:unarchived_project) { create(:project, :public, group: group) } + let!(:archived_project) { create(:project, :archived, :public, group: group) } + + let(:scope) { 'projects' } + + it_behaves_like 'search results filtered by archived' do + before do + ensure_elasticsearch_index! + end + end + end + describe 'users' do let(:query) { 'john' } let(:scope) { 'users' } diff --git a/ee/spec/lib/gitlab/elastic/search_results_spec.rb b/ee/spec/lib/gitlab/elastic/search_results_spec.rb index 57dec8386064bc2cc69a985da30e72c453222a03..2df30b673b14925b3bc23f9f17de277b97bac16a 100644 --- a/ee/spec/lib/gitlab/elastic/search_results_spec.rb +++ b/ee/spec/lib/gitlab/elastic/search_results_spec.rb @@ -373,6 +373,20 @@ include_examples 'search results filtered by state' include_examples 'search results filtered by confidential' include_examples 'search results filtered by labels' + + context 'for projects' do + let_it_be(:group) { create(:group) } + let!(:unarchived_project) { create(:project, :public, group: group) } + let!(:archived_project) { create(:project, :archived, :public, group: group) } + + let(:results) { described_class.new(user, '*', [unarchived_project.id, archived_project.id], filters: filters) } + + it_behaves_like 'search results filtered by archived' do + before do + ensure_elasticsearch_index! + end + end + end end context 'ordering' do diff --git a/ee/spec/services/search/project_service_spec.rb b/ee/spec/services/search/project_service_spec.rb index 6e8311cd026f1946304f605d5e6200a1f7c0c0d3..5be3b14d938c7eec1e906965c7584169fd8b7750 100644 --- a/ee/spec/services/search/project_service_spec.rb +++ b/ee/spec/services/search/project_service_spec.rb @@ -35,7 +35,7 @@ repository_ref: params[:repository_ref], order_by: params[:order_by], sort: params[:sort], - filters: { confidential: nil, state: nil, language: nil, labels: nil } + filters: { confidential: nil, state: nil, language: nil, labels: nil, include_archived: nil } ).and_return search_results expect(search_service.execute).to eq(search_results) diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 93befc2df57458fed646d27b99821d9a494af709..5d2dc2f7d1942ec70ad4798a4d121519f00326a9 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -174,7 +174,9 @@ def apply_sort(results, scope: nil) # rubocop: enable CodeReuse/ActiveRecord def projects - limit_projects.search(query) + scope = limit_projects + scope = filters[:include_archived] ? scope : scope.non_archived + scope.search(query) end def issues(finder_params = {}) diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 497e2d84f4fcde5c6be012c1a40f8c73de44d490..6c48962210d62d54d949ffa91d89d992b16ba0cd 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -557,6 +557,7 @@ def request project_id: '456', project_ids: %w(456 789), confidential: true, + include_archived: true, state: true, force_search_results: true, language: 'ruby' diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 2cea577a852d78620772caca101be9942b636241..807733b13bb207b48530b8bba3675415130d9305 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -711,22 +711,38 @@ def simple_sanitize(str) allow(self).to receive(:current_user).and_return(:the_current_user) end - where(:confidential, :expected) do + where(:input, :expected) do '0' | false '1' | true 'yes' | true 'no' | false + 'true' | true + 'false' | false true | true false | false end - let(:params) { { confidential: confidential } } + describe 'for confidential' do + let(:params) { { confidential: input } } - with_them do - it 'transforms confidentiality param' do - expect(::SearchService).to receive(:new).with(:the_current_user, { confidential: expected }) + with_them do + it 'transforms param' do + expect(::SearchService).to receive(:new).with(:the_current_user, { confidential: expected }) - subject + subject + end + end + end + + describe 'for include_archived' do + let(:params) { { include_archived: input } } + + with_them do + it 'transforms param' do + expect(::SearchService).to receive(:new).with(:the_current_user, { include_archived: expected }) + + subject + end end end end diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb index ec96a069b8fff11b7203c69ff5ec891760fcee77..1206a1c913194d9304243abe17005e7e8f7be033 100644 --- a/spec/lib/gitlab/group_search_results_spec.rb +++ b/spec/lib/gitlab/group_search_results_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GroupSearchResults do +RSpec.describe Gitlab::GroupSearchResults, feature_category: :global_search do # group creation calls GroupFinder, so need to create the group # before so expect(GroupsFinder) check works let_it_be(:group) { create(:group) } @@ -46,6 +46,19 @@ include_examples 'search results filtered by state' end + describe '#projects' do + let(:scope) { 'projects' } + let(:query) { 'Test' } + + describe 'filtering' do + let_it_be(:group) { create(:group) } + let_it_be(:unarchived_project) { create(:project, :public, group: group, name: 'Test1') } + let_it_be(:archived_project) { create(:project, :archived, :public, group: group, name: 'Test2') } + + it_behaves_like 'search results filtered by archived' + end + end + describe 'user search' do subject(:objects) { results.objects('users') } diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index a38073e7c513e2c01e8f94553f8e152650f1937f..18b42da659b51c5e39551f55a73eea9b555e04a1 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::SearchResults do +RSpec.describe Gitlab::SearchResults, feature_category: :global_search do include ProjectForksHelper include SearchHelpers using RSpec::Parameterized::TableSyntax @@ -260,6 +260,19 @@ end end + describe '#projects' do + let(:scope) { 'projects' } + let(:query) { 'Test' } + + describe 'filtering' do + let_it_be(:group) { create(:group) } + let_it_be(:unarchived_project) { create(:project, :public, group: group, name: 'Test1') } + let_it_be(:archived_project) { create(:project, :archived, :public, group: group, name: 'Test2') } + + it_behaves_like 'search results filtered by archived' + end + end + describe '#users' do it 'does not call the UsersFinder when the current_user is not allowed to read users list' do allow(Ability).to receive(:allowed?).and_return(false) diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb index 6250d32574f8a092edbdef28e22039ad2c42914d..f77d81851e3cc866feff2a22eb569ab58c79361a 100644 --- a/spec/services/search/global_service_spec.rb +++ b/spec/services/search/global_service_spec.rb @@ -3,13 +3,14 @@ require 'spec_helper' RSpec.describe Search::GlobalService, feature_category: :global_search do - let(:user) { create(:user) } - let(:internal_user) { create(:user) } + let_it_be(:user) { create(:user) } + let_it_be(:internal_user) { create(:user) } - let!(:found_project) { create(:project, :private, name: 'searchable_project') } - let!(:unfound_project) { create(:project, :private, name: 'unfound_project') } - let!(:internal_project) { create(:project, :internal, name: 'searchable_internal_project') } - let!(:public_project) { create(:project, :public, name: 'searchable_public_project') } + let_it_be(:found_project) { create(:project, :private, name: 'searchable_project') } + let_it_be(:unfound_project) { create(:project, :private, name: 'unfound_project') } + let_it_be(:internal_project) { create(:project, :internal, name: 'searchable_internal_project') } + let_it_be(:public_project) { create(:project, :public, name: 'searchable_public_project') } + let_it_be(:archived_project) { create(:project, :public, archived: true, name: 'archived_project') } before do found_project.add_maintainer(user) @@ -44,12 +45,16 @@ end it 'does not return archived projects' do - archived_project = create(:project, :public, archived: true, name: 'archived_project') - results = described_class.new(user, search: "archived").execute expect(results.objects('projects')).not_to include(archived_project) end + + it 'returns archived projects if the include_archived option is passed' do + results = described_class.new(user, { include_archived: true, search: "archived" }).execute + + expect(results.objects('projects')).to include(archived_project) + end end end diff --git a/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..7bcefd07fc4a787c26787d7bc2773d1657010536 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'search results filtered by archived' do + context 'when filter not provided (all behavior)' do + let(:filters) { {} } + + it 'returns unarchived results only', :aggregate_failures do + expect(results.objects('projects')).to include unarchived_project + expect(results.objects('projects')).not_to include archived_project + end + end + + context 'when include_archived is true' do + let(:filters) { { include_archived: true } } + + it 'returns archived and unarchived results', :aggregate_failures do + expect(results.objects('projects')).to include unarchived_project + expect(results.objects('projects')).to include archived_project + end + end + + context 'when include_archived filter is false' do + let(:filters) { { include_archived: false } } + + it 'returns unarchived results only', :aggregate_failures do + expect(results.objects('projects')).to include unarchived_project + expect(results.objects('projects')).not_to include archived_project + end + end +end