Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/stylesheets/framework/snippets.scss1
-rw-r--r--app/services/export_csv/base_service.rb10
-rw-r--r--app/services/issues/export_csv_service.rb4
-rw-r--r--app/views/projects/buttons/_clone.html.haml99
-rw-r--r--config/feature_flags/development/export_csv_preload_in_batches.yml8
-rw-r--r--db/post_migrate/20230130070623_add_index_on_packages_package_file_file_name.rb14
-rw-r--r--db/schema_migrations/202301300706231
-rw-r--r--doc/administration/geo/replication/troubleshooting.md20
-rw-r--r--doc/administration/gitaly/configure_gitaly.md2
-rw-r--r--doc/administration/gitaly/praefect.md51
-rw-r--r--lib/csv_builder.rb12
-rwxr-xr-xscripts/review_apps/automated_cleanup.rb4
-rw-r--r--spec/features/admin/admin_settings_spec.rb21
-rw-r--r--spec/features/projects/show/clone_button_spec.rb43
-rw-r--r--spec/services/issues/export_csv_service_spec.rb212
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