summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/review-apps/qa.gitlab-ci.yml2
-rw-r--r--.haml-lint.yml1
-rw-r--r--.rubocop_todo/gitlab/json.yml494
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/groups_projects/components/transfer_locations.vue233
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql5
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue171
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/index.js31
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js7
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue1
-rw-r--r--app/assets/javascripts/issues/list/graphql.js25
-rw-r--r--app/assets/javascripts/issues/list/index.js25
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue1
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue13
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue4
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql17
-rw-r--r--app/assets/javascripts/projects/settings/components/transfer_project_form.vue160
-rw-r--r--app/assets/javascripts/projects/settings/init_transfer_project_form.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue9
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue5
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue2
-rw-r--r--app/controllers/projects/alerting/notifications_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb14
-rw-r--r--app/controllers/projects/prometheus/alerts_controller.rb19
-rw-r--r--app/finders/license_template_finder.rb6
-rw-r--r--app/graphql/types/merge_request_type.rb3
-rw-r--r--app/models/application_setting.rb4
-rw-r--r--app/models/diff_viewer/server_side.rb8
-rw-r--r--app/models/experiment.rb32
-rw-r--r--app/models/experiment_user.rb14
-rw-r--r--app/models/integrations/datadog.rb2
-rw-r--r--app/models/ml/candidate.rb4
-rw-r--r--app/services/bulk_imports/create_pipeline_trackers_service.rb2
-rw-r--r--app/services/concerns/alert_management/responses.rb4
-rw-r--r--app/services/projects/prometheus/alerts/notify_service.rb10
-rw-r--r--app/views/projects/_transfer.html.haml1
-rw-r--r--app/views/projects/protected_tags/shared/_dropdown.html.haml8
-rw-r--r--app/views/projects/protected_tags/shared/_tags_list.html.haml13
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml4
-rw-r--r--app/workers/bulk_imports/entity_worker.rb45
-rw-r--r--app/workers/bulk_imports/export_request_worker.rb79
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb79
-rw-r--r--config/feature_flags/development/check_etags_diffs_batch_before_write_cache.yml (renamed from config/feature_flags/development/disable_load_entire_blob_for_diff_viewer.yml)10
-rw-r--r--db/migrate/20221010103207_add_product_analytics_enabled_to_application_settings.rb7
-rw-r--r--db/schema_migrations/202210101032071
-rw-r--r--db/structure.sql1
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/development/pipelines.md10
-rw-r--r--doc/user/project/issues/managing_issues.md23
-rw-r--r--lib/api/entities/ml/mlflow/run.rb2
-rw-r--r--lib/api/entities/ml/mlflow/run_info.rb2
-rw-r--r--lib/api/entities/ml/mlflow/update_run.rb8
-rw-r--r--lib/api/ml/mlflow.rb16
-rw-r--r--lib/bulk_imports/common/pipelines/entity_finisher.rb8
-rw-r--r--lib/bulk_imports/pipeline/runner.rb39
-rw-r--r--lib/gitlab/diff/file_collection/base.rb4
-rw-r--r--lib/gitlab/kas/client.rb2
-rw-r--r--locale/gitlab.pot39
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb15
-rw-r--r--rubocop/cop/gitlab/json.rb32
-rwxr-xr-xscripts/lint_templates_bash.rb6
-rw-r--r--scripts/review_apps/base-config.yaml8
-rw-r--r--spec/controllers/projects/alerting/notifications_controller_spec.rb20
-rw-r--r--spec/controllers/projects/prometheus/alerts_controller_spec.rb11
-rw-r--r--spec/factories/experiment_users.rb10
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb7
-rw-r--r--spec/fixtures/api/schemas/ml/run.json46
-rw-r--r--spec/frontend/groups_projects/components/transfer_locations_spec.js283
-rw-r--r--spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js554
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js2
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js5
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/mock_data.js32
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js268
-rw-r--r--spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js24
-rw-r--r--spec/lib/api/entities/ml/mlflow/run_info_spec.rb4
-rw-r--r--spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb2
-rw-r--r--spec/lib/bulk_imports/pipeline/runner_spec.rb25
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb1
-rw-r--r--spec/lib/gitlab/kas/client_spec.rb9
-rw-r--r--spec/models/diff_viewer/server_side_spec.rb28
-rw-r--r--spec/models/experiment_spec.rb278
-rw-r--r--spec/models/experiment_user_spec.rb14
-rw-r--r--spec/models/integrations/datadog_spec.rb8
-rw-r--r--spec/models/merge_request_spec.rb4
-rw-r--r--spec/models/ml/candidate_spec.rb8
-rw-r--r--spec/requests/api/ml/mlflow_spec.rb5
-rw-r--r--spec/requests/projects/merge_requests/diffs_spec.rb14
-rw-r--r--spec/rubocop/cop/gitlab/json_spec.rb45
-rw-r--r--spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb4
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb10
-rw-r--r--spec/support/rspec_order_todo.yml1
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb4
-rw-r--r--spec/workers/bulk_imports/entity_worker_spec.rb25
-rw-r--r--spec/workers/bulk_imports/export_request_worker_spec.rb95
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb54
98 files changed, 2589 insertions, 1122 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index 1a1c67bf572..9d306eed77c 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -7,7 +7,7 @@ include:
- local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml
- local: .gitlab/ci/package-and-test/variables.gitlab-ci.yml
- project: gitlab-org/quality/pipeline-common
- ref: 1.3.0
+ ref: 1.4.0
file:
- /ci/base.gitlab-ci.yml
- /ci/allure-report.yml
diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml
index 0214f5ef3f2..af7c0231a74 100644
--- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml
@@ -1,6 +1,6 @@
include:
- project: gitlab-org/quality/pipeline-common
- ref: 1.3.0
+ ref: 1.4.0
file:
- /ci/base.gitlab-ci.yml
- /ci/allure-report.yml
diff --git a/.haml-lint.yml b/.haml-lint.yml
index 60f86eb4158..3655ca44172 100644
--- a/.haml-lint.yml
+++ b/.haml-lint.yml
@@ -110,6 +110,7 @@ linters:
- Cop/LineBreakAfterGuardClauses
- Cop/ProjectPathHelper
- Gitlab/FeatureAvailableUsage
+ - Gitlab/Json
- GitlabSecurity/PublicSend
- Layout/EmptyLineAfterGuardClause
- Layout/LeadingCommentSpace
diff --git a/.rubocop_todo/gitlab/json.yml b/.rubocop_todo/gitlab/json.yml
new file mode 100644
index 00000000000..9a946f11f84
--- /dev/null
+++ b/.rubocop_todo/gitlab/json.yml
@@ -0,0 +1,494 @@
+---
+# Cop supports --auto-correct.
+Gitlab/Json:
+ Details: grace period
+ Exclude:
+ - 'app/controllers/admin/application_settings_controller.rb'
+ - 'app/controllers/concerns/authenticates_with_two_factor.rb'
+ - 'app/controllers/projects/commit_controller.rb'
+ - 'app/controllers/projects/google_cloud/configuration_controller.rb'
+ - 'app/controllers/projects/google_cloud/databases_controller.rb'
+ - 'app/controllers/projects/google_cloud/deployments_controller.rb'
+ - 'app/controllers/projects/google_cloud/gcp_regions_controller.rb'
+ - 'app/controllers/projects/google_cloud/service_accounts_controller.rb'
+ - 'app/controllers/projects/graphs_controller.rb'
+ - 'app/controllers/projects/merge_requests_controller.rb'
+ - 'app/controllers/projects/notes_controller.rb'
+ - 'app/controllers/projects/settings/ci_cd_controller.rb'
+ - 'app/controllers/projects/templates_controller.rb'
+ - 'app/controllers/projects_controller.rb'
+ - 'app/controllers/search_controller.rb'
+ - 'app/helpers/access_tokens_helper.rb'
+ - 'app/helpers/application_settings_helper.rb'
+ - 'app/helpers/breadcrumbs_helper.rb'
+ - 'app/helpers/ci/builds_helper.rb'
+ - 'app/helpers/ci/pipelines_helper.rb'
+ - 'app/helpers/compare_helper.rb'
+ - 'app/helpers/emails_helper.rb'
+ - 'app/helpers/environment_helper.rb'
+ - 'app/helpers/groups_helper.rb'
+ - 'app/helpers/ide_helper.rb'
+ - 'app/helpers/integrations_helper.rb'
+ - 'app/helpers/invite_members_helper.rb'
+ - 'app/helpers/issuables_description_templates_helper.rb'
+ - 'app/helpers/issuables_helper.rb'
+ - 'app/helpers/jira_connect_helper.rb'
+ - 'app/helpers/learn_gitlab_helper.rb'
+ - 'app/helpers/namespaces_helper.rb'
+ - 'app/helpers/notes_helper.rb'
+ - 'app/helpers/operations_helper.rb'
+ - 'app/helpers/packages_helper.rb'
+ - 'app/helpers/projects/project_members_helper.rb'
+ - 'app/helpers/projects_helper.rb'
+ - 'app/helpers/search_helper.rb'
+ - 'app/helpers/terms_helper.rb'
+ - 'app/helpers/users_helper.rb'
+ - 'app/mailers/emails/members.rb'
+ - 'app/models/concerns/redis_cacheable.rb'
+ - 'app/models/diff_discussion.rb'
+ - 'app/models/integrations/assembla.rb'
+ - 'app/models/integrations/pivotaltracker.rb'
+ - 'app/models/integrations/pumble.rb'
+ - 'app/models/integrations/unify_circuit.rb'
+ - 'app/models/integrations/webex_teams.rb'
+ - 'app/models/merge_request_diff_commit.rb'
+ - 'app/presenters/packages/composer/packages_presenter.rb'
+ - 'app/presenters/projects/security/configuration_presenter.rb'
+ - 'app/services/bulk_imports/lfs_objects_export_service.rb'
+ - 'app/services/ci/pipeline_artifacts/coverage_report_service.rb'
+ - 'app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb'
+ - 'app/services/ci/pipeline_trigger_service.rb'
+ - 'app/services/ci/register_job_service.rb'
+ - 'app/services/google_cloud/create_service_accounts_service.rb'
+ - 'app/services/google_cloud/setup_cloudsql_instance_service.rb'
+ - 'app/services/merge_requests/add_context_service.rb'
+ - 'app/services/projects/lfs_pointers/lfs_download_link_list_service.rb'
+ - 'app/services/service_ping/submit_service.rb'
+ - 'app/workers/google_cloud/create_cloudsql_instance_worker.rb'
+ - 'config/initializers/rack_multipart_patch.rb'
+ - 'db/migrate/20210305031822_create_dast_site_profile_variables.rb'
+ - 'db/migrate/20210317035357_create_dast_profiles_pipelines.rb'
+ - 'db/migrate/20210412111213_create_security_orchestration_policy_rule_schedule.rb'
+ - 'db/migrate/20210423054022_create_dast_site_profiles_pipelines.rb'
+ - 'db/migrate/20210604032738_create_dast_site_profiles_builds.rb'
+ - 'db/migrate/20210604051330_create_dast_scanner_profiles_builds.rb'
+ - 'db/migrate/20210713123345_create_dast_profile_schedule.rb'
+ - 'db/post_migrate/20210311120155_backfill_events_id_for_bigint_conversion.rb'
+ - 'db/post_migrate/20210311120156_backfill_push_event_payload_event_id_for_bigint_conversion.rb'
+ - 'db/post_migrate/20210415101228_backfill_ci_build_needs_for_bigint_conversion.rb'
+ - 'db/post_migrate/20210420121149_backfill_conversion_of_ci_job_artifacts.rb'
+ - 'db/post_migrate/20210422023046_backfill_ci_sources_pipelines_source_job_id_for_bigint_conversion.rb'
+ - 'db/post_migrate/20210615234935_fix_batched_migrations_old_format_job_arguments.rb'
+ - 'db/post_migrate/20221006172302_adjust_task_note_rename_background_migration_values.rb'
+ - 'ee/app/controllers/admin/geo/nodes_controller.rb'
+ - 'ee/app/controllers/ee/admin/application_settings_controller.rb'
+ - 'ee/app/controllers/ee/search_controller.rb'
+ - 'ee/app/controllers/subscriptions_controller.rb'
+ - 'ee/app/graphql/types/json_string_type.rb'
+ - 'ee/app/helpers/billing_plans_helper.rb'
+ - 'ee/app/helpers/ee/environments_helper.rb'
+ - 'ee/app/helpers/ee/geo_helper.rb'
+ - 'ee/app/helpers/ee/groups/analytics/cycle_analytics_helper.rb'
+ - 'ee/app/helpers/ee/invite_members_helper.rb'
+ - 'ee/app/helpers/ee/operations_helper.rb'
+ - 'ee/app/helpers/ee/projects/pipeline_helper.rb'
+ - 'ee/app/helpers/ee/projects_helper.rb'
+ - 'ee/app/helpers/ee/security_orchestration_helper.rb'
+ - 'ee/app/helpers/groups/ldap_sync_helper.rb'
+ - 'ee/app/helpers/groups/security_features_helper.rb'
+ - 'ee/app/helpers/incident_management/oncall_schedule_helper.rb'
+ - 'ee/app/helpers/projects/on_demand_scans_helper.rb'
+ - 'ee/app/helpers/projects/security/dast_profiles_helper.rb'
+ - 'ee/app/helpers/security_helper.rb'
+ - 'ee/app/helpers/subscriptions_helper.rb'
+ - 'ee/app/helpers/users/identity_verification_helper.rb'
+ - 'ee/app/helpers/vulnerabilities_helper.rb'
+ - 'ee/app/models/product_analytics/jitsu_authentication.rb'
+ - 'ee/app/presenters/epic_presenter.rb'
+ - 'ee/app/services/arkose/blocked_users_report_service.rb'
+ - 'ee/app/services/elastic/indexing_control_service.rb'
+ - 'ee/app/services/elastic/process_bookkeeping_service.rb'
+ - 'ee/app/services/security/token_revocation_service.rb'
+ - 'ee/app/services/status_page/publish_base_service.rb'
+ - 'ee/app/services/upcoming_reconciliations/update_service.rb'
+ - 'ee/app/services/vulnerabilities/create_service_base.rb'
+ - 'ee/app/workers/concerns/elastic/migration_state.rb'
+ - 'ee/app/workers/sync_seat_link_request_worker.rb'
+ - 'ee/db/fixtures/development/20_vulnerabilities.rb'
+ - 'ee/lib/api/analytics/product_analytics.rb'
+ - 'ee/lib/ee/gitlab/background_migration/update_vulnerability_occurrences_location.rb'
+ - 'ee/lib/gitlab/elastic/indexer.rb'
+ - 'ee/lib/gitlab/geo/signed_data.rb'
+ - 'ee/lib/gitlab/subscription_portal/clients/graphql.rb'
+ - 'ee/lib/gitlab/subscription_portal/clients/rest.rb'
+ - 'ee/lib/slack/api.rb'
+ - 'ee/lib/tasks/gitlab/elastic.rake'
+ - 'ee/lib/tasks/gitlab/spdx.rake'
+ - 'ee/spec/controllers/admin/application_settings_controller_spec.rb'
+ - 'ee/spec/controllers/countries_controller_spec.rb'
+ - 'ee/spec/controllers/country_states_controller_spec.rb'
+ - 'ee/spec/controllers/ee/search_controller_spec.rb'
+ - 'ee/spec/controllers/groups/analytics/cycle_analytics_controller_spec.rb'
+ - 'ee/spec/controllers/groups/security/policies_controller_spec.rb'
+ - 'ee/spec/controllers/projects/integrations/jira/issues_controller_spec.rb'
+ - 'ee/spec/controllers/subscriptions_controller_spec.rb'
+ - 'ee/spec/factories/vulnerabilities/findings.rb'
+ - 'ee/spec/features/admin/subscriptions/admin_views_subscription_spec.rb'
+ - 'ee/spec/features/billings/billing_plans_spec.rb'
+ - 'ee/spec/features/billings/extend_reactivate_trial_spec.rb'
+ - 'ee/spec/features/billings/qrtly_reconciliation_alert_spec.rb'
+ - 'ee/spec/features/projects/integrations/jira_issues_list_spec.rb'
+ - 'ee/spec/features/projects/integrations/user_activates_github_spec.rb'
+ - 'ee/spec/features/projects/integrations/user_activates_jira_spec.rb'
+ - 'ee/spec/frontend/fixtures/dast_profiles.rb'
+ - 'ee/spec/frontend/fixtures/epic.rb'
+ - 'ee/spec/graphql/api/vulnerabilities_spec.rb'
+ - 'ee/spec/graphql/types/json_string_type_spec.rb'
+ - 'ee/spec/helpers/ee/groups/group_members_helper_spec.rb'
+ - 'ee/spec/helpers/ee/projects/pipeline_helper_spec.rb'
+ - 'ee/spec/helpers/ee/security_orchestration_helper_spec.rb'
+ - 'ee/spec/helpers/incident_management/oncall_schedule_helper_spec.rb'
+ - 'ee/spec/helpers/projects/on_demand_scans_helper_spec.rb'
+ - 'ee/spec/helpers/projects/security/dast_profiles_helper_spec.rb'
+ - 'ee/spec/helpers/users/identity_verification_helper_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/drop_invalid_remediations_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/background_migration/update_vulnerability_occurrences_location_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/cycle_analytics/request_params_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/parsers/license_compliance/license_scanning_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/parsers/security/dast_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/parsers/security/dependency_scanning_spec.rb'
+ - 'ee/spec/lib/gitlab/elastic/bulk_indexer_spec.rb'
+ - 'ee/spec/lib/gitlab/elastic/indexer_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/replication/blob_downloader_spec.rb'
+ - 'ee/spec/lib/gitlab/tracking/standard_context_spec.rb'
+ - 'ee/spec/lib/slack/api_spec.rb'
+ - 'ee/spec/migrations/update_vulnerability_occurrences_location_spec.rb'
+ - 'ee/spec/models/ee/integrations/jira_spec.rb'
+ - 'ee/spec/models/gitlab/seat_link_data_spec.rb'
+ - 'ee/spec/models/group_member_spec.rb'
+ - 'ee/spec/models/integrations/github/status_notifier_spec.rb'
+ - 'ee/spec/models/integrations/github_spec.rb'
+ - 'ee/spec/models/license_spec.rb'
+ - 'ee/spec/models/product_analytics/jitsu_authentication_spec.rb'
+ - 'ee/spec/models/vulnerabilities/finding_spec.rb'
+ - 'ee/spec/presenters/audit_event_presenter_spec.rb'
+ - 'ee/spec/requests/api/analytics/product_analytics_spec.rb'
+ - 'ee/spec/requests/api/experiments_spec.rb'
+ - 'ee/spec/requests/api/geo_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/alert_management/http_integrations_spec.rb'
+ - 'ee/spec/requests/api/graphql/vulnerabilities/external_issue_links_spec.rb'
+ - 'ee/spec/requests/api/graphql/vulnerabilities/location_spec.rb'
+ - 'ee/spec/requests/api/integrations/slack/events_spec.rb'
+ - 'ee/spec/requests/api/releases_spec.rb'
+ - 'ee/spec/requests/api/settings_spec.rb'
+ - 'ee/spec/requests/git_http_geo_spec.rb'
+ - 'ee/spec/requests/projects/on_demand_scans_controller_spec.rb'
+ - 'ee/spec/requests/projects/security/policies_controller_spec.rb'
+ - 'ee/spec/requests/users/identity_verification_controller_spec.rb'
+ - 'ee/spec/serializers/clusters/environment_entity_spec.rb'
+ - 'ee/spec/serializers/clusters/environment_serializer_spec.rb'
+ - 'ee/spec/serializers/dependency_list_serializer_spec.rb'
+ - 'ee/spec/serializers/epics/related_epic_entity_spec.rb'
+ - 'ee/spec/serializers/evidences/evidence_entity_spec.rb'
+ - 'ee/spec/serializers/issue_serializer_spec.rb'
+ - 'ee/spec/serializers/licenses_list_serializer_spec.rb'
+ - 'ee/spec/serializers/member_entity_spec.rb'
+ - 'ee/spec/serializers/member_user_entity_spec.rb'
+ - 'ee/spec/serializers/status_page/incident_entity_spec.rb'
+ - 'ee/spec/serializers/status_page/incident_serializer_spec.rb'
+ - 'ee/spec/serializers/test_reports_comparer_serializer_spec.rb'
+ - 'ee/spec/services/arkose/blocked_users_report_service_spec.rb'
+ - 'ee/spec/services/arkose/token_verification_service_spec.rb'
+ - 'ee/spec/services/gitlab_subscriptions/fetch_subscription_plans_service_spec.rb'
+ - 'ee/spec/services/integrations/slack_events/app_home_opened_service_spec.rb'
+ - 'ee/spec/services/jira/requests/issues/list_service_spec.rb'
+ - 'ee/spec/services/projects/slack_application_install_service_spec.rb'
+ - 'ee/spec/services/security/token_revocation_service_spec.rb'
+ - 'ee/spec/support/helpers/subscription_portal_helpers.rb'
+ - 'ee/spec/support/shared_examples/controllers/cluster_metrics_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/requests/api/project_approval_rules_api_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/status_page/publish_shared_examples.rb'
+ - 'ee/spec/tasks/gitlab/spdx_rake_spec.rb'
+ - 'ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb'
+ - 'ee/spec/workers/scan_security_report_secrets_worker_spec.rb'
+ - 'ee/spec/workers/sync_seat_link_request_worker_spec.rb'
+ - 'ee/spec/workers/vulnerability_exports/export_worker_spec.rb'
+ - 'lib/api/api.rb'
+ - 'lib/api/feature_flags_user_lists.rb'
+ - 'lib/api/helpers.rb'
+ - 'lib/api/terraform/state.rb'
+ - 'lib/atlassian/jira_connect/client.rb'
+ - 'lib/atlassian/jira_connect/serializers/base_entity.rb'
+ - 'lib/backup/gitaly_backup.rb'
+ - 'lib/bitbucket_server/client.rb'
+ - 'lib/bulk_imports/clients/graphql.rb'
+ - 'lib/error_tracking/sentry_client.rb'
+ - 'lib/gitlab/alert_management/payload/prometheus.rb'
+ - 'lib/gitlab/analytics/cycle_analytics/request_params.rb'
+ - 'lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp.rb'
+ - 'lib/gitlab/auth/otp/strategies/forti_authenticator/push_otp.rb'
+ - 'lib/gitlab/auth/otp/strategies/forti_token_cloud.rb'
+ - 'lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb'
+ - 'lib/gitlab/bitbucket_import/importer.rb'
+ - 'lib/gitlab/bitbucket_server_import/importer.rb'
+ - 'lib/gitlab/chat/responder/mattermost.rb'
+ - 'lib/gitlab/chat/responder/slack.rb'
+ - 'lib/gitlab/chat_name_token.rb'
+ - 'lib/gitlab/ci/ansi2html.rb'
+ - 'lib/gitlab/ci/ansi2json/state.rb'
+ - 'lib/gitlab/ci/build/releaser.rb'
+ - 'lib/gitlab/ci/config/external/mapper.rb'
+ - 'lib/gitlab/ci/pipeline/chain/validate/external.rb'
+ - 'lib/gitlab/ci/reports/security/finding.rb'
+ - 'lib/gitlab/composer/cache.rb'
+ - 'lib/gitlab/database/background_migration/batched_migration.rb'
+ - 'lib/gitlab/database/background_migration_job.rb'
+ - 'lib/gitlab/database/migration_helpers.rb'
+ - 'lib/gitlab/database/migrations/instrumentation.rb'
+ - 'lib/gitlab/database/migrations/runner.rb'
+ - 'lib/gitlab/database/postgres_hll/buckets.rb'
+ - 'lib/gitlab/database/reindexing/grafana_notifier.rb'
+ - 'lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb'
+ - 'lib/gitlab/diff/highlight_cache.rb'
+ - 'lib/gitlab/discussions_diff/highlight_cache.rb'
+ - 'lib/gitlab/external_authorization/client.rb'
+ - 'lib/gitlab/file_hook.rb'
+ - 'lib/gitlab/gitaly_client/conflicts_service.rb'
+ - 'lib/gitlab/graphql/pagination/active_record_array_connection.rb'
+ - 'lib/gitlab/graphql/pagination/keyset/connection.rb'
+ - 'lib/gitlab/health_checks/middleware.rb'
+ - 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb'
+ - 'lib/gitlab/import_export/json/legacy_writer.rb'
+ - 'lib/gitlab/import_export/json/ndjson_writer.rb'
+ - 'lib/gitlab/import_export/lfs_saver.rb'
+ - 'lib/gitlab/jira/http_client.rb'
+ - 'lib/gitlab/json_cache.rb'
+ - 'lib/gitlab/legacy_github_import/importer.rb'
+ - 'lib/gitlab/lfs/client.rb'
+ - 'lib/gitlab/merge_requests/mergeability/redis_interface.rb'
+ - 'lib/gitlab/middleware/read_only/controller.rb'
+ - 'lib/gitlab/patch/hangouts_chat_http_override.rb'
+ - 'lib/gitlab/puma_logging/json_formatter.rb'
+ - 'lib/gitlab/sidekiq_config.rb'
+ - 'lib/gitlab/sidekiq_daemon/monitor.rb'
+ - 'lib/gitlab/sidekiq_logging/json_formatter.rb'
+ - 'lib/gitlab/usage/metrics/aggregates/sources/postgres_hll.rb'
+ - 'lib/gitlab/utils/json_size_estimator.rb'
+ - 'lib/gitlab/version_info.rb'
+ - 'lib/gitlab/workhorse.rb'
+ - 'lib/mattermost/command.rb'
+ - 'lib/mattermost/team.rb'
+ - 'lib/microsoft_teams/notifier.rb'
+ - 'lib/tasks/gitlab/background_migrations.rake'
+ - 'lib/version_check.rb'
+ - 'spec/controllers/admin/integrations_controller_spec.rb'
+ - 'spec/controllers/concerns/product_analytics_tracking_spec.rb'
+ - 'spec/controllers/groups/settings/integrations_controller_spec.rb'
+ - 'spec/controllers/jira_connect/subscriptions_controller_spec.rb'
+ - 'spec/controllers/profiles/personal_access_tokens_controller_spec.rb'
+ - 'spec/controllers/projects/alerting/notifications_controller_spec.rb'
+ - 'spec/controllers/projects/jobs_controller_spec.rb'
+ - 'spec/controllers/projects/merge_requests/drafts_controller_spec.rb'
+ - 'spec/factories/ci/pipeline_artifacts.rb'
+ - 'spec/features/dashboard/issues_spec.rb'
+ - 'spec/features/error_tracking/user_filters_errors_by_status_spec.rb'
+ - 'spec/features/file_uploads/graphql_add_design_spec.rb'
+ - 'spec/features/groups/dependency_proxy_for_containers_spec.rb'
+ - 'spec/features/markdown/copy_as_gfm_spec.rb'
+ - 'spec/features/markdown/metrics_spec.rb'
+ - 'spec/features/projects/integrations/user_activates_jira_spec.rb'
+ - 'spec/features/projects/settings/monitor_settings_spec.rb'
+ - 'spec/frontend/fixtures/timezones.rb'
+ - 'spec/helpers/access_tokens_helper_spec.rb'
+ - 'spec/helpers/breadcrumbs_helper_spec.rb'
+ - 'spec/helpers/ci/builds_helper_spec.rb'
+ - 'spec/helpers/environment_helper_spec.rb'
+ - 'spec/helpers/environments_helper_spec.rb'
+ - 'spec/helpers/groups/group_members_helper_spec.rb'
+ - 'spec/helpers/groups_helper_spec.rb'
+ - 'spec/helpers/ide_helper_spec.rb'
+ - 'spec/helpers/invite_members_helper_spec.rb'
+ - 'spec/helpers/issuables_description_templates_helper_spec.rb'
+ - 'spec/helpers/listbox_helper_spec.rb'
+ - 'spec/helpers/namespaces_helper_spec.rb'
+ - 'spec/helpers/projects/project_members_helper_spec.rb'
+ - 'spec/helpers/projects_helper_spec.rb'
+ - 'spec/initializers/hangouts_chat_http_override_spec.rb'
+ - 'spec/lib/api/entities/merge_request_basic_spec.rb'
+ - 'spec/lib/api/helpers/caching_spec.rb'
+ - 'spec/lib/api/helpers/common_helpers_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/client_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb'
+ - 'spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb'
+ - 'spec/lib/bitbucket_server/connection_spec.rb'
+ - 'spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb'
+ - 'spec/lib/bulk_imports/projects/pipelines/snippets_pipeline_spec.rb'
+ - 'spec/lib/container_registry/client_spec.rb'
+ - 'spec/lib/container_registry/gitlab_api_client_spec.rb'
+ - 'spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb'
+ - 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
+ - 'spec/lib/gitlab/chat/responder/mattermost_spec.rb'
+ - 'spec/lib/gitlab/chat/responder/slack_spec.rb'
+ - 'spec/lib/gitlab/ci/build/releaser_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/security/common_spec.rb'
+ - 'spec/lib/gitlab/ci/parsers/test/junit_spec.rb'
+ - 'spec/lib/gitlab/ci/runner_upgrade_check_spec.rb'
+ - 'spec/lib/gitlab/composer/cache_spec.rb'
+ - 'spec/lib/gitlab/composer/version_index_spec.rb'
+ - 'spec/lib/gitlab/data_builder/pipeline_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb'
+ - 'spec/lib/gitlab/database/postgres_hll/buckets_spec.rb'
+ - 'spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb'
+ - 'spec/lib/gitlab/diff/position_spec.rb'
+ - 'spec/lib/gitlab/diff/stats_cache_spec.rb'
+ - 'spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb'
+ - 'spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb'
+ - 'spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb'
+ - 'spec/lib/gitlab/external_authorization/client_spec.rb'
+ - 'spec/lib/gitlab/external_authorization/response_spec.rb'
+ - 'spec/lib/gitlab/file_hook_spec.rb'
+ - 'spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb'
+ - 'spec/lib/gitlab/github_import/client_spec.rb'
+ - 'spec/lib/gitlab/gitlab_import/importer_spec.rb'
+ - 'spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb'
+ - 'spec/lib/gitlab/harbor/client_spec.rb'
+ - 'spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb'
+ - 'spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb'
+ - 'spec/lib/gitlab/json_cache_spec.rb'
+ - 'spec/lib/gitlab/legacy_github_import/client_spec.rb'
+ - 'spec/lib/gitlab/legacy_github_import/importer_spec.rb'
+ - 'spec/lib/gitlab/lfs/client_spec.rb'
+ - 'spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb'
+ - 'spec/lib/gitlab/middleware/multipart_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb'
+ - 'spec/lib/gitlab/tracking/service_ping_context_spec.rb'
+ - 'spec/lib/gitlab/tracking/standard_context_spec.rb'
+ - 'spec/lib/gitlab/tracking_spec.rb'
+ - 'spec/lib/gitlab/usage/service_ping/legacy_metric_timing_decorator_spec.rb'
+ - 'spec/lib/gitlab/utils/json_size_estimator_spec.rb'
+ - 'spec/lib/gitlab/version_info_spec.rb'
+ - 'spec/lib/gitlab/webpack/manifest_spec.rb'
+ - 'spec/lib/gitlab/workhorse_spec.rb'
+ - 'spec/lib/gitlab/zentao/client_spec.rb'
+ - 'spec/lib/grafana/client_spec.rb'
+ - 'spec/lib/json_web_token/hmac_token_spec.rb'
+ - 'spec/lib/mattermost/command_spec.rb'
+ - 'spec/lib/mattermost/team_spec.rb'
+ - 'spec/lib/microsoft_teams/notifier_spec.rb'
+ - 'spec/lib/object_storage/direct_upload_spec.rb'
+ - 'spec/lib/service_ping/devops_report_spec.rb'
+ - 'spec/lib/version_check_spec.rb'
+ - 'spec/mailers/notify_spec.rb'
+ - 'spec/migrations/20220204194347_encrypt_integration_properties_spec.rb'
+ - 'spec/migrations/20220412143552_consume_remaining_encrypt_integration_property_jobs_spec.rb'
+ - 'spec/models/blob_viewer/package_json_spec.rb'
+ - 'spec/models/ci/runner_spec.rb'
+ - 'spec/models/concerns/prometheus_adapter_spec.rb'
+ - 'spec/models/concerns/redis_cacheable_spec.rb'
+ - 'spec/models/concerns/sensitive_serializable_hash_spec.rb'
+ - 'spec/models/diff_discussion_spec.rb'
+ - 'spec/models/diff_note_spec.rb'
+ - 'spec/models/hooks/web_hook_spec.rb'
+ - 'spec/models/integrations/datadog_spec.rb'
+ - 'spec/models/integrations/jira_spec.rb'
+ - 'spec/models/integrations/mattermost_slash_commands_spec.rb'
+ - 'spec/models/integrations/mock_ci_spec.rb'
+ - 'spec/models/merge_request_diff_commit_spec.rb'
+ - 'spec/models/packages/composer/metadatum_spec.rb'
+ - 'spec/models/terraform/state_spec.rb'
+ - 'spec/presenters/packages/composer/packages_presenter_spec.rb'
+ - 'spec/requests/api/ci/runner/jobs_request_post_spec.rb'
+ - 'spec/requests/api/composer_packages_spec.rb'
+ - 'spec/requests/api/conan_instance_packages_spec.rb'
+ - 'spec/requests/api/conan_project_packages_spec.rb'
+ - 'spec/requests/api/container_registry_event_spec.rb'
+ - 'spec/requests/api/graphql/mutations/design_management/upload_spec.rb'
+ - 'spec/requests/api/integrations/jira_connect/subscriptions_spec.rb'
+ - 'spec/requests/api/internal/base_spec.rb'
+ - 'spec/requests/api/merge_requests_spec.rb'
+ - 'spec/requests/api/namespaces_spec.rb'
+ - 'spec/requests/api/project_snapshots_spec.rb'
+ - 'spec/requests/projects/incident_management/pagerduty_incidents_spec.rb'
+ - 'spec/requests/users_controller_spec.rb'
+ - 'spec/requests/whats_new_controller_spec.rb'
+ - 'spec/scripts/pipeline_test_report_builder_spec.rb'
+ - 'spec/serializers/ci/dag_job_entity_spec.rb'
+ - 'spec/serializers/ci/dag_job_group_entity_spec.rb'
+ - 'spec/serializers/ci/dag_pipeline_entity_spec.rb'
+ - 'spec/serializers/ci/dag_pipeline_serializer_spec.rb'
+ - 'spec/serializers/ci/dag_stage_entity_spec.rb'
+ - 'spec/serializers/ci/daily_build_group_report_result_serializer_spec.rb'
+ - 'spec/serializers/ci/lint/result_serializer_spec.rb'
+ - 'spec/serializers/ci/trigger_entity_spec.rb'
+ - 'spec/serializers/ci/trigger_serializer_spec.rb'
+ - 'spec/serializers/diff_line_serializer_spec.rb'
+ - 'spec/serializers/evidences/evidence_entity_spec.rb'
+ - 'spec/serializers/feature_flags_client_serializer_spec.rb'
+ - 'spec/serializers/group_link/group_group_link_entity_spec.rb'
+ - 'spec/serializers/group_link/group_group_link_serializer_spec.rb'
+ - 'spec/serializers/group_link/group_link_entity_spec.rb'
+ - 'spec/serializers/group_link/project_group_link_entity_spec.rb'
+ - 'spec/serializers/group_link/project_group_link_serializer_spec.rb'
+ - 'spec/serializers/member_entity_spec.rb'
+ - 'spec/serializers/member_serializer_spec.rb'
+ - 'spec/serializers/member_user_entity_spec.rb'
+ - 'spec/serializers/test_reports_comparer_serializer_spec.rb'
+ - 'spec/services/ci/runners/process_runner_version_update_service_spec.rb'
+ - 'spec/services/draft_notes/create_service_spec.rb'
+ - 'spec/services/error_tracking/issue_details_service_spec.rb'
+ - 'spec/services/error_tracking/issue_latest_event_service_spec.rb'
+ - 'spec/services/error_tracking/list_issues_service_spec.rb'
+ - 'spec/services/git/branch_push_service_spec.rb'
+ - 'spec/services/jira/requests/projects/list_service_spec.rb'
+ - 'spec/services/metrics/dashboard/transient_embed_service_spec.rb'
+ - 'spec/services/packages/composer/create_package_service_spec.rb'
+ - 'spec/services/packages/rubygems/metadata_extraction_service_spec.rb'
+ - 'spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb'
+ - 'spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb'
+ - 'spec/services/service_ping/submit_service_ping_service_spec.rb'
+ - 'spec/services/webauthn/authenticate_service_spec.rb'
+ - 'spec/services/webauthn/register_service_spec.rb'
+ - 'spec/support/frontend_fixtures.rb'
+ - 'spec/support/google_api/cloud_platform_helpers.rb'
+ - 'spec/support/helpers/ci_artifact_metadata_generator.rb'
+ - 'spec/support/helpers/dependency_proxy_helpers.rb'
+ - 'spec/support/helpers/fake_webauthn_device.rb'
+ - 'spec/support/helpers/features/two_factor_helpers.rb'
+ - 'spec/support/helpers/graphql_helpers.rb'
+ - 'spec/support/helpers/input_helper.rb'
+ - 'spec/support/helpers/jira_integration_helpers.rb'
+ - 'spec/support/helpers/kubernetes_helpers.rb'
+ - 'spec/support/helpers/prometheus_helpers.rb'
+ - 'spec/support/helpers/sentry_client_helpers.rb'
+ - 'spec/support/helpers/usage_data_helpers.rb'
+ - 'spec/support/import_export/configuration_helper.rb'
+ - 'spec/support/shared_contexts/bulk_imports_requests_shared_context.rb'
+ - 'spec/support/shared_contexts/features/error_tracking_shared_context.rb'
+ - 'spec/support/shared_contexts/prometheus/alert_shared_context.rb'
+ - 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb'
+ - 'spec/support/shared_examples/blocks_unsafe_serialization_shared_examples.rb'
+ - 'spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb'
+ - 'spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb'
+ - 'spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb'
+ - 'spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb'
+ - 'spec/support/shared_examples/harbor/tags_controller_shared_examples.rb'
+ - 'spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/rack_attack_shared_examples.rb'
+ - 'spec/support_specs/helpers/graphql_helpers_spec.rb'
+ - 'spec/tasks/gitlab/update_templates_rake_spec.rb'
+ - 'spec/tasks/gitlab/usage_data_rake_spec.rb'
+ - 'spec/tooling/lib/tooling/kubernetes_client_spec.rb'
+ - 'spec/tooling/rspec_flaky/listener_spec.rb'
+ - 'spec/workers/ci/runners/process_runner_version_update_worker_spec.rb'
+ - 'spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb'
+ - 'spec/workers/packages/composer/cache_update_worker_spec.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index fe81f057738..bcdd23a89c1 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-334a620a54df6bbb1563c440514e06d7068255e7
+ef8362fdf1c0eca9c73fb0fa4dc5b45c5c7965d8
diff --git a/app/assets/javascripts/groups_projects/components/transfer_locations.vue b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
new file mode 100644
index 00000000000..11d7fa8d65b
--- /dev/null
+++ b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
@@ -0,0 +1,233 @@
+<script>
+import {
+ GlAlert,
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { s__, __ } from '~/locale';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import currentUserNamespace from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
+
+export const i18n = {
+ SELECT_A_NAMESPACE: __('Select a new namespace'),
+ GROUPS: __('Groups'),
+ USERS: __('Users'),
+ ERROR_MESSAGE: s__(
+ 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.',
+ ),
+ ALERT_DISMISS_LABEL: __('Dismiss'),
+};
+
+export default {
+ name: 'TransferLocations',
+ components: {
+ GlAlert,
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+ },
+ inject: ['resourceId'],
+ props: {
+ value: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ groupTransferLocationsApiMethod: {
+ type: Function,
+ required: true,
+ },
+ },
+ initialTransferLocationsLoaded: false,
+ data() {
+ return {
+ searchTerm: '',
+ userTransferLocations: [],
+ groupTransferLocations: [],
+ isLoading: false,
+ isSearchLoading: false,
+ hasError: false,
+ page: 1,
+ totalPages: 1,
+ };
+ },
+ computed: {
+ hasUserTransferLocations() {
+ return this.userTransferLocations.length;
+ },
+ hasGroupTransferLocations() {
+ return this.groupTransferLocations.length;
+ },
+ selectedText() {
+ return this.value?.humanName || i18n.SELECT_A_NAMESPACE;
+ },
+ hasNextPageOfGroups() {
+ return this.page < this.totalPages;
+ },
+ },
+ watch: {
+ searchTerm() {
+ this.page = 1;
+
+ this.debouncedSearch();
+ },
+ },
+ methods: {
+ handleSelect(item) {
+ this.searchTerm = '';
+ this.$emit('input', item);
+ },
+ async handleShow() {
+ if (this.$options.initialTransferLocationsLoaded) {
+ return;
+ }
+
+ this.isLoading = true;
+
+ [this.groupTransferLocations, this.userTransferLocations] = await Promise.all([
+ this.getGroupTransferLocations(),
+ this.getUserTransferLocations(),
+ ]);
+
+ this.isLoading = false;
+ this.$options.initialTransferLocationsLoaded = true;
+ },
+ async getGroupTransferLocations() {
+ try {
+ const {
+ data: groupTransferLocations,
+ headers,
+ } = await this.groupTransferLocationsApiMethod(this.resourceId, {
+ page: this.page,
+ search: this.searchTerm,
+ });
+
+ const { totalPages } = parseIntPagination(normalizeHeaders(headers));
+ this.totalPages = totalPages;
+
+ return groupTransferLocations.map(({ id, full_name: humanName }) => ({
+ id,
+ humanName,
+ }));
+ } catch {
+ this.handleError();
+
+ return [];
+ }
+ },
+ async getUserTransferLocations() {
+ try {
+ const {
+ data: {
+ currentUser: { namespace },
+ },
+ } = await this.$apollo.query({
+ query: currentUserNamespace,
+ });
+
+ if (!namespace) {
+ return [];
+ }
+
+ return [
+ {
+ id: getIdFromGraphQLId(namespace.id),
+ humanName: namespace.fullName,
+ },
+ ];
+ } catch {
+ this.handleError();
+
+ return [];
+ }
+ },
+ async handleLoadMoreGroups() {
+ this.isLoading = true;
+ this.page += 1;
+
+ const groupTransferLocations = await this.getGroupTransferLocations();
+ this.groupTransferLocations.push(...groupTransferLocations);
+
+ this.isLoading = false;
+ },
+ debouncedSearch: debounce(async function debouncedSearch() {
+ this.isSearchLoading = true;
+
+ this.groupTransferLocations = await this.getGroupTransferLocations();
+
+ this.isSearchLoading = false;
+ }, DEBOUNCE_DELAY),
+ handleError() {
+ this.hasError = true;
+ },
+ handleAlertDismiss() {
+ this.hasError = false;
+ },
+ },
+ i18n,
+};
+</script>
+<template>
+ <div>
+ <gl-alert
+ v-if="hasError"
+ variant="danger"
+ :dismiss-label="$options.i18n.ALERT_DISMISS_LABEL"
+ @dismiss="handleAlertDismiss"
+ >{{ $options.i18n.ERROR_MESSAGE }}</gl-alert
+ >
+ <gl-form-group :label="$options.i18n.SELECT_A_NAMESPACE">
+ <gl-dropdown :text="selectedText" data-qa-selector="namespaces_list" block @show="handleShow">
+ <template #header>
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ :is-loading="isSearchLoading"
+ data-qa-selector="namespaces_list_search"
+ />
+ </template>
+ <div
+ v-if="hasUserTransferLocations"
+ data-qa-selector="namespaces_list_users"
+ data-testid="user-transfer-locations"
+ >
+ <gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in userTransferLocations"
+ :key="item.id"
+ data-qa-selector="namespaces_list_item"
+ @click="handleSelect(item)"
+ >{{ item.humanName }}</gl-dropdown-item
+ >
+ </div>
+ <div
+ v-if="hasGroupTransferLocations"
+ data-qa-selector="namespaces_list_groups"
+ data-testid="group-transfer-locations"
+ >
+ <gl-dropdown-section-header>{{ $options.i18n.GROUPS }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in groupTransferLocations"
+ :key="item.id"
+ data-qa-selector="namespaces_list_item"
+ @click="handleSelect(item)"
+ >{{ item.humanName }}</gl-dropdown-item
+ >
+ </div>
+ <gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" />
+ <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="handleLoadMoreGroups" />
+ </gl-dropdown>
+ </gl-form-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql b/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql
new file mode 100644
index 00000000000..d350072425b
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql
@@ -0,0 +1,5 @@
+mutation moveIssue($moveIssueInput: IssueMoveInput!) {
+ issueMove(input: $moveIssueInput) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue b/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue
new file mode 100644
index 00000000000..6e287ac3bb7
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue
@@ -0,0 +1,171 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
+import createFlash from '~/flash';
+import { logError } from '~/lib/logger';
+import { s__ } from '~/locale';
+import {
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+} from '~/work_items/constants';
+import issuableEventHub from '~/issues/list/eventhub';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import getIssuesCountQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
+import moveIssueMutation from './graphql/mutations/move_issue.mutation.graphql';
+
+export default {
+ name: 'MoveIssuesButton',
+ components: {
+ IssuableMoveDropdown,
+ GlAlert,
+ },
+ props: {
+ projectFullPath: {
+ type: String,
+ required: true,
+ },
+ projectsFetchPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ selectedIssuables: [],
+ moveInProgress: false,
+ };
+ },
+ computed: {
+ cannotMoveTasksWarningTitle() {
+ if (this.tasksSelected && this.testCasesSelected) {
+ return s__('Issues|Tasks and test cases can not be moved.');
+ }
+
+ if (this.testCasesSelected) {
+ return s__('Issues|Test cases can not be moved.');
+ }
+
+ return s__('Issues|Tasks can not be moved.');
+ },
+ issuesSelected() {
+ return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_ISSUE);
+ },
+ incidentsSelected() {
+ return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_INCIDENT);
+ },
+ tasksSelected() {
+ return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_TASK);
+ },
+ testCasesSelected() {
+ return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_TEST_CASE);
+ },
+ },
+ mounted() {
+ issuableEventHub.$on('issuables:issuableChecked', this.handleIssuableChecked);
+ },
+ beforeDestroy() {
+ issuableEventHub.$off('issuables:issuableChecked', this.handleIssuableChecked);
+ },
+ methods: {
+ handleIssuableChecked(issuable, value) {
+ if (value) {
+ this.selectedIssuables.push(issuable);
+ } else {
+ const index = this.selectedIssuables.indexOf(issuable);
+ if (index > -1) {
+ this.selectedIssuables.splice(index, 1);
+ }
+ }
+ },
+ moveIssues(targetProject) {
+ const iids = this.selectedIssuables.reduce((result, issueData) => {
+ if (
+ issueData.type === WORK_ITEM_TYPE_ENUM_ISSUE ||
+ issueData.type === WORK_ITEM_TYPE_ENUM_INCIDENT
+ ) {
+ result.push(issueData.iid);
+ }
+ return result;
+ }, []);
+
+ if (iids.length === 0) {
+ return;
+ }
+
+ this.moveInProgress = true;
+ issuableEventHub.$emit('issuables:bulkMoveStarted');
+
+ const promises = iids.map((id) => {
+ return this.moveIssue(id, targetProject);
+ });
+
+ Promise.all(promises)
+ .then((promisesResult) => {
+ let foundError = false;
+
+ for (const promiseResult of promisesResult) {
+ if (promiseResult.data.issueMove?.errors?.length) {
+ foundError = true;
+ logError(
+ `Error moving issue. Error message: ${promiseResult.data.issueMove.errors[0].message}`,
+ );
+ }
+ }
+
+ if (!foundError) {
+ const client = this.$apollo.provider.defaultClient;
+ client.refetchQueries({
+ include: [getIssuesQuery, getIssuesCountQuery],
+ });
+ this.moveInProgress = false;
+ this.selectedIssuables = [];
+ issuableEventHub.$emit('issuables:bulkMoveEnded');
+ } else {
+ throw new Error();
+ }
+ })
+ .catch(() => {
+ this.moveInProgress = false;
+ issuableEventHub.$emit('issuables:bulkMoveEnded');
+
+ createFlash({
+ message: s__(`Issues|There was an error while moving the issues.`),
+ });
+ });
+ },
+ moveIssue(issueIid, targetProject) {
+ return this.$apollo.mutate({
+ mutation: moveIssueMutation,
+ variables: {
+ moveIssueInput: {
+ projectPath: this.projectFullPath,
+ iid: issueIid,
+ targetProjectPath: targetProject.full_path,
+ },
+ },
+ });
+ },
+ },
+ i18n: {
+ dropdownButtonTitle: s__('Issues|Move selected'),
+ },
+};
+</script>
+<template>
+ <div>
+ <issuable-move-dropdown
+ :project-full-path="projectFullPath"
+ :projects-fetch-path="projectsFetchPath"
+ :move-in-progress="moveInProgress"
+ :disabled="!issuesSelected && !incidentsSelected"
+ :dropdown-header-title="$options.i18n.dropdownButtonTitle"
+ :dropdown-button-title="$options.i18n.dropdownButtonTitle"
+ @move-issuable="moveIssues"
+ />
+ <gl-alert v-if="tasksSelected || testCasesSelected" :dismissible="false" variant="warning">
+ {{ cannotMoveTasksWarningTitle }}
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
index 4657771353f..b7cb805ee37 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { gqlClient } from '../../issues/list/graphql';
import StatusDropdown from './components/status_dropdown.vue';
import SubscriptionsDropdown from './components/subscriptions_dropdown.vue';
+import MoveIssuesButton from './components/move_issues_button.vue';
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
@@ -42,3 +45,31 @@ export function initSubscriptionsDropdown() {
render: (createElement) => createElement(SubscriptionsDropdown),
});
}
+
+export function initMoveIssuesButton() {
+ const el = document.querySelector('.js-move-issues');
+
+ if (!el) {
+ return null;
+ }
+
+ const { dataset } = el;
+
+ Vue.use(VueApollo);
+ const apolloProvider = new VueApollo({
+ defaultClient: gqlClient,
+ });
+
+ return new Vue({
+ el,
+ name: 'MoveIssuesRoot',
+ apolloProvider,
+ render: (createElement) =>
+ createElement(MoveIssuesButton, {
+ props: {
+ projectFullPath: dataset.projectFullPath,
+ projectsFetchPath: dataset.projectsFetchPath,
+ },
+ }),
+ });
+}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
index a33c6ae8030..be61831fc4d 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
@@ -46,6 +46,11 @@ export default class IssuableBulkUpdateSidebar {
// https://gitlab.com/gitlab-org/gitlab/-/issues/325874
issuableEventHub.$on('issuables:enableBulkEdit', () => this.toggleBulkEdit(null, true));
issuableEventHub.$on('issuables:updateBulkEdit', () => this.updateFormState());
+
+ // These events are connected to the logic inside `move_issues_button.vue`,
+ // so that only one action can be performed at a time
+ issuableEventHub.$on('issuables:bulkMoveStarted', () => this.toggleSubmitButtonDisabled(true));
+ issuableEventHub.$on('issuables:bulkMoveEnded', () => this.updateFormState());
}
initDropdowns() {
@@ -89,6 +94,8 @@ export default class IssuableBulkUpdateSidebar {
this.updateSelectedIssuableIds();
IssuableBulkUpdateActions.setOriginalDropdownData();
+
+ issuableEventHub.$emit('issuables:selectionChanged', !noCheckedIssues);
}
prepForSubmit() {
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index acb6aa93f0f..a110ba658f7 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -565,6 +565,7 @@ export default {
bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
bulkUpdateSidebar.initStatusDropdown();
bulkUpdateSidebar.initSubscriptionsDropdown();
+ bulkUpdateSidebar.initMoveIssuesButton();
const usersSelect = await import('~/users_select');
const UsersSelect = usersSelect.default;
diff --git a/app/assets/javascripts/issues/list/graphql.js b/app/assets/javascripts/issues/list/graphql.js
new file mode 100644
index 00000000000..5ef61727a3d
--- /dev/null
+++ b/app/assets/javascripts/issues/list/graphql.js
@@ -0,0 +1,25 @@
+import produce from 'immer';
+import createDefaultClient from '~/lib/graphql';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+
+const resolvers = {
+ Mutation: {
+ reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => {
+ const variables = JSON.parse(serializedVariables);
+ const sourceData = cache.readQuery({ query: getIssuesQuery, variables });
+
+ const data = produce(sourceData, (draftData) => {
+ const issues = draftData[namespace].issues.nodes.slice();
+ const issueToMove = issues[oldIndex];
+ issues.splice(oldIndex, 1);
+ issues.splice(newIndex, 0, issueToMove);
+
+ draftData[namespace].issues.nodes = issues;
+ });
+
+ cache.writeQuery({ query: getIssuesQuery, variables, data });
+ },
+ },
+};
+
+export const gqlClient = createDefaultClient(resolvers);
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 93333c31b34..569ca006af5 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -1,12 +1,11 @@
-import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
-import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import JiraIssuesImportStatusRoot from './components/jira_issues_import_status_app.vue';
+import { gqlClient } from './graphql';
export function mountJiraIssuesListApp() {
const el = document.querySelector('.js-jira-issues-import-status');
@@ -56,26 +55,6 @@ export function mountIssuesListApp() {
Vue.use(VueApollo);
Vue.use(VueRouter);
- const resolvers = {
- Mutation: {
- reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => {
- const variables = JSON.parse(serializedVariables);
- const sourceData = cache.readQuery({ query: getIssuesQuery, variables });
-
- const data = produce(sourceData, (draftData) => {
- const issues = draftData[namespace].issues.nodes.slice();
- const issueToMove = issues[oldIndex];
- issues.splice(oldIndex, 1);
- issues.splice(newIndex, 0, issueToMove);
-
- draftData[namespace].issues.nodes = issues;
- });
-
- cache.writeQuery({ query: getIssuesQuery, variables, data });
- },
- },
- };
-
const {
autocompleteAwardEmojisPath,
calendarPath,
@@ -125,7 +104,7 @@ export function mountIssuesListApp() {
el,
name: 'IssuesListRoot',
apolloProvider: new VueApollo({
- defaultClient: createDefaultClient(resolvers),
+ defaultClient: gqlClient,
}),
router: new VueRouter({
base: window.location.pathname,
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index 4941f22230b..ed5466ff99c 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -219,7 +219,6 @@ export default {
:empty-message="$options.i18n.empty.merge"
:keep-component-mounted="false"
:is-empty="isEmpty"
- :is-invalid="isInvalid"
:is-unavailable="isLintUnavailable"
:title="$options.i18n.tabMergedYaml"
lazy
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
index 318940478a8..ebfc7d312b4 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -45,18 +45,17 @@ export default {
};
},
update({ project: { branchRules } }) {
- this.branchProtection = branchRules.nodes.find(
- (rule) => rule.name === this.branch,
- )?.branchProtection;
+ const branchRule = branchRules.nodes.find((rule) => rule.name === this.branch);
+ this.branchProtection = branchRule?.branchProtection;
+ this.approvalRules = branchRule?.approvalRules;
},
},
},
data() {
return {
branch: getParameterByName(BRANCH_PARAM_NAME),
- branchProtection: {
- approvalRules: {},
- },
+ branchProtection: {},
+ approvalRules: {},
};
},
computed: {
@@ -104,7 +103,7 @@ export default {
: this.$options.i18n.branchNameOrPattern;
},
approvals() {
- return this.branchProtection?.approvalRules?.nodes || [];
+ return this.approvalRules?.nodes || [];
},
},
methods: {
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
index 28a1c09fa82..12de136a21a 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
@@ -64,10 +64,10 @@ export default {
<template>
<div
- class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4"
+ class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4 gl-border-t-1"
:class="{ 'gl-border-t-solid': showDivider }"
>
- <div class="gl-display-flex gl-w-half gl-justify-content-space-between">
+ <div class="gl-display-flex gl-w-half gl-justify-content-space-between gl-align-items-center">
<div class="gl-mr-7 gl-w-quarter">{{ title }}</div>
<gl-avatars-inline
diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
index 3ac165498a1..4ca474a5ceb 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
+++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
@@ -44,6 +44,23 @@ query getBranchRulesDetails($projectPath: ID!) {
}
}
}
+ approvalRules {
+ nodes {
+ id
+ name
+ type
+ approvalsRequired
+ eligibleApprovers {
+ nodes {
+ id
+ name
+ username
+ webUrl
+ avatarUrl
+ }
+ }
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
index 55420c9c732..886db07a901 100644
--- a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
+++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
@@ -1,30 +1,14 @@
<script>
-import { GlFormGroup, GlAlert } from '@gitlab/ui';
-import { debounce } from 'lodash';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
import { getTransferLocations } from '~/api/projects_api';
-import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
-import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import { s__, __ } from '~/locale';
-import currentUserNamespace from '../graphql/queries/current_user_namespace.query.graphql';
export default {
name: 'TransferProjectForm',
components: {
- GlFormGroup,
- NamespaceSelect,
+ TransferLocations,
ConfirmDanger,
- GlAlert,
},
- i18n: {
- errorMessage: s__(
- 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.',
- ),
- alertDismissAlert: __('Dismiss'),
- },
- inject: ['projectId'],
props: {
confirmationPhrase: {
type: String,
@@ -37,146 +21,32 @@ export default {
},
data() {
return {
- userNamespaces: [],
- groupNamespaces: [],
- initialNamespacesLoaded: false,
- selectedNamespace: null,
- hasError: false,
- isLoading: false,
- isSearchLoading: false,
- searchTerm: '',
- page: 1,
- totalPages: 1,
+ selectedTransferLocation: null,
};
},
+
computed: {
hasSelectedNamespace() {
- return Boolean(this.selectedNamespace?.id);
+ return Boolean(this.selectedTransferLocation?.id);
},
- hasNextPageOfGroups() {
- return this.page < this.totalPages;
+ },
+ watch: {
+ selectedTransferLocation(selectedTransferLocation) {
+ this.$emit('selectTransferLocation', selectedTransferLocation.id);
},
},
methods: {
- async handleShow() {
- if (this.initialNamespacesLoaded) {
- return;
- }
-
- this.isLoading = true;
-
- [this.groupNamespaces, this.userNamespaces] = await Promise.all([
- this.getGroupNamespaces(),
- this.getUserNamespaces(),
- ]);
-
- this.isLoading = false;
- this.initialNamespacesLoaded = true;
- },
- handleSelect(selectedNamespace) {
- this.selectedNamespace = selectedNamespace;
- this.$emit('selectNamespace', selectedNamespace.id);
- },
- async getGroupNamespaces() {
- try {
- const { data: groupNamespaces, headers } = await getTransferLocations(this.projectId, {
- page: this.page,
- search: this.searchTerm,
- });
-
- const { totalPages } = parseIntPagination(normalizeHeaders(headers));
- this.totalPages = totalPages;
-
- return groupNamespaces.map(({ id, full_name: humanName }) => ({
- id,
- humanName,
- }));
- } catch (error) {
- this.hasError = true;
-
- return [];
- }
- },
- async getUserNamespaces() {
- try {
- const {
- data: {
- currentUser: { namespace },
- },
- } = await this.$apollo.query({
- query: currentUserNamespace,
- });
-
- if (!namespace) {
- return [];
- }
-
- return [
- {
- id: getIdFromGraphQLId(namespace.id),
- humanName: namespace.fullName,
- },
- ];
- } catch (error) {
- this.hasError = true;
-
- return [];
- }
- },
- async handleLoadMoreGroups() {
- this.isLoading = true;
- this.page += 1;
-
- const groupNamespaces = await this.getGroupNamespaces();
- this.groupNamespaces.push(...groupNamespaces);
-
- this.isLoading = false;
- },
- debouncedSearch: debounce(async function debouncedSearch() {
- this.isSearchLoading = true;
-
- this.groupNamespaces = await this.getGroupNamespaces();
-
- this.isSearchLoading = false;
- }, DEBOUNCE_DELAY),
- handleSearch(searchTerm) {
- this.searchTerm = searchTerm;
- this.page = 1;
-
- this.debouncedSearch();
- },
- handleAlertDismiss() {
- this.hasError = false;
- },
+ getTransferLocations,
},
};
</script>
<template>
<div>
- <gl-alert
- v-if="hasError"
- variant="danger"
- :dismiss-label="$options.i18n.alertDismissLabel"
- @dismiss="handleAlertDismiss"
- >{{ $options.i18n.errorMessage }}</gl-alert
- >
- <gl-form-group>
- <namespace-select
- data-testid="transfer-project-namespace"
- :full-width="true"
- :group-namespaces="groupNamespaces"
- :user-namespaces="userNamespaces"
- :selected-namespace="selectedNamespace"
- :has-next-page-of-groups="hasNextPageOfGroups"
- :is-loading="isLoading"
- :is-search-loading="isSearchLoading"
- :should-filter-namespaces="false"
- @select="handleSelect"
- @load-more-groups="handleLoadMoreGroups"
- @search="handleSearch"
- @show="handleShow"
- />
- </gl-form-group>
+ <transfer-locations
+ v-model="selectedTransferLocation"
+ data-testid="transfer-project-namespace"
+ :group-transfer-locations-api-method="getTransferLocations"
+ />
<confirm-danger
:disabled="!hasSelectedNamespace"
:phrase="confirmationPhrase"
diff --git a/app/assets/javascripts/projects/settings/init_transfer_project_form.js b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
index 89c158a9ba8..7f810e647ae 100644
--- a/app/assets/javascripts/projects/settings/init_transfer_project_form.js
+++ b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
@@ -12,7 +12,7 @@ export default () => {
Vue.use(VueApollo);
const {
- projectId,
+ projectId: resourceId,
targetFormId = null,
targetHiddenInputId = null,
buttonText: confirmButtonText = '',
@@ -27,7 +27,7 @@ export default () => {
}),
provide: {
confirmDangerMessage,
- projectId,
+ resourceId,
},
render(createElement) {
return createElement(TransferProjectForm, {
@@ -36,7 +36,7 @@ export default () => {
confirmationPhrase,
},
on: {
- selectNamespace: (id) => {
+ selectTransferLocation: (id) => {
if (targetHiddenInputId && document.getElementById(targetHiddenInputId)) {
document.getElementById(targetHiddenInputId).value = id;
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
index 0f5560ff628..02323e5a0c6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
@@ -43,6 +43,11 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -128,7 +133,7 @@ export default {
</script>
<template>
- <div class="block js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown">
+ <div class="js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown">
<div
v-gl-tooltip.left.viewport
data-testid="move-collapsed"
@@ -141,7 +146,7 @@ export default {
<gl-dropdown
ref="dropdown"
:block="true"
- :disabled="moveInProgress"
+ :disabled="moveInProgress || disabled"
class="hide-collapsed"
toggle-class="js-sidebar-dropdown-toggle"
@shown="fetchProjects"
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index 7e735f358eb..9fbf8042784 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -62,6 +62,9 @@ export default {
issuableId() {
return getIdFromGraphQLId(this.issuable.id);
},
+ issuableIid() {
+ return this.issuable.iid;
+ },
createdInPastDay() {
const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
return createdSecondsAgo < SECONDS_IN_DAY;
@@ -193,6 +196,8 @@ export default {
class="issue-check gl-mr-0"
:checked="checked"
:data-id="issuableId"
+ :data-iid="issuableIid"
+ :data-type="issuable.type"
@input="$emit('checked-input', $event)"
>
<span class="gl-sr-only">{{ issuable.title }}</span>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index bc10f84b819..0318dd22bfa 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -7,6 +7,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import issuableEventHub from '~/issues/list/eventhub';
import { DEFAULT_SKELETON_COUNT, PAGE_SIZE_STORAGE_KEY } from '../constants';
import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue';
import IssuableItem from './issuable_item.vue';
@@ -266,6 +267,7 @@ export default {
handleIssuableCheckedInput(issuable, value) {
this.checkedIssuables[this.issuableId(issuable)].checked = value;
this.$emit('update-legacy-bulk-edit');
+ issuableEventHub.$emit('issuables:issuableChecked', issuable, value);
},
handleAllIssuablesCheckedInput(value) {
Object.keys(this.checkedIssuables).forEach((issuableId) => {
diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb
index f3283c88740..89e8a261288 100644
--- a/app/controllers/projects/alerting/notifications_controller.rb
+++ b/app/controllers/projects/alerting/notifications_controller.rb
@@ -18,8 +18,9 @@ module Projects
def create
token = extract_alert_manager_token(request)
result = notify_service.execute(token, integration)
+ has_something_to_return = result.success? && result.http_status != :created
- if result.success?
+ if has_something_to_return
render json: AlertManagement::AlertSerializer.new.represent(result.payload[:alerts]), code: result.http_status
else
head result.http_status
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 418e7233e21..f9a11ddb1db 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -38,9 +38,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
diffs = @compare.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options_hash)
unfoldable_positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user).unfoldable
- diffs.unfold_diff_files(unfoldable_positions)
- diffs.write_cache
-
options = {
merge_request: @merge_request,
commit: commit,
@@ -63,7 +60,16 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
options[:allow_tree_conflicts]
]
- return unless stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs])
+ if Feature.enabled?(:check_etags_diffs_batch_before_write_cache, merge_request.project) && !stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs])
+ return
+ end
+
+ diffs.unfold_diff_files(unfoldable_positions)
+ diffs.write_cache
+
+ if Feature.disabled?(:check_etags_diffs_batch_before_write_cache, merge_request.project) && !stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs])
+ return
+ end
render json: PaginatedDiffSerializer.new(current_user: current_user).represent(diffs, options)
end
diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb
index c3dc17694d9..27ac64e5758 100644
--- a/app/controllers/projects/prometheus/alerts_controller.rb
+++ b/app/controllers/projects/prometheus/alerts_controller.rb
@@ -23,11 +23,7 @@ module Projects
token = extract_alert_manager_token(request)
result = notify_service.execute(token)
- if result.success?
- render json: AlertManagement::AlertSerializer.new.represent(result.payload[:alerts]), code: result.http_status
- else
- head result.http_status
- end
+ head result.http_status
end
private
@@ -37,19 +33,6 @@ module Projects
.new(project, params.permit!)
end
- def serialize_as_json(alert_obj)
- serializer.represent(alert_obj)
- end
-
- def serializer
- PrometheusAlertSerializer
- .new(project: project, current_user: current_user)
- end
-
- def alerts
- alerts_finder.execute
- end
-
def alert
@alert ||= alerts_finder(metric: params[:id]).execute.first || render_404
end
diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb
index b4235a77867..51457d443a1 100644
--- a/app/finders/license_template_finder.rb
+++ b/app/finders/license_template_finder.rb
@@ -34,9 +34,13 @@ class LicenseTemplateFinder
private
+ def available_licenses
+ Licensee::License.all(featured: popular_only?)
+ end
+
def vendored_licenses
strong_memoize(:vendored_licenses) do
- Licensee::License.all(featured: popular_only?).map do |license|
+ available_licenses.map do |license|
LicenseTemplate.new(
key: license.key,
name: license.name,
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 8cc600fc68e..c98cfed7493 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -100,8 +100,7 @@ module Types
field :detailed_merge_status, ::Types::MergeRequests::DetailedMergeStatusEnum, null: true,
calls_gitaly: true,
- description: 'Detailed merge status of the merge request.',
- alpha: { milestone: '15.3' }
+ description: 'Detailed merge status of the merge request.'
field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true,
calls_gitaly: true,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 361b1a8dca9..a1bef44a815 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -636,6 +636,10 @@ class ApplicationSetting < ApplicationRecord
addressable_url: { allow_localhost: true, allow_local_network: false },
allow_blank: true
+ validates :product_analytics_enabled,
+ presence: true,
+ allow_blank: true
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
diff --git a/app/models/diff_viewer/server_side.rb b/app/models/diff_viewer/server_side.rb
index a1defb2594f..fb127de2bc7 100644
--- a/app/models/diff_viewer/server_side.rb
+++ b/app/models/diff_viewer/server_side.rb
@@ -9,14 +9,6 @@ module DiffViewer
self.size_limit = 5.megabytes
end
- def prepare!
- return if Feature.enabled?(:disable_load_entire_blob_for_diff_viewer, diff_file.repository.project)
-
- # TODO: remove this after resolving #342703
- diff_file.old_blob&.load_all_data!
- diff_file.new_blob&.load_all_data!
- end
-
def render_error
# Files that are not stored in the repository, like LFS files and
# build artifacts, can only be rendered using a client-side viewer,
diff --git a/app/models/experiment.rb b/app/models/experiment.rb
index 2300ec2996d..3710abc46ea 100644
--- a/app/models/experiment.rb
+++ b/app/models/experiment.rb
@@ -1,50 +1,18 @@
# frozen_string_literal: true
class Experiment < ApplicationRecord
- has_many :experiment_users
has_many :experiment_subjects, inverse_of: :experiment
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
- def self.add_user(name, group_type, user, context = {})
- by_name(name).record_user_and_group(user, group_type, context)
- end
-
- def self.add_group(name, variant:, group:)
- add_subject(name, variant: variant, subject: group)
- end
-
def self.add_subject(name, variant:, subject:)
by_name(name).record_subject_and_variant!(subject, variant)
end
- def self.record_conversion_event(name, user, context = {})
- by_name(name).record_conversion_event_for_user(user, context)
- end
-
def self.by_name(name)
find_or_create_by!(name: name)
end
- # Create or update the recorded experiment_user row for the user in this experiment.
- def record_user_and_group(user, group_type, context = {})
- experiment_user = experiment_users.find_or_initialize_by(user: user)
- experiment_user.assign_attributes(group_type: group_type, context: merged_context(experiment_user, context))
- # We only call save when necessary because this causes the request to stick to the primary DB
- # even when the save is a no-op
- # https://gitlab.com/gitlab-org/gitlab/-/issues/324649
- experiment_user.save! if experiment_user.changed?
-
- experiment_user
- end
-
- def record_conversion_event_for_user(user, context = {})
- experiment_user = experiment_users.find_by(user: user)
- return unless experiment_user
-
- experiment_user.update!(converted_at: Time.current, context: merged_context(experiment_user, context))
- end
-
def record_conversion_event_for_subject(subject, context = {})
raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject)
diff --git a/app/models/experiment_user.rb b/app/models/experiment_user.rb
deleted file mode 100644
index e447becc1bd..00000000000
--- a/app/models/experiment_user.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-class ExperimentUser < ApplicationRecord
- include ::Gitlab::Experimentation::GroupTypes
-
- belongs_to :experiment
- belongs_to :user
-
- enum group_type: { GROUP_CONTROL => 0, GROUP_EXPERIMENTAL => 1 }
-
- validates :experiment_id, presence: true
- validates :user_id, presence: true
- validates :group_type, presence: true
-end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index ab0fdbd777f..d4867600853 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -91,7 +91,7 @@ module Integrations
with_options if: :activated? do
validates :api_key, presence: true, format: { with: /\A\w+\z/ }
- validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true }
+ validates :datadog_site, format: { with: %r{\A\w+([-.]\w+)*\.[a-zA-Z]{2,5}(:[0-9]{1,5})?\z}, allow_blank: true }
validates :api_url, public_url: { allow_blank: true }
validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? }
validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? }
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index 29e1ba88528..62e2e75db2a 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -14,6 +14,10 @@ module Ml
default_value_for(:iid) { SecureRandom.uuid }
+ def artifact_root
+ "/ml_candidate_#{iid}/-/"
+ end
+
class << self
def with_project_id_and_iid(project_id, iid)
return unless project_id.present? && iid.present?
diff --git a/app/services/bulk_imports/create_pipeline_trackers_service.rb b/app/services/bulk_imports/create_pipeline_trackers_service.rb
index f5b944e6df5..7fa62e0ce8a 100644
--- a/app/services/bulk_imports/create_pipeline_trackers_service.rb
+++ b/app/services/bulk_imports/create_pipeline_trackers_service.rb
@@ -55,6 +55,8 @@ module BulkImports
message: 'Pipeline skipped as source instance version not compatible with pipeline',
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_name: pipeline[:pipeline],
minimum_source_version: minimum_version,
maximum_source_version: maximum_version,
diff --git a/app/services/concerns/alert_management/responses.rb b/app/services/concerns/alert_management/responses.rb
index 183a831a00a..e48d07d26c0 100644
--- a/app/services/concerns/alert_management/responses.rb
+++ b/app/services/concerns/alert_management/responses.rb
@@ -7,6 +7,10 @@ module AlertManagement
ServiceResponse.success(payload: { alerts: Array(alerts) })
end
+ def created
+ ServiceResponse.success(http_status: :created)
+ end
+
def bad_request
ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
end
diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb
index 9f260345937..1e084c0e5eb 100644
--- a/app/services/projects/prometheus/alerts/notify_service.rb
+++ b/app/services/projects/prometheus/alerts/notify_service.rb
@@ -36,9 +36,9 @@ module Projects
truncate_alerts! if max_alerts_exceeded?
- alert_responses = process_prometheus_alerts
+ process_prometheus_alerts
- alert_response(alert_responses)
+ created
end
def self.processable?(payload)
@@ -152,12 +152,6 @@ module Projects
.execute
end
end
-
- def alert_response(alert_responses)
- alerts = alert_responses.flat_map { |resp| resp.payload[:alerts] }.compact
-
- success(alerts)
- end
end
end
end
diff --git a/app/views/projects/_transfer.html.haml b/app/views/projects/_transfer.html.haml
index e3aa2d8afc9..108d340ec4d 100644
--- a/app/views/projects/_transfer.html.haml
+++ b/app/views/projects/_transfer.html.haml
@@ -20,5 +20,4 @@
%li= _('You will need to update your local repositories to point to the new location.')
%li= _('Project visibility level will be changed to match namespace rules when transferring to a group.')
= hidden_field_tag(hidden_input_id)
- = label_tag :new_namespace_id, _('Select a new namespace'), class: 'gl-font-weight-bold'
.js-transfer-project-form{ data: initial_data }
diff --git a/app/views/projects/protected_tags/shared/_dropdown.html.haml b/app/views/projects/protected_tags/shared/_dropdown.html.haml
index 9c7f532fa29..9d5d649bc40 100644
--- a/app/views/projects/protected_tags/shared/_dropdown.html.haml
+++ b/app/views/projects/protected_tags/shared/_dropdown.html.haml
@@ -1,8 +1,8 @@
= f.hidden_field(:name)
-= dropdown_tag('Select tag or create wildcard',
+= dropdown_tag(s_('ProtectedBranch|Select tag or create wildcard'),
options: { toggle_class: 'js-protected-tag-select js-filter-submit wide monospace',
- filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: "Search protected tags",
+ filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: s_("ProtectedBranch|Search protected tags"),
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_tag_name],
@@ -10,6 +10,6 @@
%ul.dropdown-footer-list
%li
- %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: "New Protected Tag" }
- Create wildcard
+ %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: s_("ProtectedBranch|New Protected Tag") }
+ = s_('ProtectedBranch|Create wildcard')
%code
diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml
index 5f3ea281278..0a85a353e27 100644
--- a/app/views/projects/protected_tags/shared/_tags_list.html.haml
+++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml
@@ -1,9 +1,9 @@
.protected-tags-list.js-protected-tags-list
- if @protected_tags.empty?
.card-header
- Protected tags (0)
+ = s_('ProtectedBranch|Protected tags (%{tags_count})') % { tags_count: 0 }
%p.settings-message.text-center
- No tags are protected.
+ = s_('ProtectedBranch|No tags are protected.')
- else
- can_admin_project = can?(current_user, :admin_project, @project)
@@ -16,9 +16,12 @@
%col
%thead
%tr
- %th Protected tags (#{@protected_tags_count})
- %th Last commit
- %th Allowed to create
+ %th
+ = s_('ProtectedBranch|Protected tags (%{tags_count})') % { tags_count: @protected_tags_count }
+ %th
+ = s_('ProtectedBranch|Last commit')
+ %th
+ = s_('ProtectedBranch|Allowed to create')
- if can_admin_project
%th
%tbody
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index e6bdefc64d2..beb5b527669 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -1,5 +1,6 @@
- type = local_assigns.fetch(:type)
- is_issue = type == :issues
+- move_data = { projects_fetch_path: autocomplete_projects_path(project_id: @project.id), project_full_path: @project.full_path }
%aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? }, 'aria-label': _('Bulk update') }
.issuable-sidebar.hidden
@@ -42,6 +43,9 @@
.title
= _('Subscriptions')
.js-subscriptions-dropdown
+ - if is_issue
+ .block
+ .js-move-issues{ data: move_data }
= hidden_field_tag "update[issuable_ids]", []
= hidden_field_tag :state_event, params[:state_event]
diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb
index ada3210624c..d23d57c33ab 100644
--- a/app/workers/bulk_imports/entity_worker.rb
+++ b/app/workers/bulk_imports/entity_worker.rb
@@ -12,13 +12,18 @@ module BulkImports
worker_has_external_dependencies!
def perform(entity_id, current_stage = nil)
+ @entity = ::BulkImports::Entity.find(entity_id)
+
if stage_running?(entity_id, current_stage)
logger.info(
structured_payload(
bulk_import_entity_id: entity_id,
- bulk_import_id: bulk_import_id(entity_id),
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
current_stage: current_stage,
message: 'Stage running',
+ source_version: source_version,
importer: 'gitlab_migration'
)
)
@@ -29,9 +34,12 @@ module BulkImports
logger.info(
structured_payload(
bulk_import_entity_id: entity_id,
- bulk_import_id: bulk_import_id(entity_id),
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
current_stage: current_stage,
message: 'Stage starting',
+ source_version: source_version,
importer: 'gitlab_migration'
)
)
@@ -44,23 +52,34 @@ module BulkImports
)
end
rescue StandardError => e
- logger.error(
- structured_payload(
+ log_exception(e,
+ {
bulk_import_entity_id: entity_id,
- bulk_import_id: bulk_import_id(entity_id),
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
current_stage: current_stage,
- message: e.message,
+ message: 'Entity failed',
+ source_version: source_version,
importer: 'gitlab_migration'
- )
+ }
)
Gitlab::ErrorTracking.track_exception(
- e, bulk_import_entity_id: entity_id, bulk_import_id: bulk_import_id(entity_id), importer: 'gitlab_migration'
+ e,
+ bulk_import_entity_id: entity_id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
+ source_version: source_version,
+ importer: 'gitlab_migration'
)
end
private
+ attr_reader :entity
+
def stage_running?(entity_id, stage)
return unless stage
@@ -71,12 +90,18 @@ module BulkImports
BulkImports::Tracker.next_pipeline_trackers_for(entity_id).update(status_event: 'enqueue')
end
- def bulk_import_id(entity_id)
- @bulk_import_id ||= Entity.find(entity_id).bulk_import_id
+ def source_version
+ entity.bulk_import.source_version_info.to_s
end
def logger
@logger ||= Gitlab::Import::Logger.build
end
+
+ def log_exception(exception, payload)
+ Gitlab::ExceptionLogFormatter.format!(exception, payload)
+
+ logger.error(structured_payload(payload))
+ end
end
end
diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb
index a57071ddcf1..1a5f6250429 100644
--- a/app/workers/bulk_imports/export_request_worker.rb
+++ b/app/workers/bulk_imports/export_request_worker.rb
@@ -22,7 +22,19 @@ module BulkImports
if e.retriable?(entity)
retry_request(e, entity)
else
- log_export_failure(e, entity)
+ log_exception(e,
+ {
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
+ message: "Request to export #{entity.source_type} failed",
+ source_version: entity.bulk_import.source_version_info.to_s,
+ importer: 'gitlab_migration'
+ }
+ )
+
+ BulkImports::Failure.create(failure_attributes(e, entity))
entity.fail_op!
end
@@ -41,22 +53,7 @@ module BulkImports
)
end
- def log_export_failure(exception, entity)
- Gitlab::Import::Logger.error(
- structured_payload(
- log_attributes(exception, entity).merge(
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- message: "Request to export #{entity.source_type} failed",
- importer: 'gitlab_migration'
- )
- )
- )
-
- BulkImports::Failure.create(log_attributes(exception, entity))
- end
-
- def log_attributes(exception, entity)
+ def failure_attributes(exception, entity)
{
bulk_import_entity_id: entity.id,
pipeline_class: 'ExportRequestWorker',
@@ -84,15 +81,16 @@ module BulkImports
::GlobalID.parse(response.dig(*query.data_path, 'id')).model_id
rescue StandardError => e
- Gitlab::Import::Logger.error(
- structured_payload(
- log_attributes(e, entity).merge(
- message: 'Failed to fetch source entity id',
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- importer: 'gitlab_migration'
- )
- )
+ log_exception(e,
+ {
+ message: 'Failed to fetch source entity id',
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
+ source_version: entity.bulk_import.source_version_info.to_s,
+ importer: 'gitlab_migration'
+ }
)
nil
@@ -107,18 +105,29 @@ module BulkImports
end
def retry_request(exception, entity)
- Gitlab::Import::Logger.error(
- structured_payload(
- log_attributes(exception, entity).merge(
- message: 'Retrying export request',
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- importer: 'gitlab_migration'
- )
- )
+ log_exception(exception,
+ {
+ message: 'Retrying export request',
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
+ source_version: entity.bulk_import.source_version_info.to_s,
+ importer: 'gitlab_migration'
+ }
)
self.class.perform_in(2.seconds, entity.id)
end
+
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
+ end
+
+ def log_exception(exception, payload)
+ Gitlab::ExceptionLogFormatter.format!(exception, payload)
+
+ logger.error(structured_payload(payload))
+ end
end
end
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index 6d314774cff..5716f6e3f31 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -17,24 +17,34 @@ module BulkImports
.find_by_id(pipeline_tracker_id)
if pipeline_tracker.present?
+ @entity = @pipeline_tracker.entity
+
logger.info(
structured_payload(
- bulk_import_entity_id: pipeline_tracker.entity.id,
- bulk_import_id: pipeline_tracker.entity.bulk_import_id,
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_name: pipeline_tracker.pipeline_name,
message: 'Pipeline starting',
+ source_version: source_version,
importer: 'gitlab_migration'
)
)
run
else
+ @entity = ::BulkImports::Entity.find(entity_id)
+
logger.error(
structured_payload(
bulk_import_entity_id: entity_id,
- bulk_import_id: bulk_import_id(entity_id),
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_tracker_id: pipeline_tracker_id,
message: 'Unstarted pipeline not found',
+ source_version: source_version,
importer: 'gitlab_migration'
)
)
@@ -46,10 +56,10 @@ module BulkImports
private
- attr_reader :pipeline_tracker
+ attr_reader :pipeline_tracker, :entity
def run
- return skip_tracker if pipeline_tracker.entity.failed?
+ return skip_tracker if entity.failed?
raise(Pipeline::ExpiredError, 'Pipeline timeout') if job_timeout?
raise(Pipeline::FailedError, "Export from source instance failed: #{export_status.error}") if export_failed?
@@ -65,33 +75,39 @@ module BulkImports
fail_tracker(e)
end
- def bulk_import_id(entity_id)
- @bulk_import_id ||= Entity.find(entity_id).bulk_import_id
+ def source_version
+ entity.bulk_import.source_version_info.to_s
end
def fail_tracker(exception)
pipeline_tracker.update!(status_event: 'fail_op', jid: jid)
- logger.error(
- structured_payload(
- bulk_import_entity_id: pipeline_tracker.entity.id,
- bulk_import_id: pipeline_tracker.entity.bulk_import_id,
+ log_exception(exception,
+ {
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_name: pipeline_tracker.pipeline_name,
- message: exception.message,
+ message: 'Pipeline failed',
+ source_version: source_version,
importer: 'gitlab_migration'
- )
+ }
)
Gitlab::ErrorTracking.track_exception(
exception,
- bulk_import_entity_id: pipeline_tracker.entity.id,
- bulk_import_id: pipeline_tracker.entity.bulk_import_id,
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_name: pipeline_tracker.pipeline_name,
+ source_version: source_version,
importer: 'gitlab_migration'
)
BulkImports::Failure.create(
- bulk_import_entity_id: context.entity.id,
+ bulk_import_entity_id: entity.id,
pipeline_class: pipeline_tracker.pipeline_name,
pipeline_step: 'pipeline_worker_run',
exception_class: exception.class.to_s,
@@ -109,7 +125,7 @@ module BulkImports
delay,
pipeline_tracker.id,
pipeline_tracker.stage,
- pipeline_tracker.entity.id
+ entity.id
)
end
@@ -128,7 +144,7 @@ module BulkImports
def job_timeout?
return false unless file_extraction_pipeline?
- (Time.zone.now - pipeline_tracker.entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT
+ (Time.zone.now - entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT
end
def export_failed?
@@ -150,14 +166,17 @@ module BulkImports
end
def retry_tracker(exception)
- logger.error(
- structured_payload(
- bulk_import_entity_id: pipeline_tracker.entity.id,
- bulk_import_id: pipeline_tracker.entity.bulk_import_id,
+ log_exception(exception,
+ {
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_name: pipeline_tracker.pipeline_name,
- message: "Retrying error: #{exception.message}",
+ message: "Retrying pipeline",
+ source_version: source_version,
importer: 'gitlab_migration'
- )
+ }
)
pipeline_tracker.update!(status_event: 'retry', jid: jid)
@@ -168,15 +187,23 @@ module BulkImports
def skip_tracker
logger.info(
structured_payload(
- bulk_import_entity_id: pipeline_tracker.entity.id,
- bulk_import_id: pipeline_tracker.entity.bulk_import_id,
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_name: pipeline_tracker.pipeline_name,
message: 'Skipping pipeline due to failed entity',
+ source_version: source_version,
importer: 'gitlab_migration'
)
)
pipeline_tracker.update!(status_event: 'skip', jid: jid)
end
+
+ def log_exception(exception, payload)
+ Gitlab::ExceptionLogFormatter.format!(exception, payload)
+ logger.error(structured_payload(payload))
+ end
end
end
diff --git a/config/feature_flags/development/disable_load_entire_blob_for_diff_viewer.yml b/config/feature_flags/development/check_etags_diffs_batch_before_write_cache.yml
index 5e767e3540b..fb03ff91d0a 100644
--- a/config/feature_flags/development/disable_load_entire_blob_for_diff_viewer.yml
+++ b/config/feature_flags/development/check_etags_diffs_batch_before_write_cache.yml
@@ -1,8 +1,8 @@
---
-name: disable_load_entire_blob_for_diff_viewer
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99029
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/376330
-milestone: '15.5'
+name: check_etags_diffs_batch_before_write_cache
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101421
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/378333
+milestone: '15.6'
type: development
-group: group::source code
+group: group::code review
default_enabled: false
diff --git a/db/migrate/20221010103207_add_product_analytics_enabled_to_application_settings.rb b/db/migrate/20221010103207_add_product_analytics_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..24887e7b9fb
--- /dev/null
+++ b/db/migrate/20221010103207_add_product_analytics_enabled_to_application_settings.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddProductAnalyticsEnabledToApplicationSettings < Gitlab::Database::Migration[2.0]
+ def change
+ add_column :application_settings, :product_analytics_enabled, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema_migrations/20221010103207 b/db/schema_migrations/20221010103207
new file mode 100644
index 00000000000..24fcfc34c41
--- /dev/null
+++ b/db/schema_migrations/20221010103207
@@ -0,0 +1 @@
+04997da3ff51b8be05fd765c6534f92a15eea0a4ee4a535f1cb84c6da4e1bdd5 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index c99b4488148..4ee8368d8ad 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11491,6 +11491,7 @@ CREATE TABLE application_settings (
password_expiration_enabled boolean DEFAULT false NOT NULL,
password_expires_in_days integer DEFAULT 90 NOT NULL,
password_expires_notice_before_days integer DEFAULT 7 NOT NULL,
+ product_analytics_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index fca488f9021..1a765f0ed43 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -14210,7 +14210,7 @@ Represents the merge access level of a branch protection.
| <a id="mergerequestdefaultsquashcommitmessage"></a>`defaultSquashCommitMessage` | [`String`](#string) | Default squash commit message of the merge request. |
| <a id="mergerequestdescription"></a>`description` | [`String`](#string) | Description of the merge request (Markdown rendered as HTML for caching). |
| <a id="mergerequestdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
-| <a id="mergerequestdetailedmergestatus"></a>`detailedMergeStatus` **{warning-solid}** | [`DetailedMergeStatus`](#detailedmergestatus) | **Introduced** in 15.3. This feature is in Alpha. It can be changed or removed at any time. Detailed merge status of the merge request. |
+| <a id="mergerequestdetailedmergestatus"></a>`detailedMergeStatus` | [`DetailedMergeStatus`](#detailedmergestatus) | Detailed merge status of the merge request. |
| <a id="mergerequestdiffheadsha"></a>`diffHeadSha` | [`String`](#string) | Diff head SHA of the merge request. |
| <a id="mergerequestdiffrefs"></a>`diffRefs` | [`DiffRefs`](#diffrefs) | References of the base SHA, the head SHA, and the start SHA for this merge request. |
| <a id="mergerequestdiffstatssummary"></a>`diffStatsSummary` | [`DiffStatsSummary`](#diffstatssummary) | Summary of which files were changed in this merge request. |
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index 01e42fda2c9..1244a9510c1 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -209,7 +209,15 @@ We keep track of retried tests in the `$RETRIED_TESTS_REPORT_FILE` file saved as
See the [experiment issue](https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1148).
-### Single database testing
+### Compatibility testing
+
+By default, we run all tests with the versions that runs on GitLab.com.
+
+Other versions (usually one back-compatible version, and one forward-compatible version) should be running in nightly scheduled pipelines.
+
+Exceptions to this general guideline should be motivated and documented.
+
+#### Single database testing
By default, all tests run with [multiple databases](database/multiple_databases.md).
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 213c615326f..c77ecbd802c 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -340,6 +340,29 @@ To move an issue:
### Bulk move issues **(FREE SELF)**
+#### From the issues list
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15991) in GitLab 15.5.
+
+You can move multiple issues at the same time when you’re in a project.
+You can't move tasks or test cases.
+
+Prerequisite:
+
+- You must have at least the Reporter role for the project.
+
+To move multiple issues at the same time:
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Issues**.
+1. Select **Edit issues**. A sidebar on the right of your screen appears.
+1. Select the checkboxes next to each issue you want to move.
+1. From the right sidebar, select **Move selected**.
+1. From the dropdown list, select the destination project.
+1. Select **Move**.
+
+#### From the Rails console
+
You can move all open issues from one project to another.
Prerequisites:
diff --git a/lib/api/entities/ml/mlflow/run.rb b/lib/api/entities/ml/mlflow/run.rb
index a8e1cfe08dd..8b16c67611f 100644
--- a/lib/api/entities/ml/mlflow/run.rb
+++ b/lib/api/entities/ml/mlflow/run.rb
@@ -6,7 +6,7 @@ module API
module Mlflow
class Run < Grape::Entity
expose :run do
- expose(:info) { |candidate| RunInfo.represent(candidate) }
+ expose :itself, using: RunInfo, as: :info
expose :data do
expose :metrics, using: Metric
expose :params, using: RunParam
diff --git a/lib/api/entities/ml/mlflow/run_info.rb b/lib/api/entities/ml/mlflow/run_info.rb
index 096950e349d..d3934545ba4 100644
--- a/lib/api/entities/ml/mlflow/run_info.rb
+++ b/lib/api/entities/ml/mlflow/run_info.rb
@@ -11,7 +11,7 @@ module API
expose(:start_time) { |candidate| candidate.start_time || 0 }
expose :end_time, expose_nil: false
expose(:status) { |candidate| candidate.status.to_s.upcase }
- expose(:artifact_uri) { |candidate| 'not_implemented' }
+ expose(:artifact_uri) { |candidate, options| "#{options[:packages_url]}#{candidate.artifact_root}" }
expose(:lifecycle_stage) { |candidate| 'active' }
expose(:user_id) { |candidate| candidate.user_id.to_s }
diff --git a/lib/api/entities/ml/mlflow/update_run.rb b/lib/api/entities/ml/mlflow/update_run.rb
index 090d69b8895..55def810ef5 100644
--- a/lib/api/entities/ml/mlflow/update_run.rb
+++ b/lib/api/entities/ml/mlflow/update_run.rb
@@ -5,13 +5,7 @@ module API
module Ml
module Mlflow
class UpdateRun < Grape::Entity
- expose :run_info
-
- private
-
- def run_info
- RunInfo.represent object
- end
+ expose :itself, using: RunInfo, as: :run_info
end
end
end
diff --git a/lib/api/ml/mlflow.rb b/lib/api/ml/mlflow.rb
index 2ffb04ebcbd..f3195e5b6c5 100644
--- a/lib/api/ml/mlflow.rb
+++ b/lib/api/ml/mlflow.rb
@@ -68,6 +68,15 @@ module API
def find_candidate!(iid)
candidate_repository.by_iid(iid) || resource_not_found!
end
+
+ def packages_url
+ path = api_v4_projects_packages_generic_package_version_path(
+ id: user_project.id, package_name: '', file_name: ''
+ )
+ path = path.delete_suffix('/package_version')
+
+ "#{request.base_url}#{path}"
+ end
end
params do
@@ -143,7 +152,8 @@ module API
optional :tags, type: Array, desc: 'This will be ignored'
end
post 'create', urgency: :low do
- present candidate_repository.create!(experiment, params[:start_time]), with: Entities::Ml::Mlflow::Run
+ present candidate_repository.create!(experiment, params[:start_time]),
+ with: Entities::Ml::Mlflow::Run, packages_url: packages_url
end
desc 'Gets an MLFlow Run, which maps to GitLab Candidates' do
@@ -155,7 +165,7 @@ module API
optional :run_uuid, type: String, desc: 'This parameter is ignored'
end
get 'get', urgency: :low do
- present candidate, with: Entities::Ml::Mlflow::Run
+ present candidate, with: Entities::Ml::Mlflow::Run, packages_url: packages_url
end
desc 'Updates a Run.' do
@@ -174,7 +184,7 @@ module API
post 'update', urgency: :low do
candidate_repository.update(candidate, params[:status], params[:end_time])
- present candidate, with: Entities::Ml::Mlflow::UpdateRun
+ present candidate, with: Entities::Ml::Mlflow::UpdateRun, packages_url: packages_url
end
desc 'Logs a metric to a run.' do
diff --git a/lib/bulk_imports/common/pipelines/entity_finisher.rb b/lib/bulk_imports/common/pipelines/entity_finisher.rb
index 5066f622d57..a52504d04bc 100644
--- a/lib/bulk_imports/common/pipelines/entity_finisher.rb
+++ b/lib/bulk_imports/common/pipelines/entity_finisher.rb
@@ -24,11 +24,13 @@ module BulkImports
end
logger.info(
- bulk_import_id: context.bulk_import_id,
- bulk_import_entity_id: context.entity.id,
- bulk_import_entity_type: context.entity.source_type,
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_class: self.class.name,
message: "Entity #{entity.status_name}",
+ source_version: entity.bulk_import.source_version_info.to_s,
importer: 'gitlab_migration'
)
diff --git a/lib/bulk_imports/pipeline/runner.rb b/lib/bulk_imports/pipeline/runner.rb
index ef9575d1e96..81f8dee30d9 100644
--- a/lib/bulk_imports/pipeline/runner.rb
+++ b/lib/bulk_imports/pipeline/runner.rb
@@ -99,7 +99,7 @@ module BulkImports
end
def log_import_failure(exception, step)
- attributes = {
+ failure_attributes = {
bulk_import_entity_id: context.entity.id,
pipeline_class: pipeline,
pipeline_step: step,
@@ -108,16 +108,18 @@ module BulkImports
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
}
- error(
- bulk_import_id: context.bulk_import_id,
- pipeline_step: step,
- exception_class: exception.class.to_s,
- exception_message: exception.message,
- message: "Pipeline failed",
- importer: 'gitlab_migration'
+ log_exception(
+ exception,
+ log_params(
+ {
+ bulk_import_id: context.bulk_import_id,
+ pipeline_step: step,
+ message: 'Pipeline failed'
+ }
+ )
)
- BulkImports::Failure.create(attributes)
+ BulkImports::Failure.create(failure_attributes)
end
def info(extra = {})
@@ -128,17 +130,15 @@ module BulkImports
logger.warn(log_params(extra))
end
- def error(extra = {})
- logger.error(log_params(extra))
- end
-
def log_params(extra)
defaults = {
bulk_import_id: context.bulk_import_id,
bulk_import_entity_id: context.entity.id,
bulk_import_entity_type: context.entity.source_type,
+ source_full_path: context.entity.source_full_path,
pipeline_class: pipeline,
context_extra: context.extra,
+ source_version: context.entity.bulk_import.source_version_info.to_s,
importer: 'gitlab_migration'
}
@@ -150,6 +150,19 @@ module BulkImports
def logger
@logger ||= Gitlab::Import::Logger.build
end
+
+ def log_exception(exception, payload)
+ Gitlab::ExceptionLogFormatter.format!(exception, payload)
+ logger.error(structured_payload(payload))
+ end
+
+ def structured_payload(payload = {})
+ context = Gitlab::ApplicationContext.current.merge(
+ 'class' => self.class.name
+ )
+
+ payload.stringify_keys.merge(context)
+ end
end
end
end
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index 924de132840..ae55dae1201 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -46,7 +46,9 @@ module Gitlab
# This is either the new path, otherwise the old path for the diff_file
def diff_file_paths
- diff_files.map(&:file_path)
+ diffs.map do |diff|
+ diff.new_path.presence || diff.old_path
+ end
end
# This is both the new and old paths for the diff_file
diff --git a/lib/gitlab/kas/client.rb b/lib/gitlab/kas/client.rb
index 753a185344e..768810d5545 100644
--- a/lib/gitlab/kas/client.rb
+++ b/lib/gitlab/kas/client.rb
@@ -64,7 +64,7 @@ module Gitlab
def credentials
if URI(Gitlab::Kas.internal_url).scheme == 'grpcs'
- GRPC::Core::ChannelCredentials.new
+ GRPC::Core::ChannelCredentials.new(::Gitlab::X509::Certificate.ca_certs_bundle)
else
:this_channel_is_insecure
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 48c36dfc24b..2974061a549 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -22673,6 +22673,21 @@ msgstr ""
msgid "IssuesAnalytics|Total:"
msgstr ""
+msgid "Issues|Move selected"
+msgstr ""
+
+msgid "Issues|Tasks and test cases can not be moved."
+msgstr ""
+
+msgid "Issues|Tasks can not be moved."
+msgstr ""
+
+msgid "Issues|Test cases can not be moved."
+msgstr ""
+
+msgid "Issues|There was an error while moving the issues."
+msgstr ""
+
msgid "Issue|Title"
msgstr ""
@@ -32717,6 +32732,9 @@ msgstr ""
msgid "ProtectedBranch|Allow all users with push access to force push."
msgstr ""
+msgid "ProtectedBranch|Allowed to create"
+msgstr ""
+
msgid "ProtectedBranch|Allowed to force push"
msgstr ""
@@ -32753,15 +32771,27 @@ msgstr ""
msgid "ProtectedBranch|Code owner approval"
msgstr ""
+msgid "ProtectedBranch|Create wildcard"
+msgstr ""
+
msgid "ProtectedBranch|Does not apply to users allowed to push. Optional sections are not enforced."
msgstr ""
msgid "ProtectedBranch|Keep stable branches secure and force developers to use merge requests."
msgstr ""
+msgid "ProtectedBranch|Last commit"
+msgstr ""
+
msgid "ProtectedBranch|Learn more."
msgstr ""
+msgid "ProtectedBranch|New Protected Tag"
+msgstr ""
+
+msgid "ProtectedBranch|No tags are protected."
+msgstr ""
+
msgid "ProtectedBranch|Protect"
msgstr ""
@@ -32777,12 +32807,21 @@ msgstr ""
msgid "ProtectedBranch|Protected branches, merge request approvals, and status checks will appear here once configured."
msgstr ""
+msgid "ProtectedBranch|Protected tags (%{tags_count})"
+msgstr ""
+
msgid "ProtectedBranch|Reject code pushes that change files listed in the CODEOWNERS file."
msgstr ""
msgid "ProtectedBranch|Require approval from code owners:"
msgstr ""
+msgid "ProtectedBranch|Search protected tags"
+msgstr ""
+
+msgid "ProtectedBranch|Select tag or create wildcard"
+msgstr ""
+
msgid "ProtectedBranch|There are currently no protected branches, protect a branch with the form above."
msgstr ""
diff --git a/qa/Gemfile b/qa/Gemfile
index 56552044561..35384fbbef3 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -19,7 +19,7 @@ gem 'knapsack', '~> 4.0'
gem 'parallel_tests', '~> 3.13'
gem 'rotp', '~> 6.2.0'
gem 'timecop', '~> 0.9.5'
-gem 'parallel', '~> 1.19'
+gem 'parallel', '~> 1.22', '>= 1.22.1'
gem 'rainbow', '~> 3.1.1'
gem 'rspec-parameterized', '~> 0.5.2'
gem 'octokit', '~> 5.6.1'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index a07d960f32e..da7238fbc53 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -187,7 +187,7 @@ GEM
sawyer (~> 0.9)
oj (3.13.21)
os (1.1.4)
- parallel (1.19.2)
+ parallel (1.22.1)
parallel_tests (3.13.0)
parallel
parser (3.1.2.1)
@@ -319,7 +319,7 @@ DEPENDENCIES
knapsack (~> 4.0)
nokogiri (~> 1.13, >= 1.13.9)
octokit (~> 5.6.1)
- parallel (~> 1.19)
+ parallel (~> 1.22, >= 1.22.1)
parallel_tests (~> 3.13)
pry-byebug (~> 3.10.1)
rainbow (~> 3.1.1)
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb
index bef15b46fcd..c7cb97341d1 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb
@@ -36,6 +36,15 @@ module QA
end
end
+ let(:invalid_content) do
+ <<~YAML
+
+ job3:
+ stage: stage_foo
+ script: echo 'Done.'
+ YAML
+ end
+
before do
# Make sure a pipeline is created before visiting pipeline editor page.
# Otherwise, test might timeout before the page finishing fetching pipeline status.
@@ -80,7 +89,7 @@ module QA
invalid_msg = 'syntax is invalid'
Page::Project::PipelineEditor::Show.perform do |show|
- show.write_to_editor(SecureRandom.hex(10))
+ show.write_to_editor(invalid_content)
aggregate_failures do
show.go_to_visualize_tab
@@ -90,8 +99,10 @@ module QA
show.simulate_pipeline
expect(show.tab_alert_title).to have_content('Pipeline simulation completed with errors')
+ expect(show.ci_syntax_validate_message).to have_content('CI configuration is invalid')
+
show.go_to_view_merged_yaml_tab
- expect(show.tab_alert_message).to have_content(invalid_msg)
+ expect(show).to have_source_editor
expect(show.ci_syntax_validate_message).to have_content('CI configuration is invalid')
end
diff --git a/rubocop/cop/gitlab/json.rb b/rubocop/cop/gitlab/json.rb
index 56846e3c276..3510882fd6d 100644
--- a/rubocop/cop/gitlab/json.rb
+++ b/rubocop/cop/gitlab/json.rb
@@ -7,25 +7,43 @@ module RuboCop
extend RuboCop::Cop::AutoCorrector
MSG = <<~EOL
- Avoid calling `JSON` directly. Instead, use the `Gitlab::Json`
- wrapper. This allows us to alter the JSON parser being used.
+ Prefer `Gitlab::Json` over calling `JSON` or `to_json` directly. See https://docs.gitlab.com/ee/development/json.html
EOL
def_node_matcher :json_node?, <<~PATTERN
- (send (const {nil? | (const nil? :ActiveSupport)} :JSON)...)
+ (send (const {nil? | (const nil? :ActiveSupport)} :JSON) $_ $...)
+ PATTERN
+
+ def_node_matcher :to_json_call?, <<~PATTERN
+ (send $_ :to_json)
PATTERN
def on_send(node)
- return unless json_node?(node)
+ method_name, arg_source = match_node(node)
+ return unless method_name
add_offense(node) do |corrector|
- _, method_name, *arg_nodes = *node
-
- replacement = "Gitlab::Json.#{method_name}(#{arg_nodes.map(&:source).join(', ')})"
+ replacement = "Gitlab::Json.#{method_name}(#{arg_source})"
corrector.replace(node.source_range, replacement)
end
end
+
+ private
+
+ def match_node(node)
+ method_name, arg_nodes = json_node?(node)
+
+ # Only match if the method is implemented by Gitlab::Json
+ if method_name && ::Gitlab::Json.methods(false).include?(method_name)
+ return [method_name, arg_nodes.map(&:source).join(', ')]
+ end
+
+ receiver = to_json_call?(node)
+ return [:generate, receiver.source] if receiver
+
+ nil
+ end
end
end
end
diff --git a/scripts/lint_templates_bash.rb b/scripts/lint_templates_bash.rb
index 8db9469ecdf..cd36bb629ab 100755
--- a/scripts/lint_templates_bash.rb
+++ b/scripts/lint_templates_bash.rb
@@ -58,6 +58,12 @@ module LintTemplatesBash
def check_template(template)
parsed = process_content(template.content)
+
+ unless parsed.valid?
+ warn "#{template.full_name} is invalid: #{parsed.errors.inspect}"
+ return true
+ end
+
results = parsed.jobs.map do |name, job|
out, success = check_job(job)
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index 91c645a0ed9..ae1fe527100 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -22,11 +22,11 @@ gitlab:
gitaly:
resources:
requests:
- cpu: 2400m
- memory: 1000M
+ cpu: 1200m
+ memory: 600M
limits:
- cpu: 3600m
- memory: 1500M
+ cpu: 1800m
+ memory: 1000M
persistence:
size: 10G
storageClass: ssd
diff --git a/spec/controllers/projects/alerting/notifications_controller_spec.rb b/spec/controllers/projects/alerting/notifications_controller_spec.rb
index b3feeb7c07b..5ce2950f95f 100644
--- a/spec/controllers/projects/alerting/notifications_controller_spec.rb
+++ b/spec/controllers/projects/alerting/notifications_controller_spec.rb
@@ -16,9 +16,6 @@ RSpec.describe Projects::Alerting::NotificationsController do
end
shared_examples 'process alert payload' do |notify_service_class|
- let(:alert_1) { build(:alert_management_alert, project: project) }
- let(:alert_2) { build(:alert_management_alert, project: project) }
- let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) }
let(:notify_service) { instance_double(notify_service_class, execute: service_response) }
before do
@@ -35,11 +32,14 @@ RSpec.describe Projects::Alerting::NotificationsController do
it 'responds with the alert data' do
make_request
- expect(json_response).to contain_exactly(
- { 'iid' => alert_1.iid, 'title' => alert_1.title },
- { 'iid' => alert_2.iid, 'title' => alert_2.title }
- )
- expect(response).to have_gitlab_http_status(:ok)
+ if service_response.payload.present?
+ expect(json_response).to contain_exactly(
+ { 'iid' => alert_1.iid, 'title' => alert_1.title },
+ { 'iid' => alert_2.iid, 'title' => alert_2.title }
+ )
+ end
+
+ expect(response).to have_gitlab_http_status(service_response.http_status)
end
it 'does not pass excluded parameters to the notify service' do
@@ -146,6 +146,9 @@ RSpec.describe Projects::Alerting::NotificationsController do
context 'with generic alert payload' do
it_behaves_like 'process alert payload', Projects::Alerting::NotifyService do
+ let(:alert_1) { build(:alert_management_alert, project: project) }
+ let(:alert_2) { build(:alert_management_alert, project: project) }
+ let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) }
let(:payload) { { title: 'Alert title' } }
end
end
@@ -154,6 +157,7 @@ RSpec.describe Projects::Alerting::NotificationsController do
include PrometheusHelpers
it_behaves_like 'process alert payload', Projects::Prometheus::Alerts::NotifyService do
+ let(:service_response) { ServiceResponse.success(http_status: :created) }
let(:payload) { prometheus_alert_payload }
end
end
diff --git a/spec/controllers/projects/prometheus/alerts_controller_spec.rb b/spec/controllers/projects/prometheus/alerts_controller_spec.rb
index 2c2c8180143..09b9f25c0c6 100644
--- a/spec/controllers/projects/prometheus/alerts_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/alerts_controller_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Projects::Prometheus::AlertsController do
describe 'POST #notify' do
let(:alert_1) { build(:alert_management_alert, :prometheus, project: project) }
let(:alert_2) { build(:alert_management_alert, :prometheus, project: project) }
- let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) }
+ let(:service_response) { ServiceResponse.success(http_status: :created) }
let(:notify_service) { instance_double(Projects::Prometheus::Alerts::NotifyService, execute: service_response) }
before do
@@ -68,17 +68,12 @@ RSpec.describe Projects::Prometheus::AlertsController do
.and_return(notify_service)
end
- it 'returns ok if notification succeeds' do
+ it 'returns created if notification succeeds' do
expect(notify_service).to receive(:execute).and_return(service_response)
post :notify, params: project_params, session: { as: :json }
- expect(json_response).to contain_exactly(
- { 'iid' => alert_1.iid, 'title' => alert_1.title },
- { 'iid' => alert_2.iid, 'title' => alert_2.title }
- )
-
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:created)
end
it 'returns unprocessable entity if notification fails' do
diff --git a/spec/factories/experiment_users.rb b/spec/factories/experiment_users.rb
deleted file mode 100644
index 66c39d684eb..00000000000
--- a/spec/factories/experiment_users.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :experiment_user do
- experiment
- user
- group_type { :control }
- converted_at { nil }
- end
-end
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
index 47b28b88108..a2dea7f048b 100644
--- a/spec/features/issues/user_interacts_with_awards_spec.rb
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -209,22 +209,25 @@ RSpec.describe 'User interacts with awards' do
it 'adds award to issue' do
first('[data-testid="award-button"]').click
-
+ wait_for_requests
expect(page).to have_selector('[data-testid="award-button"].selected')
expect(first('[data-testid="award-button"]')).to have_content '1'
visit project_issue_path(project, issue)
+ wait_for_requests
expect(first('[data-testid="award-button"]')).to have_content '1'
end
it 'removes award from issue', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375241' do
first('[data-testid="award-button"]').click
+ wait_for_requests
find('[data-testid="award-button"].selected').click
-
+ wait_for_requests
expect(first('[data-testid="award-button"]')).to have_content '0'
visit project_issue_path(project, issue)
+ wait_for_requests
expect(first('[data-testid="award-button"]')).to have_content '0'
end
diff --git a/spec/fixtures/api/schemas/ml/run.json b/spec/fixtures/api/schemas/ml/run.json
index 2418f44b21f..48d0ed25ce4 100644
--- a/spec/fixtures/api/schemas/ml/run.json
+++ b/spec/fixtures/api/schemas/ml/run.json
@@ -27,15 +27,43 @@
"end_time"
],
"properties": {
- "run_id": { "type": "string" },
- "run_uuid": { "type": "string" },
- "experiment_id": { "type": "string" },
- "artifact_location": { "type": "string" },
- "start_time": { "type": "integer" },
- "end_time": { "type": "integer" },
+ "run_id": {
+ "type": "string"
+ },
+ "run_uuid": {
+ "type": "string"
+ },
+ "experiment_id": {
+ "type": "string"
+ },
+ "artifact_uri": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "integer"
+ },
+ "end_time": {
+ "type": "integer"
+ },
"user_id": "",
- "status": { "type": { "enum" : ["RUNNING", "SCHEDULED", "FINISHED", "FAILED", "KILLED"] } },
- "lifecycle_stage": { "type": { "enum" : ["active"] } }
+ "status": {
+ "type": {
+ "enum": [
+ "RUNNING",
+ "SCHEDULED",
+ "FINISHED",
+ "FAILED",
+ "KILLED"
+ ]
+ }
+ },
+ "lifecycle_stage": {
+ "type": {
+ "enum": [
+ "active"
+ ]
+ }
+ }
}
},
"data": {
@@ -44,4 +72,4 @@
}
}
}
-}
+} \ No newline at end of file
diff --git a/spec/frontend/groups_projects/components/transfer_locations_spec.js b/spec/frontend/groups_projects/components/transfer_locations_spec.js
new file mode 100644
index 00000000000..c3308caf4b0
--- /dev/null
+++ b/spec/frontend/groups_projects/components/transfer_locations_spec.js
@@ -0,0 +1,283 @@
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlAlert,
+ GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json';
+import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json';
+import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
+import { getTransferLocations } from '~/api/projects_api';
+import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
+
+jest.mock('~/api/projects_api', () => ({
+ getTransferLocations: jest.fn(),
+}));
+
+describe('TransferLocations', () => {
+ let wrapper;
+
+ // Default data
+ const resourceId = '1';
+ const defaultPropsData = {
+ groupTransferLocationsApiMethod: getTransferLocations,
+ value: null,
+ };
+
+ // Mock requests
+ const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse);
+ const mockResolvedGetTransferLocations = ({
+ data = transferLocationsResponsePage1,
+ page = '1',
+ nextPage = '2',
+ total = '4',
+ totalPages = '2',
+ prevPage = null,
+ } = {}) => {
+ getTransferLocations.mockResolvedValueOnce({
+ data,
+ headers: {
+ 'x-per-page': '2',
+ 'x-page': page,
+ 'x-total': total,
+ 'x-total-pages': totalPages,
+ 'x-next-page': nextPage,
+ 'x-prev-page': prevPage,
+ },
+ });
+ };
+ const mockRejectedGetTransferLocations = () => {
+ const error = new Error();
+
+ getTransferLocations.mockRejectedValueOnce(error);
+ };
+
+ // VTU wrapper helpers
+ Vue.use(VueApollo);
+ const createComponent = ({
+ propsData = {},
+ requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]],
+ } = {}) => {
+ wrapper = mountExtended(TransferLocations, {
+ provide: {
+ resourceId,
+ },
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ apolloProvider: createMockApollo(requestHandlers),
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const showDropdown = async () => {
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+ };
+ const findUserTransferLocations = () =>
+ wrapper
+ .findByTestId('user-transfer-locations')
+ .findAllComponents(GlDropdownItem)
+ .wrappers.map((dropdownItem) => dropdownItem.text());
+ const findGroupTransferLocations = () =>
+ wrapper
+ .findByTestId('group-transfer-locations')
+ .findAllComponents(GlDropdownItem)
+ .wrappers.map((dropdownItem) => dropdownItem.text());
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
+ const searchEmitInput = () => findSearch().vm.$emit('input', 'foo');
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const intersectionObserverEmitAppear = () => findIntersectionObserver().vm.$emit('appear');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when `GlDropdown` is opened', () => {
+ it('fetches and renders user and group transfer locations', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+
+ const { namespace } = currentUserNamespaceQueryResponse.data.currentUser;
+
+ expect(findUserTransferLocations()).toEqual([namespace.fullName]);
+ expect(findGroupTransferLocations()).toEqual(
+ transferLocationsResponsePage1.map((transferLocation) => transferLocation.full_name),
+ );
+ });
+
+ describe('when transfer locations have already been fetched', () => {
+ beforeEach(async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+ });
+
+ it('does not fetch transfer locations', async () => {
+ getTransferLocations.mockClear();
+ defaultQueryHandler.mockClear();
+
+ await showDropdown();
+
+ expect(getTransferLocations).not.toHaveBeenCalled();
+ expect(defaultQueryHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when `getTransferLocations` API call fails', () => {
+ it('displays dismissible error alert', async () => {
+ mockRejectedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+
+ alert.vm.$emit('dismiss');
+ await nextTick();
+
+ expect(alert.exists()).toBe(false);
+ });
+ });
+
+ describe('when `currentUser` GraphQL query fails', () => {
+ it('displays error alert', async () => {
+ mockResolvedGetTransferLocations();
+ const error = new Error();
+ createComponent({
+ requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]],
+ });
+ await showDropdown();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when transfer location is selected', () => {
+ it('displays transfer location as selected', () => {
+ const [{ id, full_name: humanName }] = transferLocationsResponsePage1;
+
+ createComponent({
+ propsData: {
+ value: {
+ id,
+ humanName,
+ },
+ },
+ });
+
+ expect(findDropdown().props('text')).toBe(humanName);
+ });
+ });
+
+ describe('when search is typed in', () => {
+ const transferLocationsResponseSearch = [transferLocationsResponsePage1[0]];
+
+ const arrange = async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+ mockResolvedGetTransferLocations({ data: transferLocationsResponseSearch });
+ searchEmitInput();
+ await nextTick();
+ };
+
+ it('sets `isSearchLoading` prop to `true`', async () => {
+ await arrange();
+
+ expect(findSearch().props('isLoading')).toBe(true);
+ });
+
+ it('passes `search` param to API call and updates group transfer locations', async () => {
+ await arrange();
+
+ await waitForPromises();
+
+ expect(getTransferLocations).toHaveBeenCalledWith(
+ resourceId,
+ expect.objectContaining({ search: 'foo' }),
+ );
+ expect(findGroupTransferLocations()).toEqual(
+ transferLocationsResponseSearch.map((transferLocation) => transferLocation.full_name),
+ );
+ });
+ });
+
+ describe('when there are no more pages', () => {
+ it('does not show intersection observer', async () => {
+ mockResolvedGetTransferLocations({
+ data: transferLocationsResponsePage1,
+ nextPage: null,
+ total: '2',
+ totalPages: '1',
+ prevPage: null,
+ });
+ createComponent();
+ await showDropdown();
+
+ expect(findIntersectionObserver().exists()).toBe(false);
+ });
+ });
+
+ describe('when intersection observer appears', () => {
+ const arrange = async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+
+ mockResolvedGetTransferLocations({
+ data: transferLocationsResponsePage2,
+ page: '2',
+ nextPage: null,
+ prevPage: '1',
+ totalPages: '2',
+ });
+
+ intersectionObserverEmitAppear();
+ await nextTick();
+ };
+
+ it('shows loading icon', async () => {
+ await arrange();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('passes `page` param to API call', async () => {
+ await arrange();
+
+ await waitForPromises();
+
+ expect(getTransferLocations).toHaveBeenCalledWith(
+ resourceId,
+ expect.objectContaining({ page: 2 }),
+ );
+ });
+
+ it('updates dropdown with new group transfer locations', async () => {
+ await arrange();
+
+ await waitForPromises();
+
+ expect(findGroupTransferLocations()).toEqual(
+ [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map(
+ ({ full_name: fullName }) => fullName,
+ ),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js b/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js
new file mode 100644
index 00000000000..c432d722637
--- /dev/null
+++ b/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js
@@ -0,0 +1,554 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import { cloneDeep } from 'lodash';
+import VueApollo from 'vue-apollo';
+import { GlAlert } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import createFlash from '~/flash';
+import { logError } from '~/lib/logger';
+import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
+import MoveIssuesButton from '~/issuable/bulk_update_sidebar/components/move_issues_button.vue';
+import issuableEventHub from '~/issues/list/eventhub';
+import moveIssueMutation from '~/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
+import { getIssuesCountsQueryResponse, getIssuesQueryResponse } from 'jest/issues/list/mock_data';
+import {
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+} from '~/work_items/constants';
+
+jest.mock('~/flash');
+jest.mock('~/lib/logger');
+useMockLocationHelper();
+
+const mockDefaultProps = {
+ projectFullPath: 'flight/FlightJS',
+ projectsFetchPath: '/-/autocomplete/projects?project_id=1',
+};
+
+const mockDestinationProject = {
+ full_path: 'gitlab-org/GitLabTest',
+};
+
+const mockMutationErrorMessage = 'Example error message';
+
+const mockIssue = {
+ iid: '15',
+ type: WORK_ITEM_TYPE_ENUM_ISSUE,
+};
+
+const mockIncident = {
+ iid: '32',
+ type: WORK_ITEM_TYPE_ENUM_INCIDENT,
+};
+
+const mockTask = {
+ iid: '40',
+ type: WORK_ITEM_TYPE_ENUM_TASK,
+};
+
+const mockTestCase = {
+ iid: '51',
+ type: WORK_ITEM_TYPE_ENUM_TEST_CASE,
+};
+
+const selectedIssuesMocks = {
+ tasksOnly: [mockTask],
+ testCasesOnly: [mockTestCase],
+ issuesOnly: [mockIssue, mockIncident],
+ tasksAndTestCases: [mockTask, mockTestCase],
+ issuesAndTasks: [mockIssue, mockIncident, mockTask],
+ issuesAndTestCases: [mockIssue, mockIncident, mockTestCase],
+ issuesTasksAndTestCases: [mockIssue, mockIncident, mockTask, mockTestCase],
+};
+
+let getIssuesQueryCompleteResponse = getIssuesQueryResponse;
+if (IS_EE) {
+ getIssuesQueryCompleteResponse = cloneDeep(getIssuesQueryResponse);
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].blockingCount = 1;
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].healthStatus = null;
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].weight = 5;
+}
+
+const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ errors: [],
+ },
+ },
+});
+
+const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ errors: [{ message: mockMutationErrorMessage }],
+ },
+ },
+});
+
+const rejectedMutationMock = jest.fn().mockRejectedValue({});
+
+const mockIssuesQueryResponse = jest.fn().mockResolvedValue(getIssuesQueryCompleteResponse);
+const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse);
+
+describe('MoveIssuesButton', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ let fakeApollo;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDropdown = () => wrapper.findComponent(IssuableMoveDropdown);
+ const emitMoveIssuablesEvent = () => {
+ findDropdown().vm.$emit('move-issuable', mockDestinationProject);
+ };
+
+ const createComponent = (data = {}, mutationResolverMock = rejectedMutationMock) => {
+ fakeApollo = createMockApollo([
+ [moveIssueMutation, mutationResolverMock],
+ [getIssuesQuery, mockIssuesQueryResponse],
+ [getIssuesCountsQuery, mockIssuesCountsQueryResponse],
+ ]);
+
+ fakeApollo.defaultClient.cache.writeQuery({
+ query: getIssuesQuery,
+ variables: {
+ isProject: true,
+ fullPath: mockDefaultProps.projectFullPath,
+ },
+ data: getIssuesQueryCompleteResponse.data,
+ });
+
+ fakeApollo.defaultClient.cache.writeQuery({
+ query: getIssuesCountsQuery,
+ variables: {
+ isProject: true,
+ },
+ data: getIssuesCountsQueryResponse.data,
+ });
+
+ wrapper = shallowMount(MoveIssuesButton, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ propsData: {
+ ...mockDefaultProps,
+ },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ beforeEach(() => {
+ // Needed due to a bug in Apollo: https://github.com/apollographql/apollo-client/issues/8900
+ // eslint-disable-next-line no-console
+ console.warn = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ describe('`Move selected` dropdown', () => {
+ it('renders disabled by default', () => {
+ createComponent();
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDropdown().attributes('disabled')).toBe('true');
+ });
+
+ it.each`
+ selectedIssuablesMock | disabled | status | testMessage
+ ${[]} | ${true} | ${'disabled'} | ${'nothing is selected'}
+ ${selectedIssuesMocks.tasksOnly} | ${true} | ${'disabled'} | ${'only tasks are selected'}
+ ${selectedIssuesMocks.testCasesOnly} | ${true} | ${'disabled'} | ${'only test cases are selected'}
+ ${selectedIssuesMocks.issuesOnly} | ${false} | ${'enabled'} | ${'only issues are selected'}
+ ${selectedIssuesMocks.tasksAndTestCases} | ${true} | ${'disabled'} | ${'tasks and test cases are selected'}
+ ${selectedIssuesMocks.issuesAndTasks} | ${false} | ${'enabled'} | ${'issues and tasks are selected'}
+ ${selectedIssuesMocks.issuesAndTestCases} | ${false} | ${'enabled'} | ${'issues and test cases are selected'}
+ ${selectedIssuesMocks.issuesTasksAndTestCases} | ${false} | ${'enabled'} | ${'issues and tasks and test cases are selected'}
+ `('renders $status if $testMessage', async ({ selectedIssuablesMock, disabled }) => {
+ createComponent({ selectedIssuables: selectedIssuablesMock });
+
+ await nextTick();
+
+ if (disabled) {
+ expect(findDropdown().attributes('disabled')).toBe('true');
+ } else {
+ expect(findDropdown().attributes('disabled')).toBeUndefined();
+ }
+ });
+ });
+
+ describe('warning message', () => {
+ it.each`
+ selectedIssuablesMock | warningExists | visibility | message | testMessage
+ ${[]} | ${false} | ${'not visible'} | ${'empty'} | ${'nothing is selected'}
+ ${selectedIssuesMocks.tasksOnly} | ${true} | ${'visible'} | ${'Tasks can not be moved.'} | ${'only tasks are selected'}
+ ${selectedIssuesMocks.testCasesOnly} | ${true} | ${'visible'} | ${'Test cases can not be moved.'} | ${'only test cases are selected'}
+ ${selectedIssuesMocks.issuesOnly} | ${false} | ${'not visible'} | ${'empty'} | ${'only issues are selected'}
+ ${selectedIssuesMocks.tasksAndTestCases} | ${true} | ${'visible'} | ${'Tasks and test cases can not be moved.'} | ${'tasks and test cases are selected'}
+ ${selectedIssuesMocks.issuesAndTasks} | ${true} | ${'visible'} | ${'Tasks can not be moved.'} | ${'issues and tasks are selected'}
+ ${selectedIssuesMocks.issuesAndTestCases} | ${true} | ${'visible'} | ${'Test cases can not be moved.'} | ${'issues and test cases are selected'}
+ ${selectedIssuesMocks.issuesTasksAndTestCases} | ${true} | ${'visible'} | ${'Tasks and test cases can not be moved.'} | ${'issues and tasks and test cases are selected'}
+ `(
+ 'is $visibility with `$message` message if $testMessage',
+ async ({ selectedIssuablesMock, warningExists, message }) => {
+ createComponent({ selectedIssuables: selectedIssuablesMock });
+
+ await nextTick();
+
+ const alert = findAlert();
+ expect(alert.exists()).toBe(warningExists);
+
+ if (warningExists) {
+ expect(alert.text()).toBe(message);
+ expect(alert.attributes('variant')).toBe('warning');
+ }
+ },
+ );
+ });
+
+ describe('moveIssues method', () => {
+ describe('changes the `Move selected` dropdown loading state', () => {
+ it('keeps loading state to false when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to true when issues are moving', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(true);
+ });
+
+ it('sets loading state to false when all mutations succeed', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to false when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to false when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+ });
+
+ describe('handles events', () => {
+ beforeEach(() => {
+ jest.spyOn(issuableEventHub, '$emit');
+ });
+
+ it('does not emit any event when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('emits `issuables:bulkMoveStarted` when issues are moving', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveStarted');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when all mutations succeed', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+ });
+
+ describe('shows errors', () => {
+ it('does not create flashes or logs errors when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when issues are moved without errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('creates a flash and logs errors when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ // We're mocking two issues so it will log two errors
+ expect(logError).toHaveBeenCalledTimes(2);
+ expect(logError).toHaveBeenNthCalledWith(
+ 1,
+ `Error moving issue. Error message: ${mockMutationErrorMessage}`,
+ );
+ expect(logError).toHaveBeenNthCalledWith(
+ 2,
+ `Error moving issue. Error message: ${mockMutationErrorMessage}`,
+ );
+
+ // Only one flash is created even if multiple errors are reported
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error while moving the issues.',
+ });
+ });
+
+ it('creates a flash but not logs errors when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error while moving the issues.',
+ });
+ });
+ });
+
+ describe('calls mutations', () => {
+ it('does not call any mutation when no issue is selected', async () => {
+ createComponent({}, resolvedMutationWithoutErrorsMock);
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only tasks are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.tasksOnly },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only test cases are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.testCasesOnly },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only tasks and test cases are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.tasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('calls a mutation for every selected issue skipping tasks', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ // We mock three elements but only two are valid issues since the task is skipped
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenCalledTimes(2);
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenNthCalledWith(1, {
+ moveIssueInput: {
+ projectPath: mockDefaultProps.projectFullPath,
+ iid: selectedIssuesMocks.issuesTasksAndTestCases[0].iid.toString(),
+ targetProjectPath: mockDestinationProject.full_path,
+ },
+ });
+
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenNthCalledWith(2, {
+ moveIssueInput: {
+ projectPath: mockDefaultProps.projectFullPath,
+ iid: selectedIssuesMocks.issuesTasksAndTestCases[1].iid.toString(),
+ targetProjectPath: mockDestinationProject.full_path,
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 3b79739630d..27707f8b01a 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -253,7 +253,7 @@ describe('Pipeline editor tabs component', () => {
appStatus | editor | viz | validate | merged
${undefined} | ${true} | ${true} | ${true} | ${true}
${EDITOR_APP_STATUS_EMPTY} | ${true} | ${false} | ${true} | ${false}
- ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${false}
+ ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${true}
${EDITOR_APP_STATUS_VALID} | ${true} | ${true} | ${true} | ${true}
`(
'when status is $appStatus, we show - editor:$editor | viz:$viz | validate:$validate | merged:$merged',
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
index bf4026b65db..213ace4a7d6 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
@@ -12,7 +12,7 @@ import {
import Protection from '~/projects/settings/branch_rules/components/view/protection.vue';
import branchRulesQuery from '~/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
import { sprintf } from '~/locale';
-import { branchProtectionsMockResponse } from './mock_data';
+import { branchProtectionsMockResponse, approvalRulesMock } from './mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockReturnValue('main'),
@@ -105,9 +105,10 @@ describe('View branch rules', () => {
expect(findApprovalsTitle().exists()).toBe(true);
expect(findBranchProtections().at(2).props()).toMatchObject({
- header: sprintf(I18N.approvalsHeader, { total: 0 }),
+ header: sprintf(I18N.approvalsHeader, { total: 3 }),
headerLinkHref: approvalRulesPath,
headerLinkTitle: I18N.manageApprovalsLinkTitle,
+ approvals: approvalRulesMock,
});
});
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
index c3f573061da..81693b5fa46 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
@@ -1,29 +1,34 @@
const usersMock = [
{
+ id: '123',
username: 'usr1',
webUrl: 'http://test.test/usr1',
name: 'User 1',
avatarUrl: 'http://test.test/avt1.png',
},
{
+ id: '456',
username: 'usr2',
webUrl: 'http://test.test/usr2',
name: 'User 2',
avatarUrl: 'http://test.test/avt2.png',
},
{
+ id: '789',
username: 'usr3',
webUrl: 'http://test.test/usr3',
name: 'User 3',
avatarUrl: 'http://test.test/avt3.png',
},
{
+ id: '987',
username: 'usr4',
webUrl: 'http://test.test/usr4',
name: 'User 4',
avatarUrl: 'http://test.test/avt4.png',
},
{
+ id: '654',
username: 'usr5',
webUrl: 'http://test.test/usr5',
name: 'User 5',
@@ -40,6 +45,17 @@ const approvalsRequired = 3;
const groupsMock = [{ name: 'test_group_1' }, { name: 'test_group_2' }];
+export const approvalRulesMock = [
+ {
+ __typename: 'ApprovalProjectRule',
+ id: '123',
+ name: 'test',
+ type: 'REGULAR',
+ eligibleApprovers: { nodes: usersMock },
+ approvalsRequired,
+ },
+];
+
export const protectionPropsMock = {
header: 'Test protection',
headerLinkTitle: 'Test link title',
@@ -47,13 +63,7 @@ export const protectionPropsMock = {
roles: accessLevelsMock,
users: usersMock,
groups: groupsMock,
- approvals: [
- {
- name: 'test',
- eligibleApprovers: { nodes: usersMock },
- approvalsRequired,
- },
- ],
+ approvals: approvalRulesMock,
};
export const protectionRowPropsMock = {
@@ -116,6 +126,10 @@ export const branchProtectionsMockResponse = {
edges: accessLevelsMockResponse,
},
},
+ approvalRules: {
+ __typename: 'ApprovalProjectRuleConnection',
+ nodes: approvalRulesMock,
+ },
},
{
__typename: 'BranchRule',
@@ -133,6 +147,10 @@ export const branchProtectionsMockResponse = {
edges: [],
},
},
+ approvalRules: {
+ __typename: 'ApprovalProjectRuleConnection',
+ nodes: [],
+ },
},
],
},
diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
index 6e639f895a8..e091f3e25c3 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -1,18 +1,9 @@
-import Vue, { nextTick } from 'vue';
-import { GlAlert } from '@gitlab/ui';
-import VueApollo from 'vue-apollo';
-import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json';
import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json';
-import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
import { getTransferLocations } from '~/api/projects_api';
-import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/api/projects_api', () => ({
getTransferLocations: jest.fn(),
@@ -21,68 +12,36 @@ jest.mock('~/api/projects_api', () => ({
describe('Transfer project form', () => {
let wrapper;
- const projectId = '1';
+ const resourceId = '1';
const confirmButtonText = 'Confirm';
const confirmationPhrase = 'You must construct additional pylons!';
- Vue.use(VueApollo);
-
- const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse);
- const mockResolvedGetTransferLocations = ({
- data = transferLocationsResponsePage1,
- page = '1',
- nextPage = '2',
- prevPage = null,
- } = {}) => {
- getTransferLocations.mockResolvedValueOnce({
- data,
- headers: {
- 'x-per-page': '2',
- 'x-page': page,
- 'x-total': '4',
- 'x-total-pages': '2',
- 'x-next-page': nextPage,
- 'x-prev-page': prevPage,
- },
- });
- };
- const mockRejectedGetTransferLocations = () => {
- const error = new Error();
-
- getTransferLocations.mockRejectedValueOnce(error);
- };
-
- const createComponent = ({
- requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]],
- } = {}) => {
+ const createComponent = () => {
wrapper = shallowMountExtended(TransferProjectForm, {
provide: {
- projectId,
+ resourceId,
},
propsData: {
confirmButtonText,
confirmationPhrase,
},
- apolloProvider: createMockApollo(requestHandlers),
});
};
- const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
- const showNamespaceSelect = async () => {
- findNamespaceSelect().vm.$emit('show');
- await waitForPromises();
- };
+ const findTransferLocations = () => wrapper.findComponent(TransferLocations);
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
- const findAlert = () => wrapper.findComponent(GlAlert);
afterEach(() => {
wrapper.destroy();
});
- it('renders the namespace selector', () => {
+ it('renders the namespace selector and passes `groupTransferLocationsApiMethod` prop', () => {
createComponent();
- expect(findNamespaceSelect().exists()).toBe(true);
+ expect(findTransferLocations().exists()).toBe(true);
+
+ findTransferLocations().props('groupTransferLocationsApiMethod')();
+ expect(getTransferLocations).toHaveBeenCalled();
});
it('renders the confirm button', () => {
@@ -100,220 +59,29 @@ describe('Transfer project form', () => {
describe('with a selected namespace', () => {
const [selectedItem] = transferLocationsResponsePage1;
- const arrange = async () => {
- mockResolvedGetTransferLocations();
+ beforeEach(() => {
createComponent();
- await showNamespaceSelect();
- findNamespaceSelect().vm.$emit('select', selectedItem);
- };
+ findTransferLocations().vm.$emit('input', selectedItem);
+ });
- it('emits the `selectNamespace` event when a namespace is selected', async () => {
- await arrange();
+ it('sets `value` prop on `TransferLocations` component', () => {
+ expect(findTransferLocations().props('value')).toEqual(selectedItem);
+ });
+ it('emits the `selectTransferLocation` event when a namespace is selected', async () => {
const args = [selectedItem.id];
- expect(wrapper.emitted('selectNamespace')).toEqual([args]);
+ expect(wrapper.emitted('selectTransferLocation')).toEqual([args]);
});
it('enables the confirm button', async () => {
- await arrange();
-
expect(findConfirmDanger().attributes('disabled')).toBeUndefined();
});
it('clicking the confirm button emits the `confirm` event', async () => {
- await arrange();
-
findConfirmDanger().vm.$emit('confirm');
expect(wrapper.emitted('confirm')).toBeDefined();
});
});
-
- describe('when `NamespaceSelect` is opened', () => {
- it('fetches user and group namespaces and passes correct props to `NamespaceSelect` component', async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
-
- const { namespace } = currentUserNamespaceQueryResponse.data.currentUser;
-
- expect(findNamespaceSelect().props()).toMatchObject({
- userNamespaces: [
- {
- id: getIdFromGraphQLId(namespace.id),
- humanName: namespace.fullName,
- },
- ],
- groupNamespaces: transferLocationsResponsePage1.map(({ id, full_name: humanName }) => ({
- id,
- humanName,
- })),
- hasNextPageOfGroups: true,
- isLoading: false,
- isSearchLoading: false,
- shouldFilterNamespaces: false,
- });
- });
-
- describe('when namespaces have already been fetched', () => {
- beforeEach(async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- });
-
- it('does not fetch namespaces', async () => {
- getTransferLocations.mockClear();
- defaultQueryHandler.mockClear();
-
- await showNamespaceSelect();
-
- expect(getTransferLocations).not.toHaveBeenCalled();
- expect(defaultQueryHandler).not.toHaveBeenCalled();
- });
- });
-
- describe('when `getTransferLocations` API call fails', () => {
- it('displays error alert', async () => {
- mockRejectedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
-
- expect(findAlert().exists()).toBe(true);
- });
- });
-
- describe('when `currentUser` GraphQL query fails', () => {
- it('displays error alert', async () => {
- mockResolvedGetTransferLocations();
- const error = new Error();
- createComponent({
- requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]],
- });
- await showNamespaceSelect();
-
- expect(findAlert().exists()).toBe(true);
- });
- });
- });
-
- describe('when `search` event is fired', () => {
- const arrange = async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- mockResolvedGetTransferLocations();
- findNamespaceSelect().vm.$emit('search', 'foo');
- await nextTick();
- };
-
- it('sets `isSearchLoading` prop to `true`', async () => {
- await arrange();
-
- expect(findNamespaceSelect().props('isSearchLoading')).toBe(true);
- });
-
- it('passes `search` param to API call', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(getTransferLocations).toHaveBeenCalledWith(
- projectId,
- expect.objectContaining({ search: 'foo' }),
- );
- });
-
- describe('when `getTransferLocations` API call fails', () => {
- it('displays dismissible error alert', async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- mockRejectedGetTransferLocations();
- findNamespaceSelect().vm.$emit('search', 'foo');
- await waitForPromises();
-
- const alert = findAlert();
-
- expect(alert.exists()).toBe(true);
-
- alert.vm.$emit('dismiss');
- await nextTick();
-
- expect(alert.exists()).toBe(false);
- });
- });
- });
-
- describe('when `load-more-groups` event is fired', () => {
- const arrange = async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
-
- mockResolvedGetTransferLocations({
- data: transferLocationsResponsePage2,
- page: '2',
- nextPage: null,
- prevPage: '1',
- });
-
- findNamespaceSelect().vm.$emit('load-more-groups');
- await nextTick();
- };
-
- it('sets `isLoading` prop to `true`', async () => {
- await arrange();
-
- expect(findNamespaceSelect().props('isLoading')).toBe(true);
- });
-
- it('passes `page` param to API call', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(getTransferLocations).toHaveBeenCalledWith(
- projectId,
- expect.objectContaining({ page: 2 }),
- );
- });
-
- it('updates `groupNamespaces` prop with new groups', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(findNamespaceSelect().props('groupNamespaces')).toMatchObject(
- [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map(
- ({ id, full_name: humanName }) => ({
- id,
- humanName,
- }),
- ),
- );
- });
-
- it('updates `hasNextPageOfGroups` prop', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(findNamespaceSelect().props('hasNextPageOfGroups')).toBe(false);
- });
-
- describe('when `getTransferLocations` API call fails', () => {
- it('displays error alert', async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- mockRejectedGetTransferLocations();
- findNamespaceSelect().vm.$emit('load-more-groups');
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- });
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
index 9b1316677d7..d531147c0e6 100644
--- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
@@ -37,6 +37,7 @@ const mockProps = {
dropdownButtonTitle: 'Move issuable',
dropdownHeaderTitle: 'Move issuable',
moveInProgress: false,
+ disabled: false,
};
const mockEvent = {
@@ -44,20 +45,21 @@ const mockEvent = {
preventDefault: jest.fn(),
};
-const createComponent = (propsData = mockProps) =>
- shallowMount(IssuableMoveDropdown, {
- propsData,
- });
-
describe('IssuableMoveDropdown', () => {
let mock;
let wrapper;
- beforeEach(() => {
- mock = new MockAdapter(axios);
- wrapper = createComponent();
+ const createComponent = (propsData = mockProps) => {
+ wrapper = shallowMount(IssuableMoveDropdown, {
+ propsData,
+ });
wrapper.vm.$refs.dropdown.hide = jest.fn();
wrapper.vm.$refs.searchInput.focusInput = jest.fn();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent();
});
afterEach(() => {
@@ -194,6 +196,12 @@ describe('IssuableMoveDropdown', () => {
expect(findDropdownEl().findComponent(GlDropdownForm).exists()).toBe(true);
});
+ it('renders disabled dropdown when `disabled` is true', () => {
+ createComponent({ ...mockProps, disabled: true });
+
+ expect(findDropdownEl().attributes('disabled')).toBe('true');
+ });
+
it('renders header element', () => {
const headerEl = findDropdownEl().find('[data-testid="header"]');
diff --git a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
index 2a6d0825e5c..d5a37f53e21 100644
--- a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
+++ b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe API::Entities::Ml::Mlflow::RunInfo do
let_it_be(:candidate) { create(:ml_candidates) }
- subject { described_class.new(candidate).as_json }
+ subject { described_class.new(candidate, packages_url: 'http://example.com').as_json }
context 'when start_time is nil' do
it { expect(subject[:start_time]).to eq(0) }
@@ -53,7 +53,7 @@ RSpec.describe API::Entities::Ml::Mlflow::RunInfo do
describe 'artifact_uri' do
it 'is not implemented' do
- expect(subject[:artifact_uri]).to eq('not_implemented')
+ expect(subject[:artifact_uri]).to eq("http://example.com#{candidate.artifact_root}")
end
end
diff --git a/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb b/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
index 9ea519d367e..dc17dc594a8 100644
--- a/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
@@ -16,8 +16,10 @@ RSpec.describe BulkImports::Common::Pipelines::EntityFinisher do
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_id: entity.id,
bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_class: described_class.name,
message: 'Entity finished',
+ source_version: entity.bulk_import.source_version_info.to_s,
importer: 'gitlab_migration'
)
end
diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb
index a5a01354d0e..e66f2d26911 100644
--- a/spec/lib/bulk_imports/pipeline/runner_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb
@@ -55,14 +55,21 @@ RSpec.describe BulkImports::Pipeline::Runner do
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger).to receive(:error)
.with(
- log_params(
- context,
- pipeline_step: :extractor,
- pipeline_class: 'BulkImports::MyPipeline',
- exception_class: exception_class,
- exception_message: exception_message,
- message: "Pipeline failed",
- importer: 'gitlab_migration'
+ a_hash_including(
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'pipeline_step' => :extractor,
+ 'pipeline_class' => 'BulkImports::MyPipeline',
+ 'exception.class' => exception_class,
+ 'exception.message' => exception_message,
+ 'correlation_id' => anything,
+ 'class' => 'BulkImports::MyPipeline',
+ 'message' => "Pipeline failed",
+ 'importer' => 'gitlab_migration',
+ 'exception.backtrace' => anything,
+ 'source_version' => entity.bulk_import.source_version_info.to_s
)
)
end
@@ -296,6 +303,8 @@ RSpec.describe BulkImports::Pipeline::Runner do
bulk_import_id: context.bulk_import_id,
bulk_import_entity_id: context.entity.id,
bulk_import_entity_type: context.entity.source_type,
+ source_full_path: entity.source_full_path,
+ source_version: context.entity.bulk_import.source_version_info.to_s,
importer: 'gitlab_migration',
context_extra: context.extra
}.merge(extra)
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 186d4e1fb42..f83ce01c617 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe Gitlab::BitbucketImport::Importer do
before do
stub_omniauth_provider('bitbucket')
- stub_feature_flags(stricter_mr_branch_name: false)
end
let(:statuses) do
diff --git a/spec/lib/gitlab/kas/client_spec.rb b/spec/lib/gitlab/kas/client_spec.rb
index 5b89023cc13..9a0fa6c4067 100644
--- a/spec/lib/gitlab/kas/client_spec.rb
+++ b/spec/lib/gitlab/kas/client_spec.rb
@@ -111,11 +111,16 @@ RSpec.describe Gitlab::Kas::Client do
describe 'with grpcs' do
let(:stub) { instance_double(Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub) }
+ let(:credentials) { instance_double(GRPC::Core::ChannelCredentials) }
let(:kas_url) { 'grpcs://example.kas.internal' }
- it 'uses a ChannelCredentials object' do
+ it 'uses a ChannelCredentials object with the correct certificates' do
+ expect(GRPC::Core::ChannelCredentials).to receive(:new)
+ .with(Gitlab::X509::Certificate.ca_certs_bundle)
+ .and_return(credentials)
+
expect(Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub).to receive(:new)
- .with('example.kas.internal', instance_of(GRPC::Core::ChannelCredentials), timeout: described_class::TIMEOUT)
+ .with('example.kas.internal', credentials, timeout: described_class::TIMEOUT)
.and_return(stub)
allow(stub).to receive(:list_agent_config_files)
diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb
index db0814af422..8990aa94b47 100644
--- a/spec/models/diff_viewer/server_side_spec.rb
+++ b/spec/models/diff_viewer/server_side_spec.rb
@@ -16,34 +16,6 @@ RSpec.describe DiffViewer::ServerSide do
subject { viewer_class.new(diff_file) }
- describe '#prepare!' do
- before do
- stub_feature_flags(disable_load_entire_blob_for_diff_viewer: feature_flag_enabled)
- end
-
- context 'when the disable_load_entire_blob_for_diff_viewer flag is disabled' do
- let(:feature_flag_enabled) { false }
-
- it 'loads all diff file data' do
- subject
- expect(diff_file).to receive_message_chain(:old_blob, :load_all_data!)
- expect(diff_file).to receive_message_chain(:new_blob, :load_all_data!)
- subject.prepare!
- end
- end
-
- context 'when the disable_load_entire_blob_for_diff_viewer flag is enabled' do
- let(:feature_flag_enabled) { true }
-
- it 'does not load file data' do
- subject
- expect(diff_file).not_to receive(:old_blob)
- expect(diff_file).not_to receive(:new_blob)
- subject.prepare!
- end
- end
- end
-
describe '#render_error' do
context 'when the diff file is stored externally' do
before do
diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb
index de6ce3ba053..0f9d5bf3c2d 100644
--- a/spec/models/experiment_spec.rb
+++ b/spec/models/experiment_spec.rb
@@ -3,12 +3,9 @@
require 'spec_helper'
RSpec.describe Experiment do
- include AfterNextHelpers
-
subject { build(:experiment) }
describe 'associations' do
- it { is_expected.to have_many(:experiment_users) }
it { is_expected.to have_many(:experiment_subjects) }
end
@@ -18,223 +15,6 @@ RSpec.describe Experiment do
it { is_expected.to validate_length_of(:name).is_at_most(255) }
end
- describe '.add_user' do
- let_it_be(:experiment_name) { :experiment_key }
- let_it_be(:user) { 'a user' }
- let_it_be(:group) { 'a group' }
- let_it_be(:context) { { a: 42 } }
-
- subject(:add_user) { described_class.add_user(experiment_name, group, user, context) }
-
- context 'when an experiment with the provided name does not exist' do
- it 'creates a new experiment record' do
- allow_next_instance_of(described_class) do |experiment|
- allow(experiment).to receive(:record_user_and_group).with(user, group, context)
- end
- expect { add_user }.to change(described_class, :count).by(1)
- end
-
- it 'forwards the user, group_type, and context to the instance' do
- expect_next_instance_of(described_class) do |experiment|
- expect(experiment).to receive(:record_user_and_group).with(user, group, context)
- end
- add_user
- end
- end
-
- context 'when an experiment with the provided name already exists' do
- let_it_be(:experiment) { create(:experiment, name: experiment_name) }
-
- it 'does not create a new experiment record' do
- allow_next_found_instance_of(described_class) do |experiment|
- allow(experiment).to receive(:record_user_and_group).with(user, group, context)
- end
- expect { add_user }.not_to change(described_class, :count)
- end
-
- it 'forwards the user, group_type, and context to the instance' do
- expect_next_found_instance_of(described_class) do |experiment|
- expect(experiment).to receive(:record_user_and_group).with(user, group, context)
- end
- add_user
- end
- end
-
- it 'works without the optional context argument' do
- allow_next_instance_of(described_class) do |experiment|
- expect(experiment).to receive(:record_user_and_group).with(user, group, {})
- end
-
- expect { described_class.add_user(experiment_name, group, user) }.not_to raise_error
- end
- end
-
- describe '.add_group' do
- let_it_be(:experiment_name) { :experiment_key }
- let_it_be(:variant) { :control }
- let_it_be(:group) { build(:group) }
-
- subject(:add_group) { described_class.add_group(experiment_name, variant: variant, group: group) }
-
- context 'when an experiment with the provided name does not exist' do
- it 'creates a new experiment record' do
- allow_next(described_class, name: :experiment_key)
- .to receive(:record_subject_and_variant!).with(group, variant)
-
- expect { add_group }.to change(described_class, :count).by(1)
- end
- end
-
- context 'when an experiment with the provided name already exists' do
- before do
- create(:experiment, name: experiment_name)
- end
-
- it 'does not create a new experiment record' do
- expect { add_group }.not_to change(described_class, :count)
- end
- end
- end
-
- describe '.record_conversion_event' do
- let_it_be(:user) { build(:user) }
- let_it_be(:context) { { a: 42 } }
-
- let(:experiment_key) { :test_experiment }
-
- subject(:record_conversion_event) { described_class.record_conversion_event(experiment_key, user, context) }
-
- context 'when no matching experiment exists' do
- it 'creates the experiment and uses it' do
- expect_next_instance_of(described_class) do |experiment|
- expect(experiment).to receive(:record_conversion_event_for_user)
- end
- expect { record_conversion_event }.to change { described_class.count }.by(1)
- end
-
- context 'but we are unable to successfully create one' do
- let(:experiment_key) { nil }
-
- it 'raises a RecordInvalid error' do
- expect { record_conversion_event }.to raise_error(ActiveRecord::RecordInvalid)
- end
- end
- end
-
- context 'when a matching experiment already exists' do
- before do
- create(:experiment, name: experiment_key)
- end
-
- it 'sends record_conversion_event_for_user to the experiment instance' do
- expect_next_found_instance_of(described_class) do |experiment|
- expect(experiment).to receive(:record_conversion_event_for_user).with(user, context)
- end
- record_conversion_event
- end
- end
- end
-
- shared_examples 'experiment user with context' do
- let_it_be(:context) { { a: 42, 'b' => 34, 'c': { c1: 100, c2: 'c2', e: :e }, d: [1, 3] } }
- let_it_be(:initial_expected_context) { { 'a' => 42, 'b' => 34, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [1, 3] } }
-
- before do
- subject
- experiment.record_user_and_group(user, :experimental, {})
- end
-
- it 'has an initial context with stringified keys' do
- expect(ExperimentUser.last.context).to eq(initial_expected_context)
- end
-
- context 'when updated' do
- before do
- subject
- experiment.record_user_and_group(user, :experimental, new_context)
- end
-
- context 'with an empty context' do
- let_it_be(:new_context) { {} }
-
- it 'keeps the initial context' do
- expect(ExperimentUser.last.context).to eq(initial_expected_context)
- end
- end
-
- context 'with string keys' do
- let_it_be(:new_context) { { f: :some_symbol } }
-
- it 'adds new symbols stringified' do
- expected_context = initial_expected_context.merge('f' => 'some_symbol')
- expect(ExperimentUser.last.context).to eq(expected_context)
- end
- end
-
- context 'with atomic values or array values' do
- let_it_be(:new_context) { { b: 97, d: [99] } }
-
- it 'overrides the values' do
- expected_context = { 'a' => 42, 'b' => 97, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [99] }
- expect(ExperimentUser.last.context).to eq(expected_context)
- end
- end
-
- context 'with nested hashes' do
- let_it_be(:new_context) { { c: { g: 107 } } }
-
- it 'inserts nested additional values in the same keys' do
- expected_context = initial_expected_context.deep_merge('c' => { 'g' => 107 })
- expect(ExperimentUser.last.context).to eq(expected_context)
- end
- end
- end
- end
-
- describe '#record_conversion_event_for_user' do
- let_it_be(:user) { create(:user) }
- let_it_be(:experiment) { create(:experiment) }
- let_it_be(:context) { { a: 42 } }
-
- subject { experiment.record_conversion_event_for_user(user, context) }
-
- context 'when no existing experiment_user record exists for the given user' do
- it 'does not update or create an experiment_user record' do
- expect { subject }.not_to change { ExperimentUser.all.to_a }
- end
- end
-
- context 'when an existing experiment_user exists for the given user' do
- context 'but it has already been converted' do
- let!(:experiment_user) { create(:experiment_user, experiment: experiment, user: user, converted_at: 2.days.ago) }
-
- it 'does not update the converted_at value' do
- expect { subject }.not_to change { experiment_user.converted_at }
- end
-
- it_behaves_like 'experiment user with context' do
- before do
- experiment.record_user_and_group(user, :experimental, context)
- end
- end
- end
-
- context 'and it has not yet been converted' do
- let(:experiment_user) { create(:experiment_user, experiment: experiment, user: user) }
-
- it 'updates the converted_at value' do
- expect { subject }.to change { experiment_user.reload.converted_at }
- end
-
- it_behaves_like 'experiment user with context' do
- before do
- experiment.record_user_and_group(user, :experimental, context)
- end
- end
- end
- end
- end
-
describe '#record_conversion_event_for_subject' do
let_it_be(:user) { create(:user) }
let_it_be(:experiment) { create(:experiment) }
@@ -367,62 +147,4 @@ RSpec.describe Experiment do
end
end
end
-
- describe '#record_user_and_group' do
- let_it_be(:experiment) { create(:experiment) }
- let_it_be(:user) { create(:user) }
- let_it_be(:group) { :control }
- let_it_be(:context) { { a: 42 } }
-
- subject { experiment.record_user_and_group(user, group, context) }
-
- context 'when an experiment_user does not yet exist for the given user' do
- it 'creates a new experiment_user record' do
- expect { subject }.to change(ExperimentUser, :count).by(1)
- end
-
- it 'assigns the correct group_type to the experiment_user' do
- subject
-
- expect(ExperimentUser.last.group_type).to eq('control')
- end
-
- it 'adds the correct context to the experiment_user' do
- subject
-
- expect(ExperimentUser.last.context).to eq({ 'a' => 42 })
- end
- end
-
- context 'when an experiment_user already exists for the given user' do
- before do
- # Create an existing experiment_user for this experiment and the :control group
- experiment.record_user_and_group(user, :control)
- end
-
- it 'does not create a new experiment_user record' do
- expect { subject }.not_to change(ExperimentUser, :count)
- end
-
- context 'when group type or context did not change' do
- let(:context) { {} }
-
- it 'does not initiate a transaction' do
- expect(Experiment.connection).not_to receive(:transaction)
-
- subject
- end
- end
-
- context 'but the group_type and context has changed' do
- let(:group) { :experimental }
-
- it 'updates the existing experiment_user record with group_type' do
- expect { subject }.to change { ExperimentUser.last.group_type }
- end
- end
-
- it_behaves_like 'experiment user with context'
- end
- end
end
diff --git a/spec/models/experiment_user_spec.rb b/spec/models/experiment_user_spec.rb
deleted file mode 100644
index 9201529b145..00000000000
--- a/spec/models/experiment_user_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ExperimentUser do
- describe 'Associations' do
- it { is_expected.to belong_to(:experiment) }
- it { is_expected.to belong_to(:user) }
- end
-
- describe 'Validations' do
- it { is_expected.to validate_presence_of(:group_type) }
- end
-end
diff --git a/spec/models/integrations/datadog_spec.rb b/spec/models/integrations/datadog_spec.rb
index 71a5bbc4db1..65ecd9bee83 100644
--- a/spec/models/integrations/datadog_spec.rb
+++ b/spec/models/integrations/datadog_spec.rb
@@ -73,7 +73,15 @@ RSpec.describe Integrations::Datadog do
it { is_expected.to validate_presence_of(:datadog_site) }
it { is_expected.not_to validate_presence_of(:api_url) }
+ it { is_expected.to allow_value('data-dog-hq.com').for(:datadog_site) }
+ it { is_expected.to allow_value('dataDOG.com').for(:datadog_site) }
it { is_expected.not_to allow_value('datadog hq.com').for(:datadog_site) }
+ it { is_expected.not_to allow_value('-datadoghq.com').for(:datadog_site) }
+ it { is_expected.not_to allow_value('.datadoghq.com').for(:datadog_site) }
+ it { is_expected.not_to allow_value('datadoghq.com_').for(:datadog_site) }
+ it { is_expected.not_to allow_value('data-dog').for(:datadog_site) }
+ it { is_expected.not_to allow_value('datadoghq.com-').for(:datadog_site) }
+ it { is_expected.not_to allow_value('datadoghq.com.').for(:datadog_site) }
end
context 'with custom api_url' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 21bba0c270d..665e62b82b9 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -232,10 +232,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
context 'for branch' do
- before do
- stub_feature_flags(stricter_mr_branch_name: false)
- end
-
where(:branch_name, :valid) do
'foo' | true
'foo:bar' | false
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
index 3bf1e80a152..84e0e6be199 100644
--- a/spec/models/ml/candidate_spec.rb
+++ b/spec/models/ml/candidate_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ml::Candidate, factory_default: :keep do
- let_it_be(:candidate) { create_default(:ml_candidates, :with_metrics_and_params) }
+ let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params) }
describe 'associations' do
it { is_expected.to belong_to(:experiment) }
@@ -12,6 +12,12 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
it { is_expected.to have_many(:metrics) }
end
+ describe '.artifact_root' do
+ subject { candidate.artifact_root }
+
+ it { is_expected.to eq("/ml_candidate_#{candidate.iid}/-/") }
+ end
+
describe '#new' do
it 'iid is not null' do
expect(candidate.iid).not_to be_nil
diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb
index 09e9359c0b3..c588c99c1bf 100644
--- a/spec/requests/api/ml/mlflow_spec.rb
+++ b/spec/requests/api/ml/mlflow_spec.rb
@@ -295,7 +295,6 @@ RSpec.describe API::Ml::Mlflow do
'experiment_id' => params[:experiment_id],
'user_id' => current_user.id.to_s,
'start_time' => params[:start_time],
- 'artifact_uri' => 'not_implemented',
'status' => "RUNNING",
'lifecycle_stage' => "active"
}
@@ -339,7 +338,7 @@ RSpec.describe API::Ml::Mlflow do
'experiment_id' => candidate.experiment.iid.to_s,
'user_id' => candidate.user.id.to_s,
'start_time' => candidate.start_time,
- 'artifact_uri' => 'not_implemented',
+ 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_candidate_#{candidate.iid}/-/",
'status' => "RUNNING",
'lifecycle_stage' => "active"
}
@@ -378,7 +377,7 @@ RSpec.describe API::Ml::Mlflow do
'user_id' => candidate.user.id.to_s,
'start_time' => candidate.start_time,
'end_time' => params[:end_time],
- 'artifact_uri' => 'not_implemented',
+ 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_candidate_#{candidate.iid}/-/",
'status' => 'FAILED',
'lifecycle_stage' => 'active'
}
diff --git a/spec/requests/projects/merge_requests/diffs_spec.rb b/spec/requests/projects/merge_requests/diffs_spec.rb
index 9f0b9a9cb1b..57548a3c83f 100644
--- a/spec/requests/projects/merge_requests/diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/diffs_spec.rb
@@ -80,6 +80,20 @@ RSpec.describe 'Merge Requests Diffs' do
expect(response).to have_gitlab_http_status(:not_modified)
end
+ context 'with check_etags_diffs_batch_before_write_cache flag turned off' do
+ before do
+ stub_feature_flags(check_etags_diffs_batch_before_write_cache: false)
+ end
+
+ it 'does not serialize diffs' do
+ expect(PaginatedDiffSerializer).not_to receive(:new)
+
+ go(headers: headers, page: 0, per_page: 5)
+
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
+ end
+
context 'with the different user' do
let(:another_user) { create(:user) }
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
diff --git a/spec/rubocop/cop/gitlab/json_spec.rb b/spec/rubocop/cop/gitlab/json_spec.rb
index e4ec107747d..bf8a250b55c 100644
--- a/spec/rubocop/cop/gitlab/json_spec.rb
+++ b/spec/rubocop/cop/gitlab/json_spec.rb
@@ -5,12 +5,20 @@ require_relative '../../../../rubocop/cop/gitlab/json'
RSpec.describe RuboCop::Cop::Gitlab::Json do
context 'when ::JSON is called' do
- it 'registers an offense' do
+ it 'registers an offense and autocorrects' do
expect_offense(<<~RUBY)
class Foo
def bar
JSON.parse('{ "foo": "bar" }')
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid calling `JSON` directly. [...]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `Gitlab::Json` over calling `JSON` or `to_json` directly. [...]
+ end
+ end
+ RUBY
+
+ expect_correction(<<~RUBY)
+ class Foo
+ def bar
+ Gitlab::Json.parse('{ "foo": "bar" }')
end
end
RUBY
@@ -18,12 +26,41 @@ RSpec.describe RuboCop::Cop::Gitlab::Json do
end
context 'when ActiveSupport::JSON is called' do
- it 'registers an offense' do
+ it 'registers an offense and autocorrects' do
expect_offense(<<~RUBY)
class Foo
def bar
ActiveSupport::JSON.parse('{ "foo": "bar" }')
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid calling `JSON` directly. [...]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `Gitlab::Json` over calling `JSON` or `to_json` directly. [...]
+ end
+ end
+ RUBY
+
+ expect_correction(<<~RUBY)
+ class Foo
+ def bar
+ Gitlab::Json.parse('{ "foo": "bar" }')
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'when .to_json is called' do
+ it 'registers an offense and autocorrects' do
+ expect_offense(<<~RUBY)
+ class Foo
+ def bar
+ { foo: "bar" }.to_json
+ ^^^^^^^^^^^^^^^^^^^^^^ Prefer `Gitlab::Json` over calling `JSON` or `to_json` directly. [...]
+ end
+ end
+ RUBY
+
+ expect_correction(<<~RUBY)
+ class Foo
+ def bar
+ Gitlab::Json.generate({ foo: "bar" })
end
end
RUBY
diff --git a/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb b/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb
index 0de962328c5..5a7852fc32f 100644
--- a/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb
+++ b/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb
@@ -77,6 +77,8 @@ RSpec.describe BulkImports::CreatePipelineTrackersService do
message: 'Pipeline skipped as source instance version not compatible with pipeline',
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
importer: 'gitlab_migration',
pipeline_name: 'PipelineClass4',
minimum_source_version: '15.1.0',
@@ -88,6 +90,8 @@ RSpec.describe BulkImports::CreatePipelineTrackersService do
message: 'Pipeline skipped as source instance version not compatible with pipeline',
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
importer: 'gitlab_migration',
pipeline_name: 'PipelineClass5',
minimum_source_version: '16.0.0',
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index 7bf6dfd0fd8..43d23023d83 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -244,9 +244,10 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
shared_examples 'process truncated alerts' do
- it 'returns 200 but skips processing and logs a warning', :aggregate_failures do
+ it 'returns 201 but skips processing and logs a warning', :aggregate_failures do
expect(subject).to be_success
- expect(subject.payload[:alerts].size).to eq(max_alerts)
+ expect(subject.payload).to eq({})
+ expect(subject.http_status).to eq(:created)
expect(Gitlab::AppLogger)
.to have_received(:warn)
.with(
@@ -260,9 +261,10 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
shared_examples 'process all alerts' do
- it 'returns 200 and process alerts without warnings', :aggregate_failures do
+ it 'returns 201 and process alerts without warnings', :aggregate_failures do
expect(subject).to be_success
- expect(subject.payload[:alerts].size).to eq(2)
+ expect(subject.payload).to eq({})
+ expect(subject.http_status).to eq(:created)
expect(Gitlab::AppLogger).not_to have_received(:warn)
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index c4377e368ee..0558bfd51c3 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -10917,7 +10917,6 @@
- './spec/workers/environments/canary_ingress/update_worker_spec.rb'
- './spec/workers/error_tracking_issue_link_worker_spec.rb'
- './spec/workers/every_sidekiq_worker_spec.rb'
-- './spec/workers/experiments/record_conversion_event_worker_spec.rb'
- './spec/workers/expire_build_artifacts_worker_spec.rb'
- './spec/workers/export_csv_worker_spec.rb'
- './spec/workers/external_service_reactive_caching_worker_spec.rb'
diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb
index 571cb7dc03d..b46ace1824a 100644
--- a/spec/support/shared_examples/services/alert_management_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -72,8 +72,8 @@ RSpec.shared_examples 'processes one firing and one resolved prometheus alerts'
.and change(Note, :count).by(1)
expect(subject).to be_success
- expect(subject.payload[:alerts]).to all(be_a_kind_of(AlertManagement::Alert))
- expect(subject.payload[:alerts].size).to eq(1)
+ expect(subject.payload).to eq({})
+ expect(subject.http_status).to eq(:created)
end
it_behaves_like 'processes incident issues'
diff --git a/spec/workers/bulk_imports/entity_worker_spec.rb b/spec/workers/bulk_imports/entity_worker_spec.rb
index 0fcdbccc304..e3f0ee65205 100644
--- a/spec/workers/bulk_imports/entity_worker_spec.rb
+++ b/spec/workers/bulk_imports/entity_worker_spec.rb
@@ -39,8 +39,11 @@ RSpec.describe BulkImports::EntityWorker do
hash_including(
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
'current_stage' => nil,
'message' => 'Stage starting',
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
'importer' => 'gitlab_migration'
)
)
@@ -71,7 +74,10 @@ RSpec.describe BulkImports::EntityWorker do
hash_including(
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
'current_stage' => nil,
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
'importer' => 'gitlab_migration'
)
)
@@ -82,9 +88,15 @@ RSpec.describe BulkImports::EntityWorker do
hash_including(
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
'current_stage' => nil,
- 'message' => 'Error!',
- 'importer' => 'gitlab_migration'
+ 'message' => 'Entity failed',
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'StandardError',
+ 'exception.message' => 'Error!',
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
)
)
end
@@ -95,6 +107,9 @@ RSpec.describe BulkImports::EntityWorker do
exception,
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
+ source_version: entity.bulk_import.source_version_info.to_s,
importer: 'gitlab_migration'
)
@@ -112,8 +127,11 @@ RSpec.describe BulkImports::EntityWorker do
hash_including(
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
'current_stage' => 0,
'message' => 'Stage running',
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
'importer' => 'gitlab_migration'
)
)
@@ -142,7 +160,10 @@ RSpec.describe BulkImports::EntityWorker do
hash_including(
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
'current_stage' => 0,
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
'importer' => 'gitlab_migration'
)
)
diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb
index 597eed3a9b9..7eb8150fb2e 100644
--- a/spec/workers/bulk_imports/export_request_worker_spec.rb
+++ b/spec/workers/bulk_imports/export_request_worker_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe BulkImports::ExportRequestWorker do
end
shared_examples 'requests relations export for api resource' do
- include_examples 'an idempotent worker' do
+ it_behaves_like 'an idempotent worker' do
it 'requests relations export & schedules entity worker' do
expect_next_instance_of(BulkImports::Clients::HTTP) do |client|
expect(client).to receive(:post).with(expected).twice
@@ -44,18 +44,22 @@ RSpec.describe BulkImports::ExportRequestWorker do
it 'logs retry request and reenqueues' do
allow(exception).to receive(:retriable?).twice.and_return(true)
- expect(Gitlab::Import::Logger).to receive(:error).with(
- hash_including(
- 'bulk_import_entity_id' => entity.id,
- 'pipeline_class' => 'ExportRequestWorker',
- 'exception_class' => 'BulkImports::NetworkError',
- 'exception_message' => 'Export error',
- 'bulk_import_id' => bulk_import.id,
- 'bulk_import_entity_type' => entity.source_type,
- 'importer' => 'gitlab_migration',
- 'message' => 'Retrying export request'
- )
- ).twice
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ a_hash_including(
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'BulkImports::NetworkError',
+ 'exception.message' => 'Export error',
+ 'message' => 'Retrying export request',
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
+ )
+ ).twice
+ end
expect(described_class).to receive(:perform_in).twice.with(2.seconds, entity.id)
@@ -65,18 +69,24 @@ RSpec.describe BulkImports::ExportRequestWorker do
context 'when error is not retriable' do
it 'logs export failure and marks entity as failed' do
- expect(Gitlab::Import::Logger).to receive(:error).with(
- hash_including(
- 'bulk_import_entity_id' => entity.id,
- 'pipeline_class' => 'ExportRequestWorker',
- 'exception_class' => 'BulkImports::NetworkError',
- 'exception_message' => 'Export error',
- 'correlation_id_value' => anything,
- 'bulk_import_id' => bulk_import.id,
- 'bulk_import_entity_type' => entity.source_type,
- 'importer' => 'gitlab_migration'
- )
- ).twice
+ allow(exception).to receive(:retriable?).twice.and_return(false)
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ a_hash_including(
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'BulkImports::NetworkError',
+ 'exception.message' => 'Export error',
+ 'message' => "Request to export #{entity.source_type} failed",
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
+ )
+ ).twice
+ end
perform_multiple(job_args)
@@ -119,25 +129,30 @@ RSpec.describe BulkImports::ExportRequestWorker do
let(:entity_source_id) { 'invalid' }
it 'logs the error & requests relations export using full path url' do
+ allow(BulkImports::EntityWorker).to receive(:perform_async)
+
expect_next_instance_of(BulkImports::Clients::HTTP) do |client|
expect(client).to receive(:post).with(full_path_url).twice
end
entity.update!(source_xid: nil)
- expect(Gitlab::Import::Logger).to receive(:error).with(
- a_hash_including(
- 'message' => 'Failed to fetch source entity id',
- 'bulk_import_entity_id' => entity.id,
- 'pipeline_class' => 'ExportRequestWorker',
- 'exception_class' => 'NoMethodError',
- 'exception_message' => "undefined method `model_id' for nil:NilClass",
- 'correlation_id_value' => anything,
- 'bulk_import_id' => bulk_import.id,
- 'bulk_import_entity_type' => entity.source_type,
- 'importer' => 'gitlab_migration'
- )
- ).twice
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ a_hash_including(
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'NoMethodError',
+ 'exception.message' => "undefined method `model_id' for nil:NilClass",
+ 'message' => 'Failed to fetch source entity id',
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
+ )
+ ).twice
+ end
perform_multiple(job_args)
@@ -153,7 +168,7 @@ RSpec.describe BulkImports::ExportRequestWorker do
let(:expected) { "/groups/#{entity.source_xid}/export_relations" }
let(:full_path_url) { '/groups/foo%2Fbar/export_relations' }
- include_examples 'requests relations export for api resource'
+ it_behaves_like 'requests relations export for api resource'
end
context 'when entity is project' do
@@ -161,7 +176,7 @@ RSpec.describe BulkImports::ExportRequestWorker do
let(:expected) { "/projects/#{entity.source_xid}/export_relations" }
let(:full_path_url) { '/projects/foo%2Fbar/export_relations' }
- include_examples 'requests relations export for api resource'
+ it_behaves_like 'requests relations export for api resource'
end
end
end
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index ee65775f170..23fbc5688ec 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -37,9 +37,10 @@ RSpec.describe BulkImports::PipelineWorker do
.with(
hash_including(
'pipeline_name' => 'FakePipeline',
- 'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path
)
)
end
@@ -87,8 +88,10 @@ RSpec.describe BulkImports::PipelineWorker do
'pipeline_tracker_id' => pipeline_tracker.id,
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
- 'message' => 'Unstarted pipeline not found',
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
+ 'message' => 'Unstarted pipeline not found'
)
)
end
@@ -126,7 +129,13 @@ RSpec.describe BulkImports::PipelineWorker do
'pipeline_name' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
- 'message' => 'Error!',
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'class' => 'BulkImports::PipelineWorker',
+ 'exception.backtrace' => anything,
+ 'exception.message' => 'Error!',
+ 'message' => 'Pipeline failed',
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
'importer' => 'gitlab_migration'
)
)
@@ -137,9 +146,12 @@ RSpec.describe BulkImports::PipelineWorker do
.with(
instance_of(StandardError),
bulk_import_entity_id: entity.id,
- bulk_import_id: entity.bulk_import_id,
+ bulk_import_id: entity.bulk_import.id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_name: pipeline_tracker.pipeline_name,
- importer: 'gitlab_migration'
+ importer: 'gitlab_migration',
+ source_version: entity.bulk_import.source_version_info.to_s
)
expect(BulkImports::EntityWorker)
@@ -188,8 +200,9 @@ RSpec.describe BulkImports::PipelineWorker do
'pipeline_name' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
- 'message' => 'Skipping pipeline due to failed entity',
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'message' => 'Skipping pipeline due to failed entity'
)
)
end
@@ -237,7 +250,8 @@ RSpec.describe BulkImports::PipelineWorker do
'pipeline_name' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path
)
)
end
@@ -361,9 +375,16 @@ RSpec.describe BulkImports::PipelineWorker do
hash_including(
'pipeline_name' => 'NdjsonPipeline',
'bulk_import_entity_id' => entity.id,
- 'message' => 'Pipeline timeout',
'bulk_import_id' => entity.bulk_import_id,
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'class' => 'BulkImports::PipelineWorker',
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'BulkImports::Pipeline::ExpiredError',
+ 'exception.message' => 'Pipeline timeout',
+ 'importer' => 'gitlab_migration',
+ 'message' => 'Pipeline failed',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
)
)
end
@@ -390,9 +411,14 @@ RSpec.describe BulkImports::PipelineWorker do
hash_including(
'pipeline_name' => 'NdjsonPipeline',
'bulk_import_entity_id' => entity.id,
- 'message' => 'Export from source instance failed: Error!',
'bulk_import_id' => entity.bulk_import_id,
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'BulkImports::Pipeline::FailedError',
+ 'exception.message' => 'Export from source instance failed: Error!',
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
)
)
end