diff options
-rw-r--r-- | app/assets/stylesheets/framework/snippets.scss | 1 | ||||
-rw-r--r-- | app/services/export_csv/base_service.rb | 10 | ||||
-rw-r--r-- | app/services/issues/export_csv_service.rb | 4 | ||||
-rw-r--r-- | app/views/projects/buttons/_clone.html.haml | 99 | ||||
-rw-r--r-- | config/feature_flags/development/export_csv_preload_in_batches.yml | 8 | ||||
-rw-r--r-- | db/post_migrate/20230130070623_add_index_on_packages_package_file_file_name.rb | 14 | ||||
-rw-r--r-- | db/schema_migrations/20230130070623 | 1 | ||||
-rw-r--r-- | doc/administration/geo/replication/troubleshooting.md | 20 | ||||
-rw-r--r-- | doc/administration/gitaly/configure_gitaly.md | 2 | ||||
-rw-r--r-- | doc/administration/gitaly/praefect.md | 51 | ||||
-rw-r--r-- | lib/csv_builder.rb | 12 | ||||
-rwxr-xr-x | scripts/review_apps/automated_cleanup.rb | 4 | ||||
-rw-r--r-- | spec/features/admin/admin_settings_spec.rb | 21 | ||||
-rw-r--r-- | spec/features/projects/show/clone_button_spec.rb | 43 | ||||
-rw-r--r-- | spec/services/issues/export_csv_service_spec.rb | 212 |
15 files changed, 341 insertions, 161 deletions
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index 14971e3b2ee..9f8d5d25cb8 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -17,7 +17,6 @@ border-radius: 3px; .file-content { - max-height: 500px; overflow-y: auto; } diff --git a/app/services/export_csv/base_service.rb b/app/services/export_csv/base_service.rb index 21b830d427a..98ab33d4c33 100644 --- a/app/services/export_csv/base_service.rb +++ b/app/services/export_csv/base_service.rb @@ -25,7 +25,11 @@ module ExportCsv # rubocop: disable CodeReuse/ActiveRecord def csv_builder @csv_builder ||= - CsvBuilder.new(objects.preload(associations_to_preload), header_to_value_hash) + if preload_associations_in_batches? + CsvBuilder.new(objects, header_to_value_hash, associations_to_preload) + else + CsvBuilder.new(objects.preload(associations_to_preload), header_to_value_hash, []) + end end # rubocop: enable CodeReuse/ActiveRecord @@ -36,5 +40,9 @@ module ExportCsv def header_to_value_hash raise NotImplementedError end + + def preload_associations_in_batches? + false + end end end diff --git a/app/services/issues/export_csv_service.rb b/app/services/issues/export_csv_service.rb index 9efded4aa43..d7c1ea276de 100644 --- a/app/services/issues/export_csv_service.rb +++ b/app/services/issues/export_csv_service.rb @@ -55,6 +55,10 @@ module Issues issue.timelogs.sum(&:time_spent) end # rubocop: enable CodeReuse/ActiveRecord + + def preload_associations_in_batches? + Feature.enabled?(:export_csv_preload_in_batches, resource_parent) + end end end diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml index a755cb9f5b0..a8a911adb7d 100644 --- a/app/views/projects/buttons/_clone.html.haml +++ b/app/views/projects/buttons/_clone.html.haml @@ -1,54 +1,55 @@ - project = project || @project - dropdown_class = local_assigns.fetch(:dropdown_class, '') -.git-clone-holder.js-git-clone-holder - %a#clone-dropdown.gl-button.btn.btn-confirm.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } } - %span.gl-mr-2.js-clone-dropdown-label - = _('Clone') - = sprite_icon("chevron-down", css_class: "icon") - %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown{ class: dropdown_class, data: { qa_selector: 'clone_dropdown_content' } } - - if ssh_enabled? - %li{ class: 'gl-px-4!' } - %label.label-bold - = _('Clone with SSH') - .input-group.btn-group - = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'ssh_clone_url_content' } - .input-group-append - = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default") - = render_if_exists 'projects/buttons/geo' - - if http_enabled? - %li.pt-2{ class: 'gl-px-4!' } - %label.label-bold - = _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase } - .input-group.btn-group - = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'http_clone_url_content' } - .input-group-append - = clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default") - = render_if_exists 'projects/buttons/geo' - = render_if_exists 'projects/buttons/kerberos_clone_field' - %li.divider.mt-2 - %li.pt-2.gl-dropdown-item - %label.label-bold{ class: 'gl-px-4!' } - = _('Open in your IDE') +- if can?(current_user, :download_code, @project) + .git-clone-holder.js-git-clone-holder + %a#clone-dropdown.gl-button.btn.btn-confirm.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } } + %span.gl-mr-2.js-clone-dropdown-label + = _('Clone') + = sprite_icon("chevron-down", css_class: "icon") + %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown{ class: dropdown_class, data: { qa_selector: 'clone_dropdown_content' } } - if ssh_enabled? - - escaped_ssh_url_to_repo = CGI.escape(project.ssh_url_to_repo) - %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_ssh_url_to_repo } - .gl-dropdown-item-text-wrapper - = _('Visual Studio Code (SSH)') + %li{ class: 'gl-px-4!' } + %label.label-bold + = _('Clone with SSH') + .input-group.btn-group + = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'ssh_clone_url_content' } + .input-group-append + = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default") + = render_if_exists 'projects/buttons/geo' - if http_enabled? - - escaped_http_url_to_repo = CGI.escape(project.http_url_to_repo) - %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_http_url_to_repo } - .gl-dropdown-item-text-wrapper - = _('Visual Studio Code (HTTPS)') - - if ssh_enabled? - %a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_ssh_url_to_repo } - .gl-dropdown-item-text-wrapper - = _('IntelliJ IDEA (SSH)') - - if http_enabled? - %a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_http_url_to_repo } - .gl-dropdown-item-text-wrapper - = _('IntelliJ IDEA (HTTPS)') - - if show_xcode_link?(@project) - %a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) } - .gl-dropdown-item-text-wrapper - = _("Xcode") + %li.pt-2{ class: 'gl-px-4!' } + %label.label-bold + = _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase } + .input-group.btn-group + = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'http_clone_url_content' } + .input-group-append + = clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default") + = render_if_exists 'projects/buttons/geo' + = render_if_exists 'projects/buttons/kerberos_clone_field' + %li.divider.mt-2 + %li.pt-2.gl-dropdown-item + %label.label-bold{ class: 'gl-px-4!' } + = _('Open in your IDE') + - if ssh_enabled? + - escaped_ssh_url_to_repo = CGI.escape(project.ssh_url_to_repo) + %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_ssh_url_to_repo } + .gl-dropdown-item-text-wrapper + = _('Visual Studio Code (SSH)') + - if http_enabled? + - escaped_http_url_to_repo = CGI.escape(project.http_url_to_repo) + %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_http_url_to_repo } + .gl-dropdown-item-text-wrapper + = _('Visual Studio Code (HTTPS)') + - if ssh_enabled? + %a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_ssh_url_to_repo } + .gl-dropdown-item-text-wrapper + = _('IntelliJ IDEA (SSH)') + - if http_enabled? + %a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_http_url_to_repo } + .gl-dropdown-item-text-wrapper + = _('IntelliJ IDEA (HTTPS)') + - if show_xcode_link?(@project) + %a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) } + .gl-dropdown-item-text-wrapper + = _("Xcode") diff --git a/config/feature_flags/development/export_csv_preload_in_batches.yml b/config/feature_flags/development/export_csv_preload_in_batches.yml new file mode 100644 index 00000000000..60c82dce4a0 --- /dev/null +++ b/config/feature_flags/development/export_csv_preload_in_batches.yml @@ -0,0 +1,8 @@ +--- +name: export_csv_preload_in_batches +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85989 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389847 +milestone: '15.9' +type: development +group: group::import +default_enabled: false diff --git a/db/post_migrate/20230130070623_add_index_on_packages_package_file_file_name.rb b/db/post_migrate/20230130070623_add_index_on_packages_package_file_file_name.rb new file mode 100644 index 00000000000..d7b495df272 --- /dev/null +++ b/db/post_migrate/20230130070623_add_index_on_packages_package_file_file_name.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AddIndexOnPackagesPackageFileFileName < Gitlab::Database::Migration[2.1] + INDEX_NAME = 'index_packages_package_files_on_file_name' + + def up + prepare_async_index :packages_package_files, :file_name, name: INDEX_NAME, using: :gin, + opclass: { description: :gin_trgm_ops } + end + + def down + unprepare_async_index :packages_package_files, :file_name, name: INDEX_NAME + end +end diff --git a/db/schema_migrations/20230130070623 b/db/schema_migrations/20230130070623 new file mode 100644 index 00000000000..136a4612f6a --- /dev/null +++ b/db/schema_migrations/20230130070623 @@ -0,0 +1 @@ +3d098df1006f9dba019a1637cd921ff9ffe087a967841fd2d27f7bc4db7e0e42
\ No newline at end of file diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md index 36b37418230..c80fc0d67d0 100644 --- a/doc/administration/geo/replication/troubleshooting.md +++ b/doc/administration/geo/replication/troubleshooting.md @@ -381,6 +381,26 @@ sudo gitlab-rake gitlab:geo:check When performing a PostgreSQL major version (9 > 10), update this is expected. Follow the [initiate-the-replication-process](../setup/database.md#step-3-initiate-the-replication-process). +- Rails does not appear to have the configuration necessary to connect to the Geo tracking database. + + ```plaintext + Checking Geo ... + + GitLab Geo is available ... yes + GitLab Geo is enabled ... yes + GitLab Geo tracking database is correctly configured ... no + Try fixing it: + Rails does not appear to have the configuration necessary to connect to the Geo tracking database. If the tracking database is running on a node other than this one, then you may need to add configuration. + ... + Checking Geo ... Finished + ``` + + - If you are running the secondary site on a single node for all services, then follow [Geo database replication - Configure the secondary server](../setup/database.md#step-2-configure-the-secondary-server). + - If you are running the secondary site's tracking database on its own node, then follow [Geo for multiple servers - Configure the Geo tracking database on the Geo secondary site](multiple_servers.md#step-3-configure-the-geo-tracking-database-on-the-geo-secondary-site) + - If you are running the secondary site's tracking database in a Patroni cluster, then follow [Geo database replication - Configure the tracking database on the secondary sites](../setup/database.md#step-3-configure-the-tracking-database-on-the-secondary-sites) + - If you are running the secondary site's tracking database in an external database, then follow [Geo with external PostgreSQL instances](../setup/external_database.md#configure-the-tracking-database) + - If the Geo check task was run on a node which is not running a service which runs the GitLab Rails app (Puma, Sidekiq, or Geo Log Cursor), then this error can be ignored. The node does not need Rails to be configured. + ### Message: Machine clock is synchronized ... Exception The Rake task attempts to verify that the server clock is synchronized with NTP. Synchronized clocks diff --git a/doc/administration/gitaly/configure_gitaly.md b/doc/administration/gitaly/configure_gitaly.md index 6f893a5a013..143f7dca7d3 100644 --- a/doc/administration/gitaly/configure_gitaly.md +++ b/doc/administration/gitaly/configure_gitaly.md @@ -778,7 +778,7 @@ example: gitaly['concurrency'] = [ { - 'rpc' => "/gitaly.SmartHTTPService/PostUploadPackWithSidechanel", + 'rpc' => "/gitaly.SmartHTTPService/PostUploadPackWithSidechannel", 'max_per_repo' => 20, 'max_queue_time' => "1s", 'max_queue_size' => 10 diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index abcd26cae1b..f5c15f92f9d 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -1201,6 +1201,57 @@ To get started quickly: Congratulations! You've configured an observable fault-tolerant Praefect cluster. +### Manage Gitaly nodes on a Gitaly Cluster + +You can add and replace Gitaly nodes on a Gitaly Cluster. + +#### Add new Gitaly nodes + +To add a new Gitaly node to a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor): + +- Set, set the [replication factor](praefect.md#configure-replication-factor) for each repository using `set-replication-factor` Praefect command. New repositories are + replicated based on [replication factor](praefect.md#configure-replication-factor). Praefect doesn't automatically replicate existing repositories to the new Gitaly node. +- Not set, add the new node in your [Praefect configuration](praefect.md#praefect) under `praefect['virtual_storages']`. Praefect automatically replicates all data to any + new Gitaly node added to the configuration. + +#### Replace an existing Gitaly node + +You can replace an existing Gitaly node with a new node with either the same name or a different name. + +##### With a node with the same name + +To use the same name for the replacement node, use [repository verifier](praefect.md#enable-deletions) to scan the storage and remove dangling metadata records. +[Manually prioritize verification](praefect.md#prioritize-verification-manually) of the replaced storage to speed up the process. + +##### With a node with a different name + +To use a different name for the replacement node for a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor): + +- Set, use [`praefect set-replication-factor`](praefect.md#configure-replication-factor) to set the replication factor per repository again to get new storage assigned. + For example: + + ```shell + $ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml set-replication-factor -virtual-storage default -repository @hashed/3f/db/3fdba35f04dc8c462986c992bcf875546257113072a909c162f7e470e581e278.git -replication-factor 2 + + current assignments: gitaly-1, gitaly-2 + ``` + + To reassign all repositories from the old storage to the new one, after configuring the new Gitaly node: + + 1. Connect to Praefect database: + + ```shell + /opt/gitlab/embedded/bin/psql -h <psql host> -U <user> -d <database name> + ``` + + 1. Update `repository_assignments` table to replace the old Gitaly node name (for example, `old-gitaly`) with the new Gitaly node name (for example, `new-gitaly`): + + ```sql + UPDATE repository_assignments SET storage='new-gitaly' WHERE storage='old-gitaly'; + ``` + +- Not set, replace the node in the configuration. The old node's state remains in the Praefect database but it is ignored. + ## Configure replication factor WARNING: diff --git a/lib/csv_builder.rb b/lib/csv_builder.rb index f270f7984da..a54c355396d 100644 --- a/lib/csv_builder.rb +++ b/lib/csv_builder.rb @@ -23,15 +23,17 @@ class CsvBuilder # # * +collection+ - The data collection to be used # * +header_to_hash_value+ - A hash of 'Column Heading' => 'value_method'. + # * +associations_to_preload+ - An array of records to preload with a batch of records. # # The value method will be called once for each object in the collection, to # determine the value for that row. It can either be the name of a method on # the object, or a lamda to call passing in the object. - def initialize(collection, header_to_value_hash) + def initialize(collection, header_to_value_hash, associations_to_preload = []) @header_to_value_hash = header_to_value_hash @collection = collection @truncated = false @rows_written = 0 + @associations_to_preload = associations_to_preload end # Renders the csv to a string @@ -75,7 +77,13 @@ class CsvBuilder protected def each(&block) - @collection.find_each(&block) # rubocop: disable CodeReuse/ActiveRecord + if @associations_to_preload.present? && @collection.respond_to?(:each_batch) + @collection.each_batch(order_hint: :created_at) do |relation| + relation.preload(@associations_to_preload).order(:id).each(&block) # rubocop:disable CodeReuse/ActiveRecord + end + else + @collection.find_each(&block) # rubocop: disable CodeReuse/ActiveRecord + end end private diff --git a/scripts/review_apps/automated_cleanup.rb b/scripts/review_apps/automated_cleanup.rb index 33c7c818456..a1810526550 100755 --- a/scripts/review_apps/automated_cleanup.rb +++ b/scripts/review_apps/automated_cleanup.rb @@ -91,7 +91,7 @@ module ReviewApps deleted_environment = delete_environment(environment, deployment) if deleted_environment - release = Tooling::Helm3Client::Release.new(environment.slug, 1, deployed_at.to_s, nil, nil, environment.slug) + release = Tooling::Helm3Client::Release.new(name: environment.slug, namespace: environment.slug, revision: 1) releases_to_delete << release end end @@ -100,7 +100,7 @@ module ReviewApps end delete_stopped_environments(environment_type: :review_app, checked_environments: checked_environments, last_updated_threshold: delete_threshold) do |environment| - releases_to_delete << Tooling::Helm3Client::Release.new(environment.slug, 1, environment.updated_at, nil, nil, environment.slug) + releases_to_delete << Tooling::Helm3Client::Release.new(name: environment.slug, namespace: environment.slug, revision: 1, updated: environment.updated_at) end delete_helm_releases(releases_to_delete) diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index d8929e1edfb..6642bd7ac61 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -155,18 +155,27 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do context 'when Gitlab.com' do let(:dot_com?) { true } - it 'does not expose the setting' do - expect(page).to have_no_selector('#application_setting_deactivate_dormant_users') - end - - it 'does not expose the setting' do - expect(page).to have_no_selector('#application_setting_deactivate_dormant_users_period') + it 'does not expose the setting section' do + # NOTE: not_to have_content may have false positives for content + # that might not load instantly, so before checking that + # `Dormant users` subsection has _not_ loaded, we check that the + # `Account and limit` section _was_ loaded + expect(page).to have_content('Account and limit') + expect(page).not_to have_content('Dormant users') + expect(page).not_to have_field('Deactivate dormant users after a period of inactivity') + expect(page).not_to have_field('Days of inactivity before deactivation') end end context 'when not Gitlab.com' do let(:dot_com?) { false } + it 'exposes the setting section' do + expect(page).to have_content('Dormant users') + expect(page).to have_field('Deactivate dormant users after a period of inactivity') + expect(page).to have_field('Days of inactivity before deactivation') + end + it 'changes dormant users' do expect(page).to have_unchecked_field('Deactivate dormant users after a period of inactivity') expect(current_settings.deactivate_dormant_users).to be_falsey diff --git a/spec/features/projects/show/clone_button_spec.rb b/spec/features/projects/show/clone_button_spec.rb new file mode 100644 index 00000000000..48af4bf8277 --- /dev/null +++ b/spec/features/projects/show/clone_button_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects > Show > Clone button', feature_category: :projects do + let_it_be(:admin) { create(:admin) } + let_it_be(:guest) { create(:user) } + let_it_be(:project) { create(:project, :private, :in_group, :repository) } + + describe 'when checking project main page user' do + context 'with an admin role' do + before do + project.add_owner(admin) + sign_in(admin) + visit project_path(project) + end + + it 'is able to access project page' do + expect(page).to have_content project.name + end + + it 'sees clone button' do + expect(page).to have_content _('Clone') + end + end + + context 'with a guest role and no download_code access' do + before do + project.add_guest(guest) + sign_in(guest) + visit project_path(project) + end + + it 'is able to access project page' do + expect(page).to have_content project.name + end + + it 'does not see clone button' do + expect(page).not_to have_content _('Clone') + end + end + end +end diff --git a/spec/services/issues/export_csv_service_spec.rb b/spec/services/issues/export_csv_service_spec.rb index f5ff4ed3f9e..3beb28c6009 100644 --- a/spec/services/issues/export_csv_service_spec.rb +++ b/spec/services/issues/export_csv_service_spec.rb @@ -57,137 +57,151 @@ RSpec.describe Issues::ExportCsvService, :with_license, feature_category: :impor time_estimate: 72000) end - it 'includes the columns required for import' do - expect(csv.headers).to include('Title', 'Description') - end - - it 'returns two issues' do - expect(csv.count).to eq(2) - end + shared_examples 'exports CSVs for issues' do + it 'includes the columns required for import' do + expect(csv.headers).to include('Title', 'Description') + end - specify 'iid' do - expect(csv[0]['Issue ID']).to eq issue.iid.to_s - end + it 'returns two issues' do + expect(csv.count).to eq(2) + end - specify 'url' do - expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/) - end + specify 'iid' do + expect(csv[0]['Issue ID']).to eq issue.iid.to_s + end - specify 'title' do - expect(csv[0]['Title']).to eq issue.title - end + specify 'url' do + expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/) + end - specify 'state' do - expect(csv[0]['State']).to eq 'Open' - end + specify 'title' do + expect(csv[0]['Title']).to eq issue.title + end - specify 'description' do - expect(csv[0]['Description']).to eq issue.description - expect(csv[1]['Description']).to eq nil - end + specify 'state' do + expect(csv[0]['State']).to eq 'Open' + end - specify 'author name' do - expect(csv[0]['Author']).to eq issue.author_name - end + specify 'description' do + expect(csv[0]['Description']).to eq issue.description + expect(csv[1]['Description']).to eq nil + end - specify 'author username' do - expect(csv[0]['Author Username']).to eq issue.author.username - end + specify 'author name' do + expect(csv[0]['Author']).to eq issue.author_name + end - specify 'assignee name' do - expect(csv[0]['Assignee']).to eq user.name - expect(csv[1]['Assignee']).to eq '' - end + specify 'author username' do + expect(csv[0]['Author Username']).to eq issue.author.username + end - specify 'assignee username' do - expect(csv[0]['Assignee Username']).to eq user.username - expect(csv[1]['Assignee Username']).to eq '' - end + specify 'assignee name' do + expect(csv[0]['Assignee']).to eq user.name + expect(csv[1]['Assignee']).to eq '' + end - specify 'confidential' do - expect(csv[0]['Confidential']).to eq 'No' - end + specify 'assignee username' do + expect(csv[0]['Assignee Username']).to eq user.username + expect(csv[1]['Assignee Username']).to eq '' + end - specify 'milestone' do - expect(csv[0]['Milestone']).to eq issue.milestone.title - expect(csv[1]['Milestone']).to eq nil - end + specify 'confidential' do + expect(csv[0]['Confidential']).to eq 'No' + end - specify 'labels' do - expect(csv[0]['Labels']).to eq 'Feature,Idea' - expect(csv[1]['Labels']).to eq nil - end + specify 'milestone' do + expect(csv[0]['Milestone']).to eq issue.milestone.title + expect(csv[1]['Milestone']).to eq nil + end - specify 'due_date' do - expect(csv[0]['Due Date']).to eq '2014-03-02' - expect(csv[1]['Due Date']).to eq nil - end + specify 'labels' do + expect(csv[0]['Labels']).to eq 'Feature,Idea' + expect(csv[1]['Labels']).to eq nil + end - specify 'created_at' do - expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00' - end + specify 'due_date' do + expect(csv[0]['Due Date']).to eq '2014-03-02' + expect(csv[1]['Due Date']).to eq nil + end - specify 'updated_at' do - expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01' - end + specify 'created_at' do + expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00' + end - specify 'closed_at' do - expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02' - expect(csv[1]['Closed At (UTC)']).to eq nil - end + specify 'updated_at' do + expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01' + end - specify 'discussion_locked' do - expect(csv[0]['Locked']).to eq 'Yes' - end + specify 'closed_at' do + expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02' + expect(csv[1]['Closed At (UTC)']).to eq nil + end - specify 'weight' do - expect(csv[0]['Weight']).to eq '4' - end + specify 'discussion_locked' do + expect(csv[0]['Locked']).to eq 'Yes' + end - specify 'time estimate' do - expect(csv[0]['Time Estimate']).to eq '72000' - expect(csv[1]['Time Estimate']).to eq '0' - end + specify 'weight' do + expect(csv[0]['Weight']).to eq '4' + end - specify 'time spent' do - expect(csv[0]['Time Spent']).to eq '560' - expect(csv[1]['Time Spent']).to eq '0' - end + specify 'time estimate' do + expect(csv[0]['Time Estimate']).to eq '72000' + expect(csv[1]['Time Estimate']).to eq '0' + end - context 'with issues filtered by labels and project' do - subject do - described_class.new( - IssuesFinder.new(user, - project_id: project.id, - label_name: %w(Idea Feature)).execute, project) + specify 'time spent' do + expect(csv[0]['Time Spent']).to eq '560' + expect(csv[1]['Time Spent']).to eq '0' end - it 'returns only filtered objects' do - expect(csv.count).to eq(1) - expect(csv[0]['Issue ID']).to eq issue.iid.to_s + context 'with issues filtered by labels and project' do + subject do + described_class.new( + IssuesFinder.new(user, + project_id: project.id, + label_name: %w(Idea Feature)).execute, project) + end + + it 'returns only filtered objects' do + expect(csv.count).to eq(1) + expect(csv[0]['Issue ID']).to eq issue.iid.to_s + end end - end - context 'with label links' do - let(:labeled_issues) { create_list(:labeled_issue, 2, project: project, author: user, labels: [feature_label, idea_label]) } + context 'with label links' do + let(:labeled_issues) { create_list(:labeled_issue, 2, project: project, author: user, labels: [feature_label, idea_label]) } - it 'does not run a query for each label link' do - control_count = ActiveRecord::QueryRecorder.new { csv }.count + it 'does not run a query for each label link' do + control_count = ActiveRecord::QueryRecorder.new { csv }.count - labeled_issues + labeled_issues - expect { csv }.not_to exceed_query_limit(control_count) - expect(csv.count).to eq(4) - end + expect { csv }.not_to exceed_query_limit(control_count) + expect(csv.count).to eq(4) + end - it 'returns the labels in sorted order' do - labeled_issues + it 'returns the labels in sorted order' do + labeled_issues - labeled_rows = csv.select { |entry| labeled_issues.map(&:iid).include?(entry['Issue ID'].to_i) } - expect(labeled_rows.count).to eq(2) - expect(labeled_rows.map { |entry| entry['Labels'] }).to all(eq("Feature,Idea")) + labeled_rows = csv.select { |entry| labeled_issues.map(&:iid).include?(entry['Issue ID'].to_i) } + expect(labeled_rows.count).to eq(2) + expect(labeled_rows.map { |entry| entry['Labels'] }).to all(eq("Feature,Idea")) + end end end + + context 'with export_csv_preload_in_batches feature flag disabled' do + before do + stub_feature_flags(export_csv_preload_in_batches: false) + end + + it_behaves_like 'exports CSVs for issues' + end + + context 'with export_csv_preload_in_batches feature flag enabled' do + it_behaves_like 'exports CSVs for issues' + end end context 'with minimal details' do |