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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-15 18:09:59 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-15 18:09:59 +0300
commit0ff373dc416216d02760c7c162ee23382eb1f4a3 (patch)
tree45955917f14f68b0d2a2357766644f49a8476af6
parente894595ad8ebdbd565bacaeb126d44f80a636fd8 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml3
-rw-r--r--.rubocop_manual_todo.yml517
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue78
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue106
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js66
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql8
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql3
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql3
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql3
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql6
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js45
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js31
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/index.js4
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue6
-rw-r--r--app/assets/stylesheets/page_bundles/import.scss24
-rw-r--r--app/controllers/concerns/wiki_actions.rb4
-rw-r--r--app/controllers/projects/wikis_controller.rb3
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/models/application_setting.rb6
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/ci/pipeline.rb1
-rw-r--r--app/models/concerns/case_sensitivity.rb12
-rw-r--r--app/models/concerns/routable.rb47
-rw-r--r--app/models/concerns/token_authenticatable.rb7
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb10
-rw-r--r--app/models/personal_access_token.rb13
-rw-r--r--app/models/redirect_route.rb2
-rw-r--r--app/models/route.rb2
-rw-r--r--app/models/user.rb2
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml3
-rw-r--r--app/views/import/bulk_imports/status.html.haml4
-rw-r--r--app/views/shared/wikis/_sidebar.html.haml9
-rw-r--r--app/views/shared/wikis/git_access.html.haml (renamed from app/views/projects/wikis/git_access.html.haml)0
-rw-r--r--changelogs/unreleased/207869-group-wikis-git-support.yml5
-rw-r--r--changelogs/unreleased/change_unique_index_on_security_findings.yml5
-rw-r--r--changelogs/unreleased/feat-token-prefix.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-update-project-imported-usage-ping.yml5
-rw-r--r--config/feature_flags/development/ci_allow_failure_with_exit_codes.yml8
-rw-r--r--config/routes/git_http.rb2
-rw-r--r--db/migrate/20201119133534_add_personal_access_token_prefix_to_application_setting.rb12
-rw-r--r--db/migrate/20201119133604_add_text_limit_to_application_setting_personal_access_token_prefix.rb16
-rw-r--r--db/migrate/20201210101250_add_index_projects_on_import_type_and_creator_id.rb19
-rw-r--r--db/migrate/20201215132151_change_unique_index_on_security_findings.rb36
-rw-r--r--db/schema_migrations/202011191335341
-rw-r--r--db/schema_migrations/202011191336041
-rw-r--r--db/schema_migrations/202012101012501
-rw-r--r--db/schema_migrations/202012151321511
-rw-r--r--db/structure.sql6
-rw-r--r--doc/api/settings.md127
-rw-r--r--doc/architecture/blueprints/feature_flags_development/index.md24
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md19
-rw-r--r--doc/user/group/index.md2
-rw-r--r--lib/api/settings.rb1
-rw-r--r--lib/gitlab/auth/auth_finders.rb6
-rw-r--r--lib/gitlab/ci/config/entry/allow_failure.rb31
-rw-r--r--lib/gitlab/ci/config/entry/bridge.rb7
-rw-r--r--lib/gitlab/ci/config/entry/job.rb29
-rw-r--r--lib/gitlab/ci/config/entry/processable.rb7
-rw-r--r--lib/gitlab/ci/features.rb4
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb22
-rw-r--r--lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml29
-rw-r--r--lib/gitlab/ci/yaml_processor/result.rb1
-rw-r--r--lib/gitlab/config/entry/validators.rb26
-rw-r--r--lib/gitlab/gl_repository.rb1
-rw-r--r--lib/gitlab/repo_path.rb49
-rw-r--r--lib/gitlab/usage_data.rb4
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb38
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb2
-rw-r--r--spec/factories/personal_access_tokens.rb4
-rw-r--r--spec/features/groups/import_export/connect_instance_spec.rb14
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_row_spec.js112
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js103
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js178
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/fixtures.js51
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js82
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb92
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb41
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb4
-rw-r--r--spec/models/concerns/case_sensitivity_spec.rb12
-rw-r--r--spec/models/concerns/routable_spec.rb150
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb4
-rw-r--r--spec/requests/api/internal/base_spec.rb8
-rw-r--r--spec/requests/api/settings_spec.rb25
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb67
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb11
-rw-r--r--spec/views/shared/wikis/_sidebar.html.haml_spec.rb10
93 files changed, 2474 insertions, 222 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index b08dbdf9888..b133ecbcf93 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -440,9 +440,6 @@ Graphql/Descriptions:
- 'app/graphql/**/*'
- 'ee/app/graphql/**/*'
-RSpec/AnyInstanceOf:
- Enabled: false
-
# Cops for upgrade to gitlab-styles 3.1.0
RSpec/ImplicitSubject:
Enabled: false
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 290b776b33c..2ffbef850d0 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -1037,3 +1037,520 @@ Graphql/Descriptions:
- 'ee/app/graphql/types/vulnerable_dependency_type.rb'
- 'ee/app/graphql/types/vulnerable_package_type.rb'
- 'ee/app/graphql/types/vulnerable_projects_by_grade_type.rb'
+
+# WIP: https://gitlab.com/gitlab-org/gitlab/-/issues/34997
+RSpec/AnyInstanceOf:
+ Exclude:
+ - 'ee/spec/controllers/admin/geo/nodes_controller_spec.rb'
+ - 'ee/spec/controllers/ee/groups_controller_spec.rb'
+ - 'ee/spec/controllers/groups/analytics/productivity_analytics_controller_spec.rb'
+ - 'ee/spec/controllers/groups/epics/notes_controller_spec.rb'
+ - 'ee/spec/controllers/groups/omniauth_callbacks_controller_spec.rb'
+ - 'ee/spec/controllers/oauth/geo_auth_controller_spec.rb'
+ - 'ee/spec/controllers/projects/environments_controller_spec.rb'
+ - 'ee/spec/controllers/projects/integrations/jira/issues_controller_spec.rb'
+ - 'ee/spec/controllers/projects/merge_requests_controller_spec.rb'
+ - 'ee/spec/controllers/projects/path_locks_controller_spec.rb'
+ - 'ee/spec/controllers/projects_controller_spec.rb'
+ - 'ee/spec/controllers/subscriptions_controller_spec.rb'
+ - 'ee/spec/controllers/trials_controller_spec.rb'
+ - 'ee/spec/features/admin/admin_audit_logs_spec.rb'
+ - 'ee/spec/features/admin/admin_reset_pipeline_minutes_spec.rb'
+ - 'ee/spec/features/admin/admin_users_spec.rb'
+ - 'ee/spec/features/admin/licenses/admin_views_license_spec.rb'
+ - 'ee/spec/features/boards/scoped_issue_board_spec.rb'
+ - 'ee/spec/features/ci_shared_runner_warnings_spec.rb'
+ - 'ee/spec/features/groups/group_settings_spec.rb'
+ - 'ee/spec/features/groups/navbar_spec.rb'
+ - 'ee/spec/features/groups/saml_providers_spec.rb'
+ - 'ee/spec/features/issues/form_spec.rb'
+ - 'ee/spec/features/merge_request/user_creates_merge_request_spec.rb'
+ - 'ee/spec/features/projects/new_project_spec.rb'
+ - 'ee/spec/features/projects/services/user_activates_jira_spec.rb'
+ - 'ee/spec/features/registrations/welcome_spec.rb'
+ - 'ee/spec/features/security/project/internal_access_spec.rb'
+ - 'ee/spec/features/security/project/private_access_spec.rb'
+ - 'ee/spec/features/security/project/public_access_spec.rb'
+ - 'ee/spec/features/trials/capture_lead_spec.rb'
+ - 'ee/spec/features/trials/select_namespace_spec.rb'
+ - 'ee/spec/features/users/login_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_on_demand_scans/create_spec.rb'
+ - 'ee/spec/graphql/mutations/incident_management/oncall_schedule/create_spec.rb'
+ - 'ee/spec/graphql/mutations/incident_management/oncall_schedule/destroy_spec.rb'
+ - 'ee/spec/graphql/mutations/incident_management/oncall_schedule/update_spec.rb'
+ - 'ee/spec/helpers/application_helper_spec.rb'
+ - 'ee/spec/lib/ee/api/helpers_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/auth/ldap/sync/group_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/checks/push_rule_check_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/checks/push_rules/commit_check_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/group_saml/membership_enforcer_spec.rb'
+ - 'ee/spec/lib/gitlab/auth/ldap/access_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/Jobs/dast_default_branch_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/Jobs/load_performance_testing_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/Verify/browser_performance_testing_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/api_fuzzing_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/container_scanning_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/coverage_fuzzing_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/dast_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/dependency_scanning_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/license_scanning_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/sast_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/elastic/project_search_results_spec.rb'
+ - 'ee/spec/lib/gitlab/expiring_subscription_message_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/log_cursor/daemon_spec.rb'
+ - 'ee/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb'
+ - 'ee/spec/lib/omni_auth/strategies/group_saml_spec.rb'
+ - 'ee/spec/lib/security/ci_configuration/sast_build_actions_spec.rb'
+ - 'ee/spec/lib/system_check/geo/geo_database_configured_check_spec.rb'
+ - 'ee/spec/migrations/schedule_populate_resolved_on_default_branch_column_spec.rb'
+ - 'ee/spec/migrations/update_location_fingerprint_column_for_cs_spec.rb'
+ - 'ee/spec/migrations/update_occurrence_severity_column_spec.rb'
+ - 'ee/spec/migrations/update_undefined_confidence_from_occurrences_spec.rb'
+ - 'ee/spec/migrations/update_undefined_confidence_from_vulnerabilities_spec.rb'
+ - 'ee/spec/migrations/update_vulnerability_severity_column_spec.rb'
+ - 'ee/spec/models/ee/namespace_spec.rb'
+ - 'ee/spec/models/geo_node_status_spec.rb'
+ - 'ee/spec/models/group_spec.rb'
+ - 'ee/spec/models/issue_spec.rb'
+ - 'ee/spec/models/merge_request_spec.rb'
+ - 'ee/spec/models/project_import_state_spec.rb'
+ - 'ee/spec/models/push_rule_spec.rb'
+ - 'ee/spec/presenters/ci/pipeline_presenter_spec.rb'
+ - 'ee/spec/presenters/projects/security/configuration_presenter_spec.rb'
+ - 'ee/spec/requests/api/geo_nodes_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_on_demand_scans/create_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/dast_site_profiles/delete_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/pipelines/run_dast_scan_spec.rb'
+ - 'ee/spec/requests/api/issues_spec.rb'
+ - 'ee/spec/requests/api/projects_spec.rb'
+ - 'ee/spec/requests/git_http_spec.rb'
+ - 'ee/spec/requests/groups_controller_spec.rb'
+ - 'ee/spec/requests/omniauth_kerberos_spnego_spec.rb'
+ - 'ee/spec/requests/repositories/git_http_controller_spec.rb'
+ - 'ee/spec/services/alert_management/network_alert_service_spec.rb'
+ - 'ee/spec/services/ci/expire_pipeline_cache_service_spec.rb'
+ - 'ee/spec/services/ci/run_dast_scan_service_spec.rb'
+ - 'ee/spec/services/ee/git/branch_push_service_spec.rb'
+ - 'ee/spec/services/ee/merge_requests/create_from_vulnerability_data_service_spec.rb'
+ - 'ee/spec/services/ee/merge_requests/refresh_service_spec.rb'
+ - 'ee/spec/services/ee/security/ingress_modsecurity_usage_service_spec.rb'
+ - 'ee/spec/services/ee/users/create_service_spec.rb'
+ - 'ee/spec/services/ee/users/destroy_service_spec.rb'
+ - 'ee/spec/services/geo/container_repository_sync_service_spec.rb'
+ - 'ee/spec/services/geo/design_repository_sync_service_spec.rb'
+ - 'ee/spec/services/geo/framework_repository_sync_service_spec.rb'
+ - 'ee/spec/services/geo/hashed_storage_migration_service_spec.rb'
+ - 'ee/spec/services/geo/metrics_update_service_spec.rb'
+ - 'ee/spec/services/geo/move_repository_service_spec.rb'
+ - 'ee/spec/services/geo/project_housekeeping_service_spec.rb'
+ - 'ee/spec/services/geo/rename_repository_service_spec.rb'
+ - 'ee/spec/services/geo/repository_destroy_service_spec.rb'
+ - 'ee/spec/services/geo/repository_sync_service_spec.rb'
+ - 'ee/spec/services/geo/wiki_sync_service_spec.rb'
+ - 'ee/spec/services/groups/destroy_service_spec.rb'
+ - 'ee/spec/services/groups/update_service_spec.rb'
+ - 'ee/spec/services/merge_trains/check_status_service_spec.rb'
+ - 'ee/spec/services/network_policies/resources_service_spec.rb'
+ - 'ee/spec/services/projects/destroy_service_spec.rb'
+ - 'ee/spec/services/projects/group_links/destroy_service_spec.rb'
+ - 'ee/spec/services/projects/update_service_spec.rb'
+ - 'ee/spec/services/slash_commands/global_slack_handler_spec.rb'
+ - 'ee/spec/support/helpers/ee/stub_configuration.rb'
+ - 'ee/spec/support/shared_examples/controllers/analytics/cycle_analytics/shared_stage_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/features/gold_trial_callout_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/lib/gitlab/geo/geo_logs_event_source_info_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/models/member_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/services/base_sync_service_shared_examples.rb'
+ - 'ee/spec/support/shared_examples/services/geo/geo_request_service_shared_examples.rb'
+ - 'ee/spec/workers/build_finished_worker_spec.rb'
+ - 'ee/spec/workers/concerns/elastic/indexing_control_spec.rb'
+ - 'ee/spec/workers/elastic_commit_indexer_worker_spec.rb'
+ - 'ee/spec/workers/geo/design_repository_shard_sync_worker_spec.rb'
+ - 'ee/spec/workers/geo/file_download_dispatch_worker_spec.rb'
+ - 'ee/spec/workers/geo/registry_sync_worker_spec.rb'
+ - 'ee/spec/workers/geo/repository_cleanup_worker_spec.rb'
+ - 'ee/spec/workers/geo/repository_shard_sync_worker_spec.rb'
+ - 'ee/spec/workers/project_cache_worker_spec.rb'
+ - 'ee/spec/workers/repository_import_worker_spec.rb'
+ - 'ee/spec/workers/vulnerability_exports/export_deletion_worker_spec.rb'
+ - 'qa/spec/runtime/release_spec.rb'
+ - 'spec/controllers/admin/sessions_controller_spec.rb'
+ - 'spec/controllers/application_controller_spec.rb'
+ - 'spec/controllers/concerns/issuable_actions_spec.rb'
+ - 'spec/controllers/concerns/static_object_external_storage_spec.rb'
+ - 'spec/controllers/explore/projects_controller_spec.rb'
+ - 'spec/controllers/groups/clusters_controller_spec.rb'
+ - 'spec/controllers/groups/settings/ci_cd_controller_spec.rb'
+ - 'spec/controllers/groups_controller_spec.rb'
+ - 'spec/controllers/import/bitbucket_controller_spec.rb'
+ - 'spec/controllers/oauth/jira/authorizations_controller_spec.rb'
+ - 'spec/controllers/omniauth_callbacks_controller_spec.rb'
+ - 'spec/controllers/projects/artifacts_controller_spec.rb'
+ - 'spec/controllers/projects/branches_controller_spec.rb'
+ - 'spec/controllers/projects/clusters_controller_spec.rb'
+ - 'spec/controllers/projects/commit_controller_spec.rb'
+ - 'spec/controllers/projects/commits_controller_spec.rb'
+ - 'spec/controllers/projects/environments_controller_spec.rb'
+ - 'spec/controllers/projects/imports_controller_spec.rb'
+ - 'spec/controllers/projects/issues_controller_spec.rb'
+ - 'spec/controllers/projects/jobs_controller_spec.rb'
+ - 'spec/controllers/projects/labels_controller_spec.rb'
+ - 'spec/controllers/projects/merge_requests_controller_spec.rb'
+ - 'spec/controllers/projects/pipelines_controller_spec.rb'
+ - 'spec/controllers/projects/service_hook_logs_controller_spec.rb'
+ - 'spec/controllers/projects/services_controller_spec.rb'
+ - 'spec/controllers/projects/tags_controller_spec.rb'
+ - 'spec/controllers/registrations/experience_levels_controller_spec.rb'
+ - 'spec/controllers/registrations_controller_spec.rb'
+ - 'spec/controllers/sessions_controller_spec.rb'
+ - 'spec/controllers/snippets/notes_controller_spec.rb'
+ - 'spec/controllers/snippets_controller_spec.rb'
+ - 'spec/features/admin/admin_mode/login_spec.rb'
+ - 'spec/features/groups/clusters/eks_spec.rb'
+ - 'spec/features/groups/members/tabs_spec.rb'
+ - 'spec/features/ide/static_object_external_storage_csp_spec.rb'
+ - 'spec/features/issuables/issuable_list_spec.rb'
+ - 'spec/features/issues/form_spec.rb'
+ - 'spec/features/merge_request/user_creates_image_diff_notes_spec.rb'
+ - 'spec/features/merge_request/user_reviews_image_spec.rb'
+ - 'spec/features/merge_request/user_sees_diff_spec.rb'
+ - 'spec/features/merge_request/user_sees_merge_widget_spec.rb'
+ - 'spec/features/profiles/personal_access_tokens_spec.rb'
+ - 'spec/features/projects/clusters/gcp_spec.rb'
+ - 'spec/features/projects/clusters_spec.rb'
+ - 'spec/features/projects/container_registry_spec.rb'
+ - 'spec/features/projects/files/user_browses_lfs_files_spec.rb'
+ - 'spec/features/projects/jobs_spec.rb'
+ - 'spec/features/projects/navbar_spec.rb'
+ - 'spec/features/projects/pages_spec.rb'
+ - 'spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb'
+ - 'spec/features/projects/settings/service_desk_setting_spec.rb'
+ - 'spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb'
+ - 'spec/features/snippets/embedded_snippet_spec.rb'
+ - 'spec/features/usage_stats_consent_spec.rb'
+ - 'spec/finders/prometheus_metrics_finder_spec.rb'
+ - 'spec/graphql/mutations/alert_management/create_alert_issue_spec.rb'
+ - 'spec/graphql/mutations/alert_management/http_integration/create_spec.rb'
+ - 'spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb'
+ - 'spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb'
+ - 'spec/graphql/mutations/alert_management/http_integration/update_spec.rb'
+ - 'spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb'
+ - 'spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb'
+ - 'spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb'
+ - 'spec/helpers/analytics/unique_visits_helper_spec.rb'
+ - 'spec/helpers/projects_helper_spec.rb'
+ - 'spec/initializers/lograge_spec.rb'
+ - 'spec/lib/api/entities/merge_request_basic_spec.rb'
+ - 'spec/lib/api/entities/merge_request_changes_spec.rb'
+ - 'spec/lib/api/helpers_spec.rb'
+ - 'spec/lib/backup/files_spec.rb'
+ - 'spec/lib/backup/manager_spec.rb'
+ - 'spec/lib/banzai/commit_renderer_spec.rb'
+ - 'spec/lib/banzai/filter/external_issue_reference_filter_spec.rb'
+ - 'spec/lib/banzai/filter/issue_reference_filter_spec.rb'
+ - 'spec/lib/banzai/filter/repository_link_filter_spec.rb'
+ - 'spec/lib/banzai/pipeline/gfm_pipeline_spec.rb'
+ - 'spec/lib/extracts_ref_spec.rb'
+ - 'spec/lib/feature_spec.rb'
+ - 'spec/lib/gitlab/app_logger_spec.rb'
+ - 'spec/lib/gitlab/asciidoc_spec.rb'
+ - 'spec/lib/gitlab/auth/auth_finders_spec.rb'
+ - 'spec/lib/gitlab/auth/blocked_user_tracker_spec.rb'
+ - 'spec/lib/gitlab/auth/request_authenticator_spec.rb'
+ - 'spec/lib/gitlab/auth_spec.rb'
+ - 'spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb'
+ - 'spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb'
+ - 'spec/lib/gitlab/checks/diff_check_spec.rb'
+ - 'spec/lib/gitlab/checks/lfs_check_spec.rb'
+ - 'spec/lib/gitlab/checks/lfs_integrity_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/base_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/local_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/processor_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/build_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/command_spec.rb'
+ - 'spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb'
+ - 'spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb'
+ - 'spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb'
+ - 'spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb'
+ - 'spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb'
+ - 'spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb'
+ - 'spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb'
+ - 'spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb'
+ - 'spec/lib/gitlab/ci/templates/npm_spec.rb'
+ - 'spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb'
+ - 'spec/lib/gitlab/ci/trace_spec.rb'
+ - 'spec/lib/gitlab/current_settings_spec.rb'
+ - 'spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb'
+ - 'spec/lib/gitlab/database/multi_threaded_migration_spec.rb'
+ - 'spec/lib/gitlab/diff/highlight_cache_spec.rb'
+ - 'spec/lib/gitlab/diff/highlight_spec.rb'
+ - 'spec/lib/gitlab/diff/position_spec.rb'
+ - 'spec/lib/gitlab/email/handler/create_issue_handler_spec.rb'
+ - 'spec/lib/gitlab/email/handler/create_note_handler_spec.rb'
+ - 'spec/lib/gitlab/etag_caching/middleware_spec.rb'
+ - 'spec/lib/gitlab/exclusive_lease_helpers_spec.rb'
+ - 'spec/lib/gitlab/fogbugz_import/importer_spec.rb'
+ - 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
+ - 'spec/lib/gitlab/git/repository_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/blob_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/commit_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/health_check_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/operation_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/praefect_info_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/ref_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/remote_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/repository_service_spec.rb'
+ - 'spec/lib/gitlab/gitaly_client/wiki_service_spec.rb'
+ - 'spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb'
+ - 'spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb'
+ - 'spec/lib/gitlab/hashed_storage/migrator_spec.rb'
+ - 'spec/lib/gitlab/import/merge_request_helpers_spec.rb'
+ - 'spec/lib/gitlab/import_export/config_spec.rb'
+ - 'spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb'
+ - 'spec/lib/gitlab/import_export/importer_spec.rb'
+ - 'spec/lib/gitlab/import_export/lfs_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/tree_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/version_checker_spec.rb'
+ - 'spec/lib/gitlab/job_waiter_spec.rb'
+ - 'spec/lib/gitlab/legacy_github_import/importer_spec.rb'
+ - 'spec/lib/gitlab/legacy_github_import/project_creator_spec.rb'
+ - 'spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb'
+ - 'spec/lib/gitlab/metrics/rack_middleware_spec.rb'
+ - 'spec/lib/gitlab/metrics/subscribers/active_record_spec.rb'
+ - 'spec/lib/gitlab/metrics_spec.rb'
+ - 'spec/lib/gitlab/patch/action_dispatch_journey_formatter_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware_spec.rb'
+ - 'spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb'
+ - 'spec/lib/gitlab/tracking/destinations/snowplow_spec.rb'
+ - 'spec/lib/gitlab/tracking_spec.rb'
+ - 'spec/lib/gitlab/usage_data_spec.rb'
+ - 'spec/lib/gitlab/workhorse_spec.rb'
+ - 'spec/lib/gitlab/x509/commit_spec.rb'
+ - 'spec/lib/gitlab/x509/signature_spec.rb'
+ - 'spec/lib/google_api/cloud_platform/client_spec.rb'
+ - 'spec/lib/json_web_token/rsa_token_spec.rb'
+ - 'spec/lib/mattermost/command_spec.rb'
+ - 'spec/lib/mattermost/team_spec.rb'
+ - 'spec/lib/system_check/simple_executor_spec.rb'
+ - 'spec/models/ci/build_spec.rb'
+ - 'spec/models/ci/runner_spec.rb'
+ - 'spec/models/commit_spec.rb'
+ - 'spec/models/environment_spec.rb'
+ - 'spec/models/group_spec.rb'
+ - 'spec/models/hooks/service_hook_spec.rb'
+ - 'spec/models/hooks/system_hook_spec.rb'
+ - 'spec/models/hooks/web_hook_spec.rb'
+ - 'spec/models/issue_spec.rb'
+ - 'spec/models/key_spec.rb'
+ - 'spec/models/member_spec.rb'
+ - 'spec/models/merge_request_diff_spec.rb'
+ - 'spec/models/merge_request_spec.rb'
+ - 'spec/models/note_spec.rb'
+ - 'spec/models/project_import_state_spec.rb'
+ - 'spec/models/project_services/jira_service_spec.rb'
+ - 'spec/models/project_services/mattermost_slash_commands_service_spec.rb'
+ - 'spec/models/project_spec.rb'
+ - 'spec/models/repository_spec.rb'
+ - 'spec/models/user_spec.rb'
+ - 'spec/models/x509_certificate_spec.rb'
+ - 'spec/policies/ci/build_policy_spec.rb'
+ - 'spec/policies/ci/pipeline_policy_spec.rb'
+ - 'spec/presenters/gitlab/blame_presenter_spec.rb'
+ - 'spec/presenters/merge_request_presenter_spec.rb'
+ - 'spec/requests/api/api_spec.rb'
+ - 'spec/requests/api/ci/runner/jobs_artifacts_spec.rb'
+ - 'spec/requests/api/ci/runner/jobs_put_spec.rb'
+ - 'spec/requests/api/ci/runner/jobs_request_post_spec.rb'
+ - 'spec/requests/api/ci/runner/jobs_trace_spec.rb'
+ - 'spec/requests/api/ci/runner/runners_delete_spec.rb'
+ - 'spec/requests/api/ci/runner/runners_post_spec.rb'
+ - 'spec/requests/api/ci/runner/runners_verify_post_spec.rb'
+ - 'spec/requests/api/graphql/gitlab_schema_spec.rb'
+ - 'spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb'
+ - 'spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb'
+ - 'spec/requests/api/graphql_spec.rb'
+ - 'spec/requests/api/helpers_spec.rb'
+ - 'spec/requests/api/internal/base_spec.rb'
+ - 'spec/requests/api/maven_packages_spec.rb'
+ - 'spec/requests/api/merge_requests_spec.rb'
+ - 'spec/requests/api/pages/pages_spec.rb'
+ - 'spec/requests/api/project_export_spec.rb'
+ - 'spec/requests/api/project_import_spec.rb'
+ - 'spec/requests/api/projects_spec.rb'
+ - 'spec/requests/api/snippets_spec.rb'
+ - 'spec/requests/api/todos_spec.rb'
+ - 'spec/requests/git_http_spec.rb'
+ - 'spec/requests/import/gitlab_projects_controller_spec.rb'
+ - 'spec/routing/routing_spec.rb'
+ - 'spec/serializers/analytics_stage_serializer_spec.rb'
+ - 'spec/serializers/merge_request_poll_cached_widget_entity_spec.rb'
+ - 'spec/serializers/merge_request_poll_widget_entity_spec.rb'
+ - 'spec/services/application_settings/update_service_spec.rb'
+ - 'spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb'
+ - 'spec/services/boards/lists/update_service_spec.rb'
+ - 'spec/services/ci/create_pipeline_service_spec.rb'
+ - 'spec/services/ci/destroy_expired_job_artifacts_service_spec.rb'
+ - 'spec/services/ci/expire_pipeline_cache_service_spec.rb'
+ - 'spec/services/ci/list_config_variables_service_spec.rb'
+ - 'spec/services/ci/register_job_service_spec.rb'
+ - 'spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb'
+ - 'spec/services/ci/retry_build_service_spec.rb'
+ - 'spec/services/ci/retry_pipeline_service_spec.rb'
+ - 'spec/services/ci/stop_environments_service_spec.rb'
+ - 'spec/services/clusters/applications/create_service_spec.rb'
+ - 'spec/services/clusters/cleanup/project_namespace_service_spec.rb'
+ - 'spec/services/clusters/cleanup/service_account_service_spec.rb'
+ - 'spec/services/deployments/older_deployments_drop_service_spec.rb'
+ - 'spec/services/deployments/update_environment_service_spec.rb'
+ - 'spec/services/draft_notes/destroy_service_spec.rb'
+ - 'spec/services/events/render_service_spec.rb'
+ - 'spec/services/git/branch_push_service_spec.rb'
+ - 'spec/services/git/process_ref_changes_service_spec.rb'
+ - 'spec/services/groups/create_service_spec.rb'
+ - 'spec/services/groups/update_service_spec.rb'
+ - 'spec/services/integrations/test/project_service_spec.rb'
+ - 'spec/services/issuable/destroy_service_spec.rb'
+ - 'spec/services/issues/close_service_spec.rb'
+ - 'spec/services/issues/reopen_service_spec.rb'
+ - 'spec/services/members/destroy_service_spec.rb'
+ - 'spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb'
+ - 'spec/services/merge_requests/build_service_spec.rb'
+ - 'spec/services/merge_requests/merge_service_spec.rb'
+ - 'spec/services/merge_requests/mergeability_check_service_spec.rb'
+ - 'spec/services/merge_requests/refresh_service_spec.rb'
+ - 'spec/services/merge_requests/reload_diffs_service_spec.rb'
+ - 'spec/services/merge_requests/resolved_discussion_notification_service_spec.rb'
+ - 'spec/services/metrics/dashboard/custom_dashboard_service_spec.rb'
+ - 'spec/services/metrics/dashboard/transient_embed_service_spec.rb'
+ - 'spec/services/notes/create_service_spec.rb'
+ - 'spec/services/notes/render_service_spec.rb'
+ - 'spec/services/packages/conan/create_package_file_service_spec.rb'
+ - 'spec/services/packages/nuget/metadata_extraction_service_spec.rb'
+ - 'spec/services/packages/nuget/update_package_from_metadata_service_spec.rb'
+ - 'spec/services/pages/delete_services_spec.rb'
+ - 'spec/services/pod_logs/elasticsearch_service_spec.rb'
+ - 'spec/services/pod_logs/kubernetes_service_spec.rb'
+ - 'spec/services/post_receive_service_spec.rb'
+ - 'spec/services/projects/after_rename_service_spec.rb'
+ - 'spec/services/projects/container_repository/cleanup_tags_service_spec.rb'
+ - 'spec/services/projects/container_repository/delete_tags_service_spec.rb'
+ - 'spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb'
+ - 'spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb'
+ - 'spec/services/projects/destroy_service_spec.rb'
+ - 'spec/services/projects/fork_service_spec.rb'
+ - 'spec/services/projects/import_service_spec.rb'
+ - 'spec/services/projects/lfs_pointers/lfs_download_service_spec.rb'
+ - 'spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb'
+ - 'spec/services/projects/prometheus/alerts/notify_service_spec.rb'
+ - 'spec/services/projects/transfer_service_spec.rb'
+ - 'spec/services/projects/update_remote_mirror_service_spec.rb'
+ - 'spec/services/projects/update_service_spec.rb'
+ - 'spec/services/projects/update_statistics_service_spec.rb'
+ - 'spec/services/resource_events/change_labels_service_spec.rb'
+ - 'spec/services/search_service_spec.rb'
+ - 'spec/services/snippets/create_service_spec.rb'
+ - 'spec/services/test_hooks/project_service_spec.rb'
+ - 'spec/services/test_hooks/system_service_spec.rb'
+ - 'spec/services/todo_service_spec.rb'
+ - 'spec/services/users/destroy_service_spec.rb'
+ - 'spec/services/users/migrate_to_ghost_user_service_spec.rb'
+ - 'spec/spec_helper.rb'
+ - 'spec/support/capybara.rb'
+ - 'spec/support/helpers/api_helpers.rb'
+ - 'spec/support/helpers/graphql_helpers.rb'
+ - 'spec/support/helpers/ldap_helpers.rb'
+ - 'spec/support/helpers/login_helpers.rb'
+ - 'spec/support/helpers/metrics_dashboard_url_helpers.rb'
+ - 'spec/support/helpers/rake_helpers.rb'
+ - 'spec/support/helpers/stub_configuration.rb'
+ - 'spec/support/helpers/stub_gitlab_calls.rb'
+ - 'spec/support/helpers/test_env.rb'
+ - 'spec/support/import_export/common_util.rb'
+ - 'spec/support/services/migrate_to_ghost_user_service_shared_examples.rb'
+ - 'spec/support/shared_contexts/email_shared_context.rb'
+ - 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb'
+ - 'spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb'
+ - 'spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb'
+ - 'spec/support/shared_examples/controllers/issuables_requiring_filter_shared_examples.rb'
+ - 'spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb'
+ - 'spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb'
+ - 'spec/support/shared_examples/controllers/unique_visits_shared_examples.rb'
+ - 'spec/support/shared_examples/controllers/update_invalid_issuable_shared_examples.rb'
+ - 'spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb'
+ - 'spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb'
+ - 'spec/support/shared_examples/features/snippets_shared_examples.rb'
+ - 'spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb'
+ - 'spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb'
+ - 'spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb'
+ - 'spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb'
+ - 'spec/support/shared_examples/models/mentionable_shared_examples.rb'
+ - 'spec/support/shared_examples/models/with_uploads_shared_examples.rb'
+ - 'spec/support/shared_examples/path_extraction_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/discussions_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/api/snippets_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/rack_attack_shared_examples.rb'
+ - 'spec/support/shared_examples/requests/snippet_shared_examples.rb'
+ - 'spec/support/shared_examples/services/alert_management_shared_examples.rb'
+ - 'spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb'
+ - 'spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb'
+ - 'spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb'
+ - 'spec/support/shared_examples/services/issuable_shared_examples.rb'
+ - 'spec/support/shared_examples/uploaders/object_storage_shared_examples.rb'
+ - 'spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb'
+ - 'spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb'
+ - 'spec/support/snowplow.rb'
+ - 'spec/support/unicorn.rb'
+ - 'spec/tasks/gitlab/cleanup_rake_spec.rb'
+ - 'spec/tasks/gitlab/container_registry_rake_spec.rb'
+ - 'spec/tasks/gitlab/db_rake_spec.rb'
+ - 'spec/tasks/gitlab/git_rake_spec.rb'
+ - 'spec/tasks/gitlab/praefect_rake_spec.rb'
+ - 'spec/tasks/gitlab/shell_rake_spec.rb'
+ - 'spec/tasks/gitlab/x509/update_rake_spec.rb'
+ - 'spec/uploaders/file_mover_spec.rb'
+ - 'spec/uploaders/records_uploads_spec.rb'
+ - 'spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb'
+ - 'spec/views/layouts/_head.html.haml_spec.rb'
+ - 'spec/views/projects/artifacts/_artifact.html.haml_spec.rb'
+ - 'spec/views/shared/runners/show.html.haml_spec.rb'
+ - 'spec/workers/archive_trace_worker_spec.rb'
+ - 'spec/workers/build_coverage_worker_spec.rb'
+ - 'spec/workers/build_hooks_worker_spec.rb'
+ - 'spec/workers/build_trace_sections_worker_spec.rb'
+ - 'spec/workers/ci/build_schedule_worker_spec.rb'
+ - 'spec/workers/ci/daily_build_group_report_results_worker_spec.rb'
+ - 'spec/workers/cluster_configure_istio_worker_spec.rb'
+ - 'spec/workers/cluster_provision_worker_spec.rb'
+ - 'spec/workers/clusters/cleanup/project_namespace_worker_spec.rb'
+ - 'spec/workers/clusters/cleanup/service_account_worker_spec.rb'
+ - 'spec/workers/concerns/project_import_options_spec.rb'
+ - 'spec/workers/create_commit_signature_worker_spec.rb'
+ - 'spec/workers/create_note_diff_file_worker_spec.rb'
+ - 'spec/workers/delete_diff_files_worker_spec.rb'
+ - 'spec/workers/email_receiver_worker_spec.rb'
+ - 'spec/workers/emails_on_push_worker_spec.rb'
+ - 'spec/workers/error_tracking_issue_link_worker_spec.rb'
+ - 'spec/workers/expire_pipeline_cache_worker_spec.rb'
+ - 'spec/workers/git_garbage_collect_worker_spec.rb'
+ - 'spec/workers/group_export_worker_spec.rb'
+ - 'spec/workers/group_import_worker_spec.rb'
+ - 'spec/workers/namespaceless_project_destroy_worker_spec.rb'
+ - 'spec/workers/namespaces/root_statistics_worker_spec.rb'
+ - 'spec/workers/new_note_worker_spec.rb'
+ - 'spec/workers/object_pool/create_worker_spec.rb'
+ - 'spec/workers/packages/nuget/extraction_worker_spec.rb'
+ - 'spec/workers/pages_remove_worker_spec.rb'
+ - 'spec/workers/pipeline_hooks_worker_spec.rb'
+ - 'spec/workers/pipeline_process_worker_spec.rb'
+ - 'spec/workers/pipeline_schedule_worker_spec.rb'
+ - 'spec/workers/project_cache_worker_spec.rb'
+ - 'spec/workers/stage_update_worker_spec.rb'
+ - 'spec/workers/stuck_ci_jobs_worker_spec.rb'
+ - 'spec/workers/wait_for_cluster_creation_worker_spec.rb'
+ - 'ee/spec/workers/security/auto_fix_worker_spec.rb'
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
new file mode 100644
index 00000000000..153c58b556e
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -0,0 +1,78 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
+import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
+import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql';
+import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql';
+import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql';
+import ImportTableRow from './import_table_row.vue';
+
+const mapApolloMutations = mutations =>
+ Object.fromEntries(
+ Object.entries(mutations).map(([key, mutation]) => [
+ key,
+ function mutate(config) {
+ return this.$apollo.mutate({
+ mutation,
+ ...config,
+ });
+ },
+ ]),
+ );
+
+export default {
+ components: {
+ GlLoadingIcon,
+ ImportTableRow,
+ },
+
+ apollo: {
+ bulkImportSourceGroups: bulkImportSourceGroupsQuery,
+ availableNamespaces: availableNamespacesQuery,
+ },
+
+ methods: {
+ ...mapApolloMutations({
+ setTargetNamespace: setTargetNamespaceMutation,
+ setNewName: setNewNameMutation,
+ importGroup: importGroupMutation,
+ }),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
+ <div v-else-if="bulkImportSourceGroups.length">
+ <table class="gl-w-full">
+ <thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1">
+ <th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th>
+ <th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th>
+ <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th>
+ <th class="gl-py-4 import-jobs-cta-col"></th>
+ </thead>
+ <tbody>
+ <template v-for="group in bulkImportSourceGroups">
+ <import-table-row
+ :key="group.id"
+ :group="group"
+ :available-namespaces="availableNamespaces"
+ @update-target-namespace="
+ setTargetNamespace({
+ variables: { sourceGroupId: group.id, targetNamespace: $event },
+ })
+ "
+ @update-new-name="
+ setNewName({
+ variables: { sourceGroupId: group.id, newName: $event },
+ })
+ "
+ @import-group="importGroup({ variables: { sourceGroupId: group.id } })"
+ />
+ </template>
+ </tbody>
+ </table>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
new file mode 100644
index 00000000000..07603d89f0f
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
@@ -0,0 +1,106 @@
+<script>
+import { GlButton, GlIcon, GlLink, GlFormInput } from '@gitlab/ui';
+import { joinPaths } from '~/lib/utils/url_utility';
+import Select2Select from '~/vue_shared/components/select2_select.vue';
+import ImportStatus from '../../components/import_status.vue';
+import { STATUSES } from '../../constants';
+
+export default {
+ components: {
+ Select2Select,
+ ImportStatus,
+ GlButton,
+ GlLink,
+ GlIcon,
+ GlFormInput,
+ },
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ availableNamespaces: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ isDisabled() {
+ return this.group.status !== STATUSES.NONE;
+ },
+
+ isFinished() {
+ return this.group.status === STATUSES.FINISHED;
+ },
+
+ select2Options() {
+ return {
+ data: this.availableNamespaces.map(namespace => ({
+ id: namespace.full_path,
+ text: namespace.full_path,
+ })),
+ };
+ },
+ },
+ methods: {
+ getPath(group) {
+ return `${group.import_target.target_namespace}/${group.import_target.new_name}`;
+ },
+
+ getFullPath(group) {
+ return joinPaths(gon.relative_url_root || '/', this.getPath(group));
+ },
+ },
+};
+</script>
+
+<template>
+ <tr class="gl-border-gray-200 gl-border-0 gl-border-b-1">
+ <td class="gl-p-4">
+ <gl-link :href="group.web_url" target="_blank">
+ {{ group.full_path }} <gl-icon name="external-link" />
+ </gl-link>
+ </td>
+ <td class="gl-p-4">
+ <gl-link v-if="isFinished" :href="getFullPath(group)">{{ getPath(group) }}</gl-link>
+
+ <div
+ v-else
+ class="import-entities-target-select gl-display-flex gl-align-items-stretch"
+ :class="{
+ disabled: isDisabled,
+ }"
+ >
+ <select2-select
+ :disabled="isDisabled"
+ :options="select2Options"
+ :value="group.import_target.target_namespace"
+ @input="$emit('update-target-namespace', $event)"
+ />
+ <div
+ class="import-entities-target-select-separator gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
+ >
+ /
+ </div>
+ <gl-form-input
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
+ :disabled="isDisabled"
+ :value="group.import_target.new_name"
+ @input="$emit('update-new-name', $event)"
+ />
+ </div>
+ </td>
+ <td class="gl-p-4 gl-white-space-nowrap">
+ <import-status :status="group.status" />
+ </td>
+ <td class="gl-p-4">
+ <gl-button
+ v-if="!isDisabled"
+ variant="success"
+ category="secondary"
+ @click="$emit('import-group')"
+ >{{ __('Import') }}</gl-button
+ >
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
new file mode 100644
index 00000000000..23f4190c2d0
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -0,0 +1,66 @@
+import axios from '~/lib/utils/axios_utils';
+import createDefaultClient from '~/lib/graphql';
+import { STATUSES } from '../../constants';
+import availableNamespacesQuery from './queries/available_namespaces.query.graphql';
+import { SourceGroupsManager } from './services/source_groups_manager';
+
+export const clientTypenames = {
+ BulkImportSourceGroup: 'ClientBulkImportSourceGroup',
+ AvailableNamespace: 'ClientAvailableNamespace',
+};
+
+export function createResolvers({ endpoints }) {
+ return {
+ Query: {
+ async bulkImportSourceGroups(_, __, { client }) {
+ const {
+ data: { availableNamespaces },
+ } = await client.query({ query: availableNamespacesQuery });
+
+ return axios.get(endpoints.status).then(({ data }) => {
+ return data.importable_data.map(group => ({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ ...group,
+ status: STATUSES.NONE,
+ import_target: {
+ new_name: group.full_path,
+ target_namespace: availableNamespaces[0].full_path,
+ },
+ }));
+ });
+ },
+
+ availableNamespaces: () =>
+ axios.get(endpoints.availableNamespaces).then(({ data }) =>
+ data.map(namespace => ({
+ __typename: clientTypenames.AvailableNamespace,
+ ...namespace,
+ })),
+ ),
+ },
+ Mutation: {
+ setTargetNamespace(_, { targetNamespace, sourceGroupId }, { client }) {
+ new SourceGroupsManager({ client }).updateById(sourceGroupId, sourceGroup => {
+ // eslint-disable-next-line no-param-reassign
+ sourceGroup.import_target.target_namespace = targetNamespace;
+ });
+ },
+
+ setNewName(_, { newName, sourceGroupId }, { client }) {
+ new SourceGroupsManager({ client }).updateById(sourceGroupId, sourceGroup => {
+ // eslint-disable-next-line no-param-reassign
+ sourceGroup.import_target.new_name = newName;
+ });
+ },
+
+ async importGroup(_, { sourceGroupId }, { client }) {
+ const groupManager = new SourceGroupsManager({ client });
+ const group = groupManager.findById(sourceGroupId);
+ groupManager.setImportStatus(group, STATUSES.SCHEDULING);
+ },
+ },
+ };
+}
+
+export const createApolloClient = ({ endpoints }) =>
+ createDefaultClient(createResolvers({ endpoints }), { assumeImmutableResults: true });
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
new file mode 100644
index 00000000000..50774e36599
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
@@ -0,0 +1,8 @@
+fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup {
+ id
+ web_url
+ full_path
+ full_name
+ status
+ import_target
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql
new file mode 100644
index 00000000000..412608d3faf
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql
@@ -0,0 +1,3 @@
+mutation importGroup($sourceGroupId: String!) {
+ importGroup(sourceGroupId: $sourceGroupId) @client
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql
new file mode 100644
index 00000000000..2bc19891401
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql
@@ -0,0 +1,3 @@
+mutation setNewName($newName: String!, $sourceGroupId: String!) {
+ setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql
new file mode 100644
index 00000000000..fc98a1652c1
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql
@@ -0,0 +1,3 @@
+mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) {
+ setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
new file mode 100644
index 00000000000..5ab9796b50a
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
@@ -0,0 +1,6 @@
+query availableNamespaces {
+ availableNamespaces @client {
+ id
+ full_path
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql
new file mode 100644
index 00000000000..8d52d94925c
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql
@@ -0,0 +1,7 @@
+#import "../fragments/bulk_import_source_group_item.fragment.graphql"
+
+query bulkImportSourceGroups {
+ bulkImportSourceGroups @client {
+ ...BulkImportSourceGroupItem
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
new file mode 100644
index 00000000000..f752ecc8cd6
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
@@ -0,0 +1,45 @@
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import produce from 'immer';
+import ImportSourceGroupFragment from '../fragments/bulk_import_source_group_item.fragment.graphql';
+
+function extractTypeConditionFromFragment(fragment) {
+ return fragment.definitions[0]?.typeCondition.name.value;
+}
+
+function generateGroupId(id) {
+ return defaultDataIdFromObject({
+ __typename: extractTypeConditionFromFragment(ImportSourceGroupFragment),
+ id,
+ });
+}
+
+export class SourceGroupsManager {
+ constructor({ client }) {
+ this.client = client;
+ }
+
+ findById(id) {
+ const cacheId = generateGroupId(id);
+ return this.client.readFragment({ fragment: ImportSourceGroupFragment, id: cacheId });
+ }
+
+ update(group, fn) {
+ this.client.writeFragment({
+ fragment: ImportSourceGroupFragment,
+ id: generateGroupId(group.id),
+ data: produce(group, fn),
+ });
+ }
+
+ updateById(id, fn) {
+ const group = this.findById(id);
+ this.update(group, fn);
+ }
+
+ setImportStatus(group, status) {
+ this.update(group, sourceGroup => {
+ // eslint-disable-next-line no-param-reassign
+ sourceGroup.status = status;
+ });
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
new file mode 100644
index 00000000000..bf427075564
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import Translate from '~/vue_shared/translate';
+import { createApolloClient } from './graphql/client_factory';
+import ImportTable from './components/import_table.vue';
+
+Vue.use(Translate);
+Vue.use(VueApollo);
+
+export function mountImportGroupsApp(mountElement) {
+ if (!mountElement) return undefined;
+
+ const { statusPath, availableNamespacesPath, createBulkImportPath } = mountElement.dataset;
+ const apolloProvider = new VueApollo({
+ defaultClient: createApolloClient({
+ endpoints: {
+ status: statusPath,
+ availableNamespaces: availableNamespacesPath,
+ createBulkImport: createBulkImportPath,
+ },
+ }),
+ });
+
+ return new Vue({
+ el: mountElement,
+ apolloProvider,
+ render(createElement) {
+ return createElement(ImportTable);
+ },
+ });
+}
diff --git a/app/assets/javascripts/pages/import/bulk_imports/index.js b/app/assets/javascripts/pages/import/bulk_imports/index.js
new file mode 100644
index 00000000000..37ac1a98466
--- /dev/null
+++ b/app/assets/javascripts/pages/import/bulk_imports/index.js
@@ -0,0 +1,4 @@
+import { mountImportGroupsApp } from '~/import_entities/import_groups';
+
+const mountElement = document.getElementById('import-groups-mount-element');
+mountImportGroupsApp(mountElement);
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 8b5bed08931..4bf837faed1 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -1,7 +1,7 @@
<script>
import dateFormat from 'dateformat';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql';
@@ -50,6 +50,7 @@ export default {
components: {
GlAlert,
GlColumnChart,
+ GlSkeletonLoader,
StatisticsList,
PipelinesAreaChart,
},
@@ -278,7 +279,8 @@ export default {
<h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
<div class="row">
<div class="col-md-6">
- <statistics-list :counts="formattedCounts" />
+ <gl-skeleton-loader v-if="$apollo.queries.counts.loading" :lines="5" />
+ <statistics-list v-else :counts="formattedCounts" />
</div>
<div class="col-md-6">
<strong>
diff --git a/app/assets/stylesheets/page_bundles/import.scss b/app/assets/stylesheets/page_bundles/import.scss
index 2a154393630..5f43d5df7e3 100644
--- a/app/assets/stylesheets/page_bundles/import.scss
+++ b/app/assets/stylesheets/page_bundles/import.scss
@@ -43,9 +43,33 @@
}
.import-entities-target-select {
+ &.disabled {
+ .import-entities-target-select-separator,
+ .select2-container.select2-container-disabled .select2-choice {
+ color: var(--gray-400, $gray-400);
+ border-color: var(--gray-100, $gray-100);
+ background-color: var(--gray-10, $gray-10);
+ }
+
+ .select2-container.select2-container-disabled .select2-choice .select2-arrow {
+ background-color: var(--gray-10, $gray-10);
+ }
+ }
+
+ .import-entities-target-select-separator {
+ border-color: var(--gray-200, $gray-200);
+ background-color: var(--gray-10, $gray-10);
+ }
+
.select2-container {
> .select2-choice {
+ .select2-arrow {
+ background-color: var(--white, $white);
+ }
+
border-color: var(--gray-200, $gray-200);
+ color: var(--gray-900, $gray-900) !important;
+ background-color: var(--white, $white) !important;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 65ab835c33c..1ae90edd8f7 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -186,6 +186,10 @@ module WikiActions
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
+ def git_access
+ render 'shared/wikis/git_access'
+ end
+
private
def container
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 8f794512486..d1486f765e4 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -6,7 +6,4 @@ class Projects::WikisController < Projects::ApplicationController
alias_method :container, :project
feature_category :wiki
-
- def git_access
- end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index f50b3297e7e..7866e3e3d9f 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -255,6 +255,7 @@ module ApplicationSettingsHelper
:password_authentication_enabled_for_git,
:performance_bar_allowed_group_path,
:performance_bar_enabled,
+ :personal_access_token_prefix,
:kroki_enabled,
:kroki_url,
:plantuml_enabled,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 007de5812ee..9b9db7f93fd 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -249,6 +249,12 @@ class ApplicationSetting < ApplicationRecord
validates :user_default_internal_regex, js_regex: true, allow_nil: true
+ validates :personal_access_token_prefix,
+ format: { with: /\A[a-zA-Z0-9_+=\/@:.-]+\z/,
+ message: _("can contain only letters of the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'") },
+ length: { maximum: 20, message: _('is too long (maximum is %{count} characters)') },
+ allow_blank: true
+
validates :commit_email_hostname, format: { with: /\A[^@]+\z/ }
validates :archive_builds_in_seconds,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 9c6f25d1986..105889a364a 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -104,6 +104,7 @@ module ApplicationSettingImplementation
password_authentication_enabled_for_git: true,
password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'],
performance_bar_allowed_group_id: nil,
+ personal_access_token_prefix: nil,
plantuml_enabled: false,
plantuml_url: nil,
polling_interval_multiplier: 1,
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index fbc02ed9cb0..462f09bf1b9 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -104,7 +104,6 @@ module Ci
accepts_nested_attributes_for :variables, reject_if: :persisted?
- delegate :id, to: :project, prefix: true
delegate :full_path, to: :project, prefix: true
validates :sha, presence: { unless: :importing? }
diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb
index abddbf1c7e3..31b5afd604d 100644
--- a/app/models/concerns/case_sensitivity.rb
+++ b/app/models/concerns/case_sensitivity.rb
@@ -11,12 +11,14 @@ module CaseSensitivity
def iwhere(params)
criteria = self
- params.each do |key, value|
+ params.each do |column, value|
+ column = arel_table[column] unless column.is_a?(Arel::Attribute)
+
criteria = case value
when Array
- criteria.where(value_in(key, value))
+ criteria.where(value_in(column, value))
else
- criteria.where(value_equal(key, value))
+ criteria.where(value_equal(column, value))
end
end
@@ -28,7 +30,7 @@ module CaseSensitivity
def value_equal(column, value)
lower_value = lower_value(value)
- lower_column(arel_table[column]).eq(lower_value).to_sql
+ lower_column(column).eq(lower_value).to_sql
end
def value_in(column, values)
@@ -36,7 +38,7 @@ module CaseSensitivity
lower_value(value)
end
- lower_column(arel_table[column]).in(lower_values).to_sql
+ lower_column(column).in(lower_values).to_sql
end
def lower_value(value)
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index c70ce9bebcc..71d8e06de76 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -4,6 +4,36 @@
# Object must have name and path db fields and respond to parent and parent_changed? methods.
module Routable
extend ActiveSupport::Concern
+ include CaseSensitivity
+
+ # Finds a Routable object by its full path, without knowing the class.
+ #
+ # Usage:
+ #
+ # Routable.find_by_full_path('groupname') # -> Group
+ # Routable.find_by_full_path('groupname/projectname') # -> Project
+ #
+ # Returns a single object, or nil.
+ def self.find_by_full_path(path, follow_redirects: false, route_scope: Route, redirect_route_scope: RedirectRoute)
+ return unless path.present?
+
+ # Case sensitive match first (it's cheaper and the usual case)
+ # If we didn't have an exact match, we perform a case insensitive search
+ #
+ # We need to qualify the columns with the table name, to support both direct lookups on
+ # Route/RedirectRoute, and scoped lookups through the Routable classes.
+ route =
+ route_scope.find_by(routes: { path: path }) ||
+ route_scope.iwhere(Route.arel_table[:path] => path).take
+
+ if follow_redirects
+ route ||= redirect_route_scope.iwhere(RedirectRoute.arel_table[:path] => path).take
+ end
+
+ return unless route
+
+ route.is_a?(Routable) ? route : route.source
+ end
included do
# Remove `inverse_of: source` when upgraded to rails 5.2
@@ -30,15 +60,14 @@ module Routable
#
# Returns a single object, or nil.
def find_by_full_path(path, follow_redirects: false)
- # Case sensitive match first (it's cheaper and the usual case)
- # If we didn't have an exact match, we perform a case insensitive search
- found = includes(:route).find_by(routes: { path: path }) || where_full_path_in([path]).take
-
- return found if found
-
- if follow_redirects
- joins(:redirect_routes).find_by("LOWER(redirect_routes.path) = LOWER(?)", path)
- end
+ # TODO: Optimize these queries by avoiding joins
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/292252
+ Routable.find_by_full_path(
+ path,
+ follow_redirects: follow_redirects,
+ route_scope: includes(:route).references(:routes),
+ redirect_route_scope: joins(:redirect_routes)
+ )
end
# Builds a relation to find multiple objects by their full paths.
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index a1f83884f02..535cf25eb9d 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -57,6 +57,13 @@ module TokenAuthenticatable
token = read_attribute(token_field)
token.present? && ActiveSupport::SecurityUtils.secure_compare(other_token, token)
end
+
+ # Base strategy delegates to this method for formatting a token before
+ # calling set_token. Can be overridden in models to e.g. add a prefix
+ # to the tokens
+ mod.define_method("format_#{token_field}") do |token|
+ token
+ end
end
def token_authenticatable_module
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index aafd0b538a3..f72a41f06b1 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -18,10 +18,15 @@ module TokenAuthenticatableStrategies
raise NotImplementedError
end
- def set_token(instance)
+ def set_token(instance, token)
raise NotImplementedError
end
+ # Default implementation returns the token as-is
+ def format_token(instance, token)
+ instance.send("format_#{@token_field}", token) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
def ensure_token(instance)
write_new_token(instance) unless token_set?(instance)
get_token(instance)
@@ -57,7 +62,8 @@ module TokenAuthenticatableStrategies
def write_new_token(instance)
new_token = generate_available_token
- set_token(instance, new_token)
+ formatted_token = format_token(instance, new_token)
+ set_token(instance, formatted_token)
end
def unique
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 5aa5f2c842b..3b07551fe05 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -9,7 +9,9 @@ class PersonalAccessToken < ApplicationRecord
add_authentication_token_field :token, digest: true
REDIS_EXPIRY_TIME = 3.minutes
- TOKEN_LENGTH = 20
+
+ # PATs are 20 characters + optional configurable settings prefix (0..20)
+ TOKEN_LENGTH_RANGE = (20..40).freeze
serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize
@@ -77,6 +79,15 @@ class PersonalAccessToken < ApplicationRecord
)
end
+ def self.token_prefix
+ Gitlab::CurrentSettings.current_application_settings.personal_access_token_prefix
+ end
+
+ override :format_token
+ def format_token(token)
+ "#{self.class.token_prefix}#{token}"
+ end
+
protected
def validate_scopes
diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb
index 22f60802257..749f4a87818 100644
--- a/app/models/redirect_route.rb
+++ b/app/models/redirect_route.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class RedirectRoute < ApplicationRecord
+ include CaseSensitivity
+
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :source, presence: true
diff --git a/app/models/route.rb b/app/models/route.rb
index fe4846b3be5..fcc8459d6e5 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -4,7 +4,7 @@ class Route < ApplicationRecord
include CaseSensitivity
include Gitlab::SQL::Pattern
- belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :source, polymorphic: true, inverse_of: :route # rubocop:disable Cop/PolymorphicAssociations
validates :source, presence: true
validates :path,
diff --git a/app/models/user.rb b/app/models/user.rb
index bccc6aa6ed2..f1c7644901a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1665,7 +1665,7 @@ class User < ApplicationRecord
save
end
- # each existing user needs to have an `feed_token`.
+ # each existing user needs to have a `feed_token`.
# we do this on read since migrating all existing users is not a feasible
# solution.
def feed_token
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index f46eb84ce8e..46155f3f670 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -52,6 +52,9 @@
= link_to _('More information'), help_page_path('user/permissions', anchor: 'setting-new-users-to-external'),
target: '_blank'
.form-group
+ = f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light'
+ = f.text_field :personal_access_token_prefix, placeholder: _('Max 20 characters'), class: 'form-control'
+ .form-group
= f.label :user_show_add_ssh_key_message, _('Prompt users to upload SSH keys'), class: 'label-bold'
.form-check
= f.check_box :user_show_add_ssh_key_message, class: 'form-check-input'
diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml
index e6511b46bbc..80b96a25ebb 100644
--- a/app/views/import/bulk_imports/status.html.haml
+++ b/app/views/import/bulk_imports/status.html.haml
@@ -6,3 +6,7 @@
= s_('ImportGroups|Import groups from GitLab')
%p.gl-my-0.gl-py-5.gl-border-solid.gl-border-gray-200.gl-border-0.gl-border-b-1
= s_('ImportGroups|Importing groups from %{link}').html_safe % { link: external_link(@source_url, @source_url) }
+
+#import-groups-mount-element{ data: { status_path: status_import_bulk_imports_path(format: :json),
+ available_namespaces_path: import_available_namespaces_path(format: :json),
+ create_bulk_import_path: import_bulk_imports_path(format: :json) } }
diff --git a/app/views/shared/wikis/_sidebar.html.haml b/app/views/shared/wikis/_sidebar.html.haml
index 7022762840e..a906bf7aa63 100644
--- a/app/views/shared/wikis/_sidebar.html.haml
+++ b/app/views/shared/wikis/_sidebar.html.haml
@@ -4,11 +4,10 @@
%a.gutter-toggle.float-right.d-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-right', css_class: 'gl-icon')
- - if @wiki.container.is_a?(Project)
- - git_access_url = wiki_path(@wiki, action: :git_access)
- = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do
- = sprite_icon('download', css_class: 'gl-mr-2')
- %span= _("Clone repository")
+ - git_access_url = wiki_path(@wiki, action: :git_access)
+ = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do
+ = sprite_icon('download', css_class: 'gl-mr-2')
+ %span= _("Clone repository")
- if @sidebar_error.present?
= render 'shared/alert_info', body: s_('Wiki|The sidebar failed to load. You can reload the page to try again.')
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/shared/wikis/git_access.html.haml
index 2542860c742..2542860c742 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/shared/wikis/git_access.html.haml
diff --git a/changelogs/unreleased/207869-group-wikis-git-support.yml b/changelogs/unreleased/207869-group-wikis-git-support.yml
new file mode 100644
index 00000000000..9eb8f2f2434
--- /dev/null
+++ b/changelogs/unreleased/207869-group-wikis-git-support.yml
@@ -0,0 +1,5 @@
+---
+title: Support Git access for group wikis
+merge_request: 45892
+author:
+type: added
diff --git a/changelogs/unreleased/change_unique_index_on_security_findings.yml b/changelogs/unreleased/change_unique_index_on_security_findings.yml
new file mode 100644
index 00000000000..47360fd6394
--- /dev/null
+++ b/changelogs/unreleased/change_unique_index_on_security_findings.yml
@@ -0,0 +1,5 @@
+---
+title: Change the unique index on `security_findings` table
+merge_request: 50046
+author:
+type: changed
diff --git a/changelogs/unreleased/feat-token-prefix.yml b/changelogs/unreleased/feat-token-prefix.yml
new file mode 100644
index 00000000000..228880cf9e4
--- /dev/null
+++ b/changelogs/unreleased/feat-token-prefix.yml
@@ -0,0 +1,5 @@
+---
+title: Configurable personal access token prefix
+merge_request: 20968
+author: 'Max Wittig & Diego Louzán'
+type: added
diff --git a/changelogs/unreleased/georgekoltsov-update-project-imported-usage-ping.yml b/changelogs/unreleased/georgekoltsov-update-project-imported-usage-ping.yml
new file mode 100644
index 00000000000..1751500b2d3
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-update-project-imported-usage-ping.yml
@@ -0,0 +1,5 @@
+---
+title: Update projects_imported.total usage metric
+merge_request: 49568
+author:
+type: fixed
diff --git a/config/feature_flags/development/ci_allow_failure_with_exit_codes.yml b/config/feature_flags/development/ci_allow_failure_with_exit_codes.yml
new file mode 100644
index 00000000000..c2701705616
--- /dev/null
+++ b/config/feature_flags/development/ci_allow_failure_with_exit_codes.yml
@@ -0,0 +1,8 @@
+---
+name: ci_allow_failure_with_exit_codes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49145
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292024
+milestone: '13.7'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb
index f27fc2b79b3..715d4b5cc59 100644
--- a/config/routes/git_http.rb
+++ b/config/routes/git_http.rb
@@ -9,7 +9,7 @@ scope(path: '*repository_path', format: false) do
end
# NOTE: LFS routes are exposed on all repository types, but we still check for
- # LFS availability on the repository container in LfsRequest#require_lfs_enabled!
+ # LFS availability on the repository container in LfsRequest#lfs_check_access!
# Git LFS API (metadata)
scope(path: 'info/lfs/objects', controller: :lfs_api) do
diff --git a/db/migrate/20201119133534_add_personal_access_token_prefix_to_application_setting.rb b/db/migrate/20201119133534_add_personal_access_token_prefix_to_application_setting.rb
new file mode 100644
index 00000000000..c6bb6b7d514
--- /dev/null
+++ b/db/migrate/20201119133534_add_personal_access_token_prefix_to_application_setting.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AddPersonalAccessTokenPrefixToApplicationSetting < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # limit is added in 20201119133604_add_text_limit_to_application_setting_personal_access_token_prefix
+ def change
+ add_column :application_settings, :personal_access_token_prefix, :text
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+end
diff --git a/db/migrate/20201119133604_add_text_limit_to_application_setting_personal_access_token_prefix.rb b/db/migrate/20201119133604_add_text_limit_to_application_setting_personal_access_token_prefix.rb
new file mode 100644
index 00000000000..a118da9e3e7
--- /dev/null
+++ b/db/migrate/20201119133604_add_text_limit_to_application_setting_personal_access_token_prefix.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddTextLimitToApplicationSettingPersonalAccessTokenPrefix < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :application_settings, :personal_access_token_prefix, 20
+ end
+
+ def down
+ remove_text_limit :application_settings, :personal_access_token_prefix
+ end
+end
diff --git a/db/migrate/20201210101250_add_index_projects_on_import_type_and_creator_id.rb b/db/migrate/20201210101250_add_index_projects_on_import_type_and_creator_id.rb
new file mode 100644
index 00000000000..5eb8f1d658e
--- /dev/null
+++ b/db/migrate/20201210101250_add_index_projects_on_import_type_and_creator_id.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIndexProjectsOnImportTypeAndCreatorId < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :projects, [:creator_id, :import_type, :created_at],
+ where: 'import_type IS NOT NULL',
+ name: 'index_projects_on_creator_id_import_type_and_created_at_partial'
+ end
+
+ def down
+ remove_concurrent_index_by_name :projects, 'index_projects_on_creator_id_import_type_and_created_at_partial'
+ end
+end
diff --git a/db/migrate/20201215132151_change_unique_index_on_security_findings.rb b/db/migrate/20201215132151_change_unique_index_on_security_findings.rb
new file mode 100644
index 00000000000..fe474ef3991
--- /dev/null
+++ b/db/migrate/20201215132151_change_unique_index_on_security_findings.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class ChangeUniqueIndexOnSecurityFindings < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ OLD_INDEX_NAME = 'index_security_findings_on_uuid'
+ NEW_INDEX_NAME = 'index_security_findings_on_uuid_and_scan_id'
+
+ disable_ddl_transaction!
+
+ class SecurityFinding < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'security_findings'
+ end
+
+ def up
+ add_concurrent_index :security_findings, [:uuid, :scan_id], unique: true, name: NEW_INDEX_NAME
+
+ remove_concurrent_index_by_name :security_findings, OLD_INDEX_NAME
+ end
+
+ def down
+ # It is very unlikely that we rollback this migration but just in case if we have to,
+ # we have to clear the table because there can be multiple records with the same UUID
+ # which would break the creation of unique index on the `uuid` column.
+ # We choose clearing the table because just removing the duplicated records would
+ # cause data inconsistencies.
+ SecurityFinding.each_batch(of: 10000) { |relation| relation.delete_all }
+
+ add_concurrent_index :security_findings, :uuid, unique: true, name: OLD_INDEX_NAME
+
+ remove_concurrent_index_by_name :security_findings, NEW_INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20201119133534 b/db/schema_migrations/20201119133534
new file mode 100644
index 00000000000..b3999923381
--- /dev/null
+++ b/db/schema_migrations/20201119133534
@@ -0,0 +1 @@
+6c8fc7904f50a792e10b5f1b0abe90ba21b1bdfd47430b3caa0df870c0a24079 \ No newline at end of file
diff --git a/db/schema_migrations/20201119133604 b/db/schema_migrations/20201119133604
new file mode 100644
index 00000000000..865ce7db9e7
--- /dev/null
+++ b/db/schema_migrations/20201119133604
@@ -0,0 +1 @@
+bfb8ac3b697675bd4fca53273c6c6feb2f7a5659cbdaf57b9b4adb3e189b74ad \ No newline at end of file
diff --git a/db/schema_migrations/20201210101250 b/db/schema_migrations/20201210101250
new file mode 100644
index 00000000000..4657c9f264e
--- /dev/null
+++ b/db/schema_migrations/20201210101250
@@ -0,0 +1 @@
+734ef1c319549df72bbbfe3acf93ca05f7a6c5547a1efdcaba780195181f5f9a \ No newline at end of file
diff --git a/db/schema_migrations/20201215132151 b/db/schema_migrations/20201215132151
new file mode 100644
index 00000000000..e051fb91e12
--- /dev/null
+++ b/db/schema_migrations/20201215132151
@@ -0,0 +1 @@
+916f29e6ab89551fd785c3a8584c24b72d9002ada30d159e9ff826cb247199b5 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index df69c8fdfe6..81c21fc0c17 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9372,11 +9372,13 @@ CREATE TABLE application_settings (
secret_detection_revocation_token_types_url text,
cloud_license_enabled boolean DEFAULT false NOT NULL,
disable_feed_token boolean DEFAULT false NOT NULL,
+ personal_access_token_prefix text,
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
CONSTRAINT check_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)),
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT check_57123c9593 CHECK ((char_length(help_page_documentation_base_url) <= 255)),
+ CONSTRAINT check_718b4458ae CHECK ((char_length(personal_access_token_prefix) <= 20)),
CONSTRAINT check_85a39b68ff CHECK ((char_length(encrypted_ci_jwt_signing_key_iv) <= 255)),
CONSTRAINT check_9a719834eb CHECK ((char_length(secret_detection_token_revocation_url) <= 255)),
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
@@ -22275,6 +22277,8 @@ CREATE INDEX index_projects_on_creator_id_and_created_at_and_id ON projects USIN
CREATE INDEX index_projects_on_creator_id_and_id ON projects USING btree (creator_id, id);
+CREATE INDEX index_projects_on_creator_id_import_type_and_created_at_partial ON projects USING btree (creator_id, import_type, created_at) WHERE (import_type IS NOT NULL);
+
CREATE INDEX index_projects_on_description_trigram ON projects USING gin (description gin_trgm_ops);
CREATE INDEX index_projects_on_id_and_archived_and_pending_delete ON projects USING btree (id) WHERE ((archived = false) AND (pending_delete = false));
@@ -22527,7 +22531,7 @@ CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING bt
CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity);
-CREATE UNIQUE INDEX index_security_findings_on_uuid ON security_findings USING btree (uuid);
+CREATE UNIQUE INDEX index_security_findings_on_uuid_and_scan_id ON security_findings USING btree (uuid, scan_id);
CREATE INDEX index_self_managed_prometheus_alert_events_on_environment_id ON self_managed_prometheus_alert_events USING btree (environment_id);
diff --git a/doc/api/settings.md b/doc/api/settings.md
index f9c1a73addf..5680687e87e 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -28,61 +28,62 @@ Example response:
```json
{
- "default_projects_limit" : 100000,
- "signup_enabled" : true,
- "id" : 1,
- "default_branch_protection" : 2,
- "restricted_visibility_levels" : [],
- "password_authentication_enabled_for_web" : true,
- "after_sign_out_path" : null,
- "max_attachment_size" : 10,
- "max_import_size": 50,
- "user_oauth_applications" : true,
- "updated_at" : "2016-01-04T15:44:55.176Z",
- "session_expire_delay" : 10080,
- "home_page_url" : null,
- "default_snippet_visibility" : "private",
- "outbound_local_requests_whitelist": [],
- "domain_allowlist" : [],
- "domain_denylist_enabled" : false,
- "domain_denylist" : [],
- "created_at" : "2016-01-04T15:44:55.176Z",
- "default_ci_config_path" : null,
- "default_project_visibility" : "private",
- "default_group_visibility" : "private",
- "gravatar_enabled" : true,
- "sign_in_text" : null,
- "container_expiration_policies_enable_historic_entries": true,
- "container_registry_token_expire_delay": 5,
- "repository_storages_weighted": {"default": 100},
- "plantuml_enabled": false,
- "plantuml_url": null,
- "kroki_enabled": false,
- "kroki_url": null,
- "terminal_max_session_time": 0,
- "polling_interval_multiplier": 1.0,
- "rsa_key_restriction": 0,
- "dsa_key_restriction": 0,
- "ecdsa_key_restriction": 0,
- "ed25519_key_restriction": 0,
- "first_day_of_week": 0,
- "enforce_terms": true,
- "terms": "Hello world!",
- "performance_bar_allowed_group_id": 42,
- "user_show_add_ssh_key_message": true,
- "local_markdown_version": 0,
- "allow_local_requests_from_hooks_and_services": true,
- "allow_local_requests_from_web_hooks_and_services": true,
- "allow_local_requests_from_system_hooks": false,
- "asset_proxy_enabled": true,
- "asset_proxy_url": "https://assets.example.com",
- "asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"],
- "npm_package_requests_forwarding": true,
- "snippet_size_limit": 52428800,
- "issues_create_limit": 300,
- "raw_blob_request_limit": 300,
- "wiki_page_max_content_bytes": 52428800,
- "require_admin_approval_after_user_signup": false
+ "default_projects_limit" : 100000,
+ "signup_enabled" : true,
+ "id" : 1,
+ "default_branch_protection" : 2,
+ "restricted_visibility_levels" : [],
+ "password_authentication_enabled_for_web" : true,
+ "after_sign_out_path" : null,
+ "max_attachment_size" : 10,
+ "max_import_size": 50,
+ "user_oauth_applications" : true,
+ "updated_at" : "2016-01-04T15:44:55.176Z",
+ "session_expire_delay" : 10080,
+ "home_page_url" : null,
+ "default_snippet_visibility" : "private",
+ "outbound_local_requests_whitelist": [],
+ "domain_allowlist" : [],
+ "domain_denylist_enabled" : false,
+ "domain_denylist" : [],
+ "created_at" : "2016-01-04T15:44:55.176Z",
+ "default_ci_config_path" : null,
+ "default_project_visibility" : "private",
+ "default_group_visibility" : "private",
+ "gravatar_enabled" : true,
+ "sign_in_text" : null,
+ "container_expiration_policies_enable_historic_entries": true,
+ "container_registry_token_expire_delay": 5,
+ "repository_storages_weighted": {"default": 100},
+ "plantuml_enabled": false,
+ "plantuml_url": null,
+ "kroki_enabled": false,
+ "kroki_url": null,
+ "terminal_max_session_time": 0,
+ "polling_interval_multiplier": 1.0,
+ "rsa_key_restriction": 0,
+ "dsa_key_restriction": 0,
+ "ecdsa_key_restriction": 0,
+ "ed25519_key_restriction": 0,
+ "first_day_of_week": 0,
+ "enforce_terms": true,
+ "terms": "Hello world!",
+ "performance_bar_allowed_group_id": 42,
+ "user_show_add_ssh_key_message": true,
+ "local_markdown_version": 0,
+ "allow_local_requests_from_hooks_and_services": true,
+ "allow_local_requests_from_web_hooks_and_services": true,
+ "allow_local_requests_from_system_hooks": false,
+ "asset_proxy_enabled": true,
+ "asset_proxy_url": "https://assets.example.com",
+ "asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"],
+ "npm_package_requests_forwarding": true,
+ "snippet_size_limit": 52428800,
+ "issues_create_limit": 300,
+ "raw_blob_request_limit": 300,
+ "wiki_page_max_content_bytes": 52428800,
+ "require_admin_approval_after_user_signup": false,
+ "personal_access_token_prefix": "GL-"
}
```
@@ -91,12 +92,12 @@ the `file_template_project_id`, `deletion_adjourned_period`, or the `geo_node_al
```json
{
- "id" : 1,
- "signup_enabled" : true,
- "file_template_project_id": 1,
- "geo_node_allowed_ips": "0.0.0.0/0, ::/0",
- "deletion_adjourned_period": 7,
- ...
+ "id" : 1,
+ "signup_enabled" : true,
+ "file_template_project_id": 1,
+ "geo_node_allowed_ips": "0.0.0.0/0, ::/0",
+ "deletion_adjourned_period": 7,
+ ...
}
```
@@ -174,7 +175,8 @@ Example response:
"issues_create_limit": 300,
"raw_blob_request_limit": 300,
"wiki_page_max_content_bytes": 52428800,
- "require_admin_approval_after_user_signup": false
+ "require_admin_approval_after_user_signup": false,
+ "personal_access_token_prefix": "GL-"
}
```
@@ -318,6 +320,7 @@ listed in the descriptions of the relevant settings.
| `performance_bar_allowed_group_id` | string | no | (Deprecated: Use `performance_bar_allowed_group_path` instead) Path of the group that is allowed to toggle the performance bar. |
| `performance_bar_allowed_group_path` | string | no | Path of the group that is allowed to toggle the performance bar. |
| `performance_bar_enabled` | boolean | no | (Deprecated: Pass `performance_bar_allowed_group_path: nil` instead) Allow enabling the performance bar. |
+| `personal_access_token_prefix` | string | no | Prefix for all generated personal access tokens. |
| `plantuml_enabled` | boolean | no | (**If enabled, requires:** `plantuml_url`) Enable PlantUML integration. Default is `false`. |
| `plantuml_url` | string | required by: `plantuml_enabled` | The PlantUML instance URL for integration. |
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. |
diff --git a/doc/architecture/blueprints/feature_flags_development/index.md b/doc/architecture/blueprints/feature_flags_development/index.md
index 6be582bb8af..c92b4113465 100644
--- a/doc/architecture/blueprints/feature_flags_development/index.md
+++ b/doc/architecture/blueprints/feature_flags_development/index.md
@@ -119,7 +119,9 @@ This work is being done as part of dedicated epic: [Improve internal usage of
Feature Flags](https://gitlab.com/groups/gitlab-org/-/epics/3551). This epic
describes a meta reasons for making these changes.
-## Who
+## [Who](#who)
+
+### Blueprint
Proposal:
@@ -140,4 +142,24 @@ DRIs:
| Leadership | Craig Gomes |
| Engineering | Kamil Trzciński |
+### [Stakeholders](#stakeholders)
+
+| Role | Person | Title
+|--------------------|-----------------------|--------------------------------------------------------------------|
+| Executive Sponsor | Christopher Lefelhocz | Senior Director of Development |
+| Facilitator | Darby Frey | Senior Engineering Manager, Verify |
+| DRI / Leadership | Craig Gomes | Backend Engineering Manager, Memory and Database |
+| DRI / Engineering | Kamil Trzciński | Distinguished Engineer, Ops and Enablement |
+| DRI / Product | Kenny Johnston | Senior Director of Product Management, Ops |
+| Functional Lead | Ricky Wiens | Backend Engineering Manager, Verify:Testing |
+| Functional Lead | Anthony Sandoval | Engineering Manager, Reliability |
+| Functional Lead | James Heimbuck | Senior Product Manager, Verify:Testing |
+| Member | Grzegorz Bizon | Staff Backend Engineer, Verify |
+| Member | Michelle Gill | Engineering Manager, Create:Source Code |
+| Member | Wayne Haber | Director of Engineering, Threat Management |
+| Member | Doug Stull | Senior Fullstack Engineer, Growth:Expansion |
+| Member | Andrew Fontaine | Senior Frontend Engineer, Release |
+| Member | Rémy Coutable | Staff Backend Engineer, Engineering Productivity |
+| Member | Marin Jankovski | Senior Engineering Manager, Infrastructure, Delivery & Scalability |
+
<!-- vale gitlab.Spelling = YES -->
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index b297a19ca30..9eb15c53b66 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -35,6 +35,25 @@ If you choose a size larger than what is currently configured for the web server
you will likely get errors. See the [troubleshooting section](#troubleshooting) for more
details.
+## Personal Access Token prefix
+
+You can set a global prefix for all generated Personal Access Tokens.
+
+A prefix can help you identify PATs visually, as well as with automation tools.
+
+### Setting a prefix
+
+Only a GitLab administrator can set the prefix, which is a global setting applied
+to any PAT generated in the system by any user:
+
+1. Navigate to **Admin Area > Settings > General**.
+1. Expand the **Account and limit** section.
+1. Fill in the **Personal Access Token prefix** field.
+1. Click **Save changes**.
+
+It is also possible to configure the prefix via the [settings API](../../../api/settings.md)
+using the `personal_access_token_prefix` field.
+
## Repository size limit **(STARTER ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/740) in [GitLab Enterprise Edition 8.12](https://about.gitlab.com/releases/2016/09/22/gitlab-8-12-released/#limit-project-size-ee).
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 7940276cd82..0eac10663ec 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -404,7 +404,7 @@ and above.
There are a few limitations compared to project wikis:
-- Local Git access is not supported yet.
+- Git LFS is not supported.
- Group wikis are not included in global search, group exports, backups, and Geo replication.
- Changes to group wikis don't show up in the group's activity feed.
- Group wikis [can't be moved](../../api/project_repository_storage_moves.md#limitations) using the project
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index e68c282030b..b3f09b431b0 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -103,6 +103,7 @@ module API
optional :performance_bar_allowed_group_id, type: String, desc: 'Deprecated: Use :performance_bar_allowed_group_path instead. Path of the group that is allowed to toggle the performance bar.' # support legacy names, can be removed in v6
optional :performance_bar_allowed_group_path, type: String, desc: 'Path of the group that is allowed to toggle the performance bar.'
optional :performance_bar_enabled, type: String, desc: 'Deprecated: Pass `performance_bar_allowed_group_path: nil` instead. Allow enabling the performance.' # support legacy names, can be removed in v6
+ optional :personal_access_token_prefix, type: String, desc: 'Prefix to prepend to all personal access tokens'
optional :kroki_enabled, type: Boolean, desc: 'Enable Kroki'
given kroki_enabled: ->(val) { val } do
requires :kroki_url, type: String, desc: 'The Kroki server URL'
diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb
index 223cb227c53..caa881eeeab 100644
--- a/lib/gitlab/auth/auth_finders.rb
+++ b/lib/gitlab/auth/auth_finders.rb
@@ -194,6 +194,10 @@ module Gitlab
def access_token
strong_memoize(:access_token) do
+ # The token can be a PAT or an OAuth (doorkeeper) token
+ # It is also possible that a PAT is encapsulated in a `Bearer` OAuth token
+ # (e.g. NPM client registry auth), this case will be properly handled
+ # by find_personal_access_token
find_oauth_access_token || find_personal_access_token
end
end
@@ -237,7 +241,7 @@ module Gitlab
end
def matches_personal_access_token_length?(token)
- token.length == PersonalAccessToken::TOKEN_LENGTH
+ PersonalAccessToken::TOKEN_LENGTH_RANGE.include?(token.length)
end
# Check if the request is GET/HEAD, or if CSRF token is valid.
diff --git a/lib/gitlab/ci/config/entry/allow_failure.rb b/lib/gitlab/ci/config/entry/allow_failure.rb
new file mode 100644
index 00000000000..de768c3a03b
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/allow_failure.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents allow_failure settings.
+ #
+ class AllowFailure < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Validatable
+
+ ALLOWED_KEYS = %i[exit_codes].freeze
+ attributes ALLOWED_KEYS
+
+ validations do
+ validates :config, hash_or_boolean: true
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :exit_codes, array_of_integers_or_integer: true, allow_nil: true
+ end
+
+ def value
+ @config[:exit_codes] = Array.wrap(exit_codes) if exit_codes.present?
+ @config
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/bridge.rb b/lib/gitlab/ci/config/entry/bridge.rb
index 70fcc1d586a..e8e2eef281e 100644
--- a/lib/gitlab/ci/config/entry/bridge.rb
+++ b/lib/gitlab/ci/config/entry/bridge.rb
@@ -22,6 +22,7 @@ module Gitlab
in: ALLOWED_WHEN,
message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
}
+ validates :allow_failure, boolean: true
end
validate on: :composed do
@@ -47,7 +48,7 @@ module Gitlab
inherit: false,
metadata: { allowed_needs: %i[job bridge] }
- attributes :when
+ attributes :when, :allow_failure
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -72,6 +73,10 @@ module Gitlab
def bridge_needs
needs_value[:bridge] if needs_value
end
+
+ def ignored?
+ allow_failure.nil? ? manual_action? : allow_failure
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 1ce7060df22..85e3514499c 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -31,6 +31,7 @@ module Gitlab
validates :dependencies, array_of_strings: true
validates :resource_group, type: String
+ validates :allow_failure, hash_or_boolean: true
end
validates :start_in, duration: { limit: '1 week' }, if: :delayed?
@@ -117,9 +118,14 @@ module Gitlab
description: 'Parallel configuration for this job.',
inherit: false
+ entry :allow_failure, ::Gitlab::Ci::Config::Entry::AllowFailure,
+ description: 'Indicates whether this job is allowed to fail or not.',
+ inherit: false
+
attributes :script, :tags, :when, :dependencies,
:needs, :retry, :parallel, :start_in,
- :interruptible, :timeout, :resource_group, :release
+ :interruptible, :timeout, :resource_group,
+ :release, :allow_failure
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -166,11 +172,32 @@ module Gitlab
release: release_value,
after_script: after_script_value,
ignore: ignored?,
+ allow_failure_criteria: allow_failure_criteria,
needs: needs_defined? ? needs_value : nil,
resource_group: resource_group,
scheduling_type: needs_defined? ? :dag : :stage
).compact
end
+
+ def ignored?
+ allow_failure_defined? ? static_allow_failure : manual_action?
+ end
+
+ private
+
+ def allow_failure_criteria
+ return unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled?
+
+ if allow_failure_defined? && allow_failure_value.is_a?(Hash)
+ allow_failure_value
+ end
+ end
+
+ def static_allow_failure
+ return false if allow_failure_value.is_a?(Hash)
+
+ allow_failure_value
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/processable.rb b/lib/gitlab/ci/config/entry/processable.rb
index c0315e5f901..5ef8cfbddb7 100644
--- a/lib/gitlab/ci/config/entry/processable.rb
+++ b/lib/gitlab/ci/config/entry/processable.rb
@@ -32,7 +32,6 @@ module Gitlab
with_options allow_nil: true do
validates :extends, array_of_strings_or_string: true
validates :rules, array_of_hashes: true
- validates :allow_failure, boolean: true
end
end
@@ -65,7 +64,7 @@ module Gitlab
inherit: false,
default: {}
- attributes :extends, :rules, :allow_failure
+ attributes :extends, :rules
end
def compose!(deps = nil)
@@ -141,10 +140,6 @@ module Gitlab
def manual_action?
self.when == 'manual'
end
-
- def ignored?
- allow_failure.nil? ? manual_action? : allow_failure
- end
end
end
end
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index 3c679dce9aa..c4dca298839 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -62,6 +62,10 @@ module Gitlab
def self.ci_pipeline_editor_page_enabled?(project)
::Feature.enabled?(:ci_pipeline_editor_page, project, default_enabled: false)
end
+
+ def self.allow_failure_with_exit_codes_enabled?
+ ::Feature.enabled?(:ci_allow_failure_with_exit_codes)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 91dbcc616ea..105221dd6af 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -60,6 +60,7 @@ module Gitlab
@seed_attributes
.deep_merge(pipeline_attributes)
.deep_merge(rules_attributes)
+ .deep_merge(allow_failure_criteria_attributes)
.deep_merge(cache_attributes)
end
@@ -154,9 +155,13 @@ module Gitlab
end
def rules_attributes
- return {} unless @using_rules
-
- rules_result.build_attributes
+ strong_memoize(:rules_attributes) do
+ if @using_rules
+ rules_result.build_attributes
+ else
+ {}
+ end
+ end
end
def rules_result
@@ -176,6 +181,17 @@ module Gitlab
@cache.build_attributes
end
end
+
+ # If a job uses `allow_failure:exit_codes` and `rules:allow_failure`
+ # we need to prevent the exit codes from being persisted because they
+ # would break the behavior defined by `rules:allow_failure`.
+ def allow_failure_criteria_attributes
+ return {} unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled?
+ return {} if rules_attributes[:allow_failure].nil?
+ return {} unless @seed_attributes.dig(:options, :allow_failure_criteria)
+
+ { options: { allow_failure_criteria: nil } }
+ end
end
end
end
diff --git a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
index 401a710182d..135f0df99fe 100644
--- a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml
@@ -15,7 +15,8 @@ variables:
FUZZAPI_VERSION: latest
FUZZAPI_CONFIG: .gitlab-api-fuzzing.yml
FUZZAPI_TIMEOUT: 30
- FUZZAPI_REPORT: gl-api-fuzzing-report.xml
+ FUZZAPI_REPORT: gl-api-fuzzing-report.json
+ FUZZAPI_REPORT_ASSET_PATH: assets
#
FUZZAPI_D_NETWORK: testing-net
#
@@ -45,6 +46,7 @@ apifuzzer_fuzz:
variables:
FUZZAPI_PROJECT: $CI_PROJECT_PATH
FUZZAPI_API: http://apifuzzer:80
+ FUZZAPI_NEW_REPORT: 1
TZ: America/Los_Angeles
services:
- name: $FUZZAPI_IMAGE
@@ -75,6 +77,9 @@ apifuzzer_fuzz:
# Run user provided pre-script
- sh -c "$FUZZAPI_PRE_SCRIPT"
#
+ # Make sure asset path exists
+ - mkdir -p $FUZZAPI_REPORT_ASSET_PATH
+ #
# Start scanning
- worker-entry
#
@@ -82,8 +87,12 @@ apifuzzer_fuzz:
- sh -c "$FUZZAPI_POST_SCRIPT"
#
artifacts:
+ when: always
+ paths:
+ - $FUZZAPI_REPORT_ASSET_PATH
+ - $FUZZAPI_REPORT
reports:
- junit: $FUZZAPI_REPORT
+ api_fuzzing: $FUZZAPI_REPORT
apifuzzer_fuzz_dnd:
stage: fuzz
@@ -115,6 +124,9 @@ apifuzzer_fuzz_dnd:
# Run user provided pre-script
- sh -c "$FUZZAPI_PRE_SCRIPT"
#
+ # Make sure asset path exists
+ - mkdir -p $FUZZAPI_REPORT_ASSET_PATH
+ #
# Start peach testing engine container
- |
docker run -d \
@@ -155,6 +167,8 @@ apifuzzer_fuzz_dnd:
-e FUZZAPI_PROFILE \
-e FUZZAPI_CONFIG \
-e FUZZAPI_REPORT \
+ -e FUZZAPI_REPORT_ASSET_PATH \
+ -e FUZZAPI_NEW_REPORT=1 \
-e FUZZAPI_HAR \
-e FUZZAPI_OPENAPI \
-e FUZZAPI_POSTMAN_COLLECTION \
@@ -168,6 +182,8 @@ apifuzzer_fuzz_dnd:
-e FUZZAPI_SERVICE_START_TIMEOUT \
-e FUZZAPI_HTTP_USERNAME \
-e FUZZAPI_HTTP_PASSWORD \
+ -e CI_PROJECT_URL \
+ -e CI_JOB_ID \
-e CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH} \
$FUZZAPI_D_WORKER_ENV \
$FUZZAPI_D_WORKER_PORTS \
@@ -193,6 +209,8 @@ apifuzzer_fuzz_dnd:
-e FUZZAPI_PROFILE \
-e FUZZAPI_CONFIG \
-e FUZZAPI_REPORT \
+ -e FUZZAPI_REPORT_ASSET_PATH \
+ -e FUZZAPI_NEW_REPORT=1 \
-e FUZZAPI_HAR \
-e FUZZAPI_OPENAPI \
-e FUZZAPI_POSTMAN_COLLECTION \
@@ -206,7 +224,10 @@ apifuzzer_fuzz_dnd:
-e FUZZAPI_SERVICE_START_TIMEOUT \
-e FUZZAPI_HTTP_USERNAME \
-e FUZZAPI_HTTP_PASSWORD \
+ -e CI_PROJECT_URL \
+ -e CI_JOB_ID \
-v $CI_PROJECT_DIR:/app \
+ -v `pwd`/$FUZZAPI_REPORT_ASSET_PATH:/app/$FUZZAPI_REPORT_ASSET_PATH:rw \
-p 81:80 \
-p 8001:8000 \
-p 515:514 \
@@ -239,7 +260,9 @@ apifuzzer_fuzz_dnd:
paths:
- ./gl-api_fuzzing*.log
- ./gl-api_fuzzing*.zip
+ - $FUZZAPI_REPORT_ASSET_PATH
+ - $FUZZAPI_REPORT
reports:
- junit: $FUZZAPI_REPORT
+ api_fuzzing: $FUZZAPI_REPORT
# end
diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb
index 52a00e41214..cd7d781a574 100644
--- a/lib/gitlab/ci/yaml_processor/result.rb
+++ b/lib/gitlab/ci/yaml_processor/result.rb
@@ -77,6 +77,7 @@ module Gitlab
options: {
image: job[:image],
services: job[:services],
+ allow_failure_criteria: job[:allow_failure_criteria],
artifacts: job[:artifacts],
dependencies: job[:dependencies],
cross_dependencies: job.dig(:needs, :cross_dependency),
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 2a386657e0b..88786ed82ff 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -134,6 +134,16 @@ module Gitlab
end
end
+ class HashOrBooleanValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Hash) || validate_boolean(value)
+ record.errors.add(attribute, 'should be a hash or a boolean value')
+ end
+ end
+ end
+
class KeyValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
@@ -158,6 +168,22 @@ module Gitlab
end
end
+ class ArrayOfIntegersOrIntegerValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_integer(value) || validate_array_of_integers(value)
+ record.errors.add(attribute, 'should be an array of integers or an integer')
+ end
+ end
+
+ private
+
+ def validate_array_of_integers(values)
+ values.is_a?(Array) && values.all? { |value| validate_integer(value) }
+ end
+ end
+
class RegexpValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index 352a93817be..d123989ef8e 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -20,6 +20,7 @@ module Gitlab
end,
container_class: ProjectWiki,
project_resolver: -> (wiki) { wiki.try(:project) },
+ guest_read_ability: :download_wiki_code,
suffix: :wiki
).freeze
SNIPPET = RepoType.new(
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
index 79cf081b9dc..46c84107e0f 100644
--- a/lib/gitlab/repo_path.rb
+++ b/lib/gitlab/repo_path.rb
@@ -4,6 +4,13 @@ module Gitlab
module RepoPath
NotFoundError = Class.new(StandardError)
+ # Returns an array containing:
+ # - The repository container
+ # - The related project (if available)
+ # - The repository type
+ # - The original container path (if redirected)
+ #
+ # @returns [HasRepository, Project, String, String]
def self.parse(path)
repo_path = path.delete_prefix('/').delete_suffix('.git')
redirected_path = nil
@@ -30,7 +37,15 @@ module Gitlab
[nil, nil, Gitlab::GlRepository.default_type, nil]
end
+ # Returns an array containing:
+ # - The repository container
+ # - The related project (if available)
+ # - The original container path (if redirected)
+ #
+ # @returns [HasRepository, Project, String]
def self.find_container(type, full_path)
+ return [nil, nil, nil] if full_path.blank?
+
if type.snippet?
snippet, redirected_path = find_snippet(full_path)
@@ -47,26 +62,24 @@ module Gitlab
end
def self.find_project(project_path)
- return [nil, nil] if project_path.blank?
-
project = Project.find_by_full_path(project_path, follow_redirects: true)
- redirected_path = redirected?(project, project_path) ? project_path : nil
+ redirected_path = project_path if redirected?(project, project_path)
[project, redirected_path]
end
- def self.redirected?(project, project_path)
- project && project.full_path.casecmp(project_path) != 0
+ def self.redirected?(container, container_path)
+ container && container.full_path.casecmp(container_path) != 0
end
# Snippet_path can be either:
# - snippets/1
# - h5bp/html5-boilerplate/snippets/53
def self.find_snippet(snippet_path)
- return [nil, nil] if snippet_path.blank?
-
snippet_id, project_path = extract_snippet_info(snippet_path)
- project, redirected_path = find_project(project_path)
+ return [nil, nil] unless snippet_id
+
+ project, redirected_path = find_project(project_path) if project_path
[Snippet.find_by_id_and_project(id: snippet_id, project: project), redirected_path]
end
@@ -74,19 +87,23 @@ module Gitlab
# Wiki path can be either:
# - namespace/project
# - group/subgroup/project
- def self.find_wiki(wiki_path)
- return [nil, nil] if wiki_path.blank?
-
- project, redirected_path = find_project(wiki_path)
-
- [project&.wiki, redirected_path]
+ #
+ # And also in EE:
+ # - group
+ # - group/subgroup
+ def self.find_wiki(container_path)
+ container = Routable.find_by_full_path(container_path, follow_redirects: true)
+ redirected_path = container_path if redirected?(container, container_path)
+
+ # In CE, Group#wiki is not available so this will return nil for a group path.
+ [container&.try(:wiki), redirected_path]
end
def self.extract_snippet_info(snippet_path)
path_segments = snippet_path.split('/')
snippet_id = path_segments.pop
- path_segments.pop # Remove snippets from path
- project_path = File.join(path_segments)
+ path_segments.pop # Remove 'snippets' from path
+ project_path = File.join(path_segments).presence
[snippet_id, project_path]
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index e7ee6c3d846..f935c677930 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -584,7 +584,7 @@ module Gitlab
gitlab: distinct_count(::BulkImport.where(time_period, source_type: :gitlab), :user_id)
},
projects_imported: {
- total: count(Project.where(time_period).where.not(import_type: nil)),
+ total: distinct_count(::Project.where(time_period).where.not(import_type: nil), :creator_id),
gitlab_project: projects_imported_count('gitlab_project', time_period),
gitlab: projects_imported_count('gitlab', time_period),
github: projects_imported_count('github', time_period),
@@ -894,7 +894,7 @@ module Gitlab
end
def projects_imported_count(from, time_period)
- distinct_count(::Project.imported_from(from).where(time_period), :creator_id) # rubocop: disable CodeReuse/ActiveRecord
+ distinct_count(::Project.imported_from(from).where(time_period).where.not(import_type: nil), :creator_id) # rubocop: disable CodeReuse/ActiveRecord
end
# rubocop:disable CodeReuse/ActiveRecord
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1201cafb85c..4401ee1193a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4774,6 +4774,12 @@ msgstr ""
msgid "Bulk request concurrency"
msgstr ""
+msgid "BulkImport|From source group"
+msgstr ""
+
+msgid "BulkImport|To new group"
+msgstr ""
+
msgid "BulkImport|expected an associated Group but has an associated Project"
msgstr ""
@@ -16876,6 +16882,9 @@ msgstr ""
msgid "Max 100,000 events"
msgstr ""
+msgid "Max 20 characters"
+msgstr ""
+
msgid "Max Group Export Download requests per minute per user"
msgstr ""
@@ -20173,6 +20182,9 @@ msgstr ""
msgid "Personal Access Token"
msgstr ""
+msgid "Personal Access Token prefix"
+msgstr ""
+
msgid "Personal project creation is not allowed. Please contact your administrator with questions"
msgstr ""
@@ -32331,6 +32343,9 @@ msgstr ""
msgid "by"
msgstr ""
+msgid "can contain only letters of the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'"
+msgstr ""
+
msgid "cannot be a date in the past"
msgstr ""
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index f71f859a704..f0b224484c6 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -157,6 +157,44 @@ RSpec.describe Admin::ApplicationSettingsController do
expect(ApplicationSetting.current.default_branch_name).to eq("example_branch_name")
end
+ context "personal access token prefix settings" do
+ let(:application_settings) { ApplicationSetting.current }
+
+ shared_examples "accepts prefix setting" do |prefix|
+ it "updates personal_access_token_prefix setting" do
+ put :update, params: { application_setting: { personal_access_token_prefix: prefix } }
+
+ expect(response).to redirect_to(general_admin_application_settings_path)
+ expect(application_settings.reload.personal_access_token_prefix).to eq(prefix)
+ end
+ end
+
+ shared_examples "rejects prefix setting" do |prefix|
+ it "does not update personal_access_token_prefix setting" do
+ put :update, params: { application_setting: { personal_access_token_prefix: prefix } }
+
+ expect(response).not_to redirect_to(general_admin_application_settings_path)
+ expect(application_settings.reload.personal_access_token_prefix).not_to eq(prefix)
+ end
+ end
+
+ context "with valid prefix" do
+ include_examples("accepts prefix setting", "a_prefix@")
+ end
+
+ context "with blank prefix" do
+ include_examples("accepts prefix setting", "")
+ end
+
+ context "with too long prefix" do
+ include_examples("rejects prefix setting", "a_prefix@" * 10)
+ end
+
+ context "with invalid characters prefix" do
+ include_examples("rejects prefix setting", "a_préfixñ:")
+ end
+ end
+
context 'external policy classification settings' do
let(:settings) do
{
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 4b1ec7850e0..551abf9241d 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Repositories::GitHttpController do
- include GitHttpHelpers
-
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) }
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index d8ff2e08657..9625fdc195d 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -26,5 +26,9 @@ FactoryBot.define do
trait :invalid do
token_digest { nil }
end
+
+ trait :no_prefix do
+ after(:build) { |personal_access_token| personal_access_token.set_token(Devise.friendly_token) }
+ end
end
end
diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb
index abb4b30fc2d..c0f967fd0b9 100644
--- a/spec/features/groups/import_export/connect_instance_spec.rb
+++ b/spec/features/groups/import_export/connect_instance_spec.rb
@@ -22,6 +22,19 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
it 'successfully connects to remote instance' do
source_url = 'https://gitlab.com'
pat = 'demo-pat'
+ stub_path = 'stub-group'
+
+ stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=30&top_level_only=true" % { url: source_url }).to_return(
+ body: [{
+ id: 2595438,
+ web_url: 'https://gitlab.com/groups/auto-breakfast',
+ name: 'Stub',
+ path: stub_path,
+ full_name: 'Stub',
+ full_path: stub_path
+ }].to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
expect(page).to have_content 'Import groups from another instance of GitLab'
@@ -31,6 +44,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
click_on 'Connect instance'
expect(page).to have_content 'Importing groups from %{url}' % { url: source_url }
+ expect(page).to have_content stub_path
end
end
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
new file mode 100644
index 00000000000..d88a31a0e47
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
@@ -0,0 +1,112 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlLink, GlFormInput } from '@gitlab/ui';
+import Select2Select from '~/vue_shared/components/select2_select.vue';
+import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
+import { STATUSES } from '~/import_entities/constants';
+import { availableNamespacesFixture } from '../graphql/fixtures';
+
+const getFakeGroup = status => ({
+ web_url: 'https://fake.host/',
+ full_path: 'fake_group_1',
+ full_name: 'fake_name_1',
+ import_target: {
+ target_namespace: 'root',
+ new_name: 'group1',
+ },
+ id: 1,
+ status,
+});
+
+describe('import table row', () => {
+ let wrapper;
+ let group;
+
+ const findByText = (cmp, text) => {
+ return wrapper.findAll(cmp).wrappers.find(node => node.text().indexOf(text) === 0);
+ };
+ const findImportButton = () => findByText(GlButton, 'Import');
+ const findNameInput = () => wrapper.find(GlFormInput);
+ const findNamespaceDropdown = () => wrapper.find(Select2Select);
+
+ const createComponent = props => {
+ wrapper = shallowMount(ImportTableRow, {
+ propsData: {
+ availableNamespaces: availableNamespacesFixture,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ group = getFakeGroup(STATUSES.NONE);
+ createComponent({ group });
+ });
+
+ it.each`
+ selector | sourceEvent | payload | event
+ ${findNamespaceDropdown} | ${'input'} | ${'demo'} | ${'update-target-namespace'}
+ ${findNameInput} | ${'input'} | ${'demo'} | ${'update-new-name'}
+ ${findImportButton} | ${'click'} | ${undefined} | ${'import-group'}
+ `('invokes $event', ({ selector, sourceEvent, payload, event }) => {
+ selector().vm.$emit(sourceEvent, payload);
+ expect(wrapper.emitted(event)).toBeDefined();
+ expect(wrapper.emitted(event)[0][0]).toBe(payload);
+ });
+ });
+
+ describe('when entity status is NONE', () => {
+ beforeEach(() => {
+ group = getFakeGroup(STATUSES.NONE);
+ createComponent({ group });
+ });
+
+ it('renders Import button', () => {
+ expect(findByText(GlButton, 'Import').exists()).toBe(true);
+ });
+
+ it('renders namespace dropdown as not disabled', () => {
+ expect(findNamespaceDropdown().attributes('disabled')).toBe(undefined);
+ });
+ });
+
+ describe('when entity status is SCHEDULING', () => {
+ beforeEach(() => {
+ group = getFakeGroup(STATUSES.SCHEDULING);
+ createComponent({ group });
+ });
+
+ it('does not render Import button', () => {
+ expect(findByText(GlButton, 'Import')).toBe(undefined);
+ });
+
+ it('renders namespace dropdown as disabled', () => {
+ expect(findNamespaceDropdown().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when entity status is FINISHED', () => {
+ beforeEach(() => {
+ group = getFakeGroup(STATUSES.FINISHED);
+ createComponent({ group });
+ });
+
+ it('does not render Import button', () => {
+ expect(findByText(GlButton, 'Import')).toBe(undefined);
+ });
+
+ it('does not render namespace dropdown', () => {
+ expect(findNamespaceDropdown().exists()).toBe(false);
+ });
+
+ it('renders target as link', () => {
+ const TARGET_LINK = `${group.import_target.target_namespace}/${group.import_target.new_name}`;
+ expect(findByText(GlLink, TARGET_LINK).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
new file mode 100644
index 00000000000..0ca721cd951
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -0,0 +1,103 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
+import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
+import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
+import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
+import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
+import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
+
+import { STATUSES } from '~/import_entities/constants';
+
+import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('import table', () => {
+ let wrapper;
+ let apolloProvider;
+
+ const createComponent = ({ bulkImportSourceGroups }) => {
+ apolloProvider = createMockApollo([], {
+ Query: {
+ availableNamespaces: () => availableNamespacesFixture,
+ bulkImportSourceGroups,
+ },
+ Mutation: {
+ setTargetNamespace: jest.fn(),
+ setNewName: jest.fn(),
+ importGroup: jest.fn(),
+ },
+ });
+
+ wrapper = shallowMount(ImportTable, {
+ localVue,
+ apolloProvider,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders loading icon while performing request', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => new Promise(() => {}),
+ });
+ await waitForPromises();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('does not renders loading icon when request is completed', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => [],
+ });
+ await waitForPromises();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+
+ it('renders import row for each group in response', async () => {
+ const FAKE_GROUPS = [
+ generateFakeEntry({ id: 1, status: STATUSES.NONE }),
+ generateFakeEntry({ id: 2, status: STATUSES.FINISHED }),
+ ];
+ createComponent({
+ bulkImportSourceGroups: () => FAKE_GROUPS,
+ });
+ await waitForPromises();
+
+ expect(wrapper.findAll(ImportTableRow)).toHaveLength(FAKE_GROUPS.length);
+ });
+
+ describe('converts row events to mutation invocations', () => {
+ const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+
+ beforeEach(() => {
+ createComponent({
+ bulkImportSourceGroups: () => [FAKE_GROUP],
+ });
+ return waitForPromises();
+ });
+
+ it.each`
+ event | payload | mutation | variables
+ ${'update-target-namespace'} | ${'new-namespace'} | ${setTargetNamespaceMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'new-namespace' }}
+ ${'update-new-name'} | ${'new-name'} | ${setNewNameMutation} | ${{ sourceGroupId: FAKE_GROUP.id, newName: 'new-name' }}
+ ${'import-group'} | ${undefined} | ${importGroupMutation} | ${{ sourceGroupId: FAKE_GROUP.id }}
+ `('correctly maps $event to mutation', async ({ event, payload, mutation, variables }) => {
+ jest.spyOn(apolloProvider.defaultClient, 'mutate');
+ wrapper.find(ImportTableRow).vm.$emit(event, payload);
+ await waitForPromises();
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation,
+ variables,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
new file mode 100644
index 00000000000..e10231dd68b
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -0,0 +1,178 @@
+import MockAdapter from 'axios-mock-adapter';
+import { InMemoryCache } from 'apollo-cache-inmemory';
+import { createMockClient } from 'mock-apollo-client';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import {
+ clientTypenames,
+ createResolvers,
+} from '~/import_entities/import_groups/graphql/client_factory';
+import { STATUSES } from '~/import_entities/constants';
+
+import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
+import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
+import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
+import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
+import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
+import httpStatus from '~/lib/utils/http_status';
+import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
+
+const FAKE_ENDPOINTS = {
+ status: '/fake_status_url',
+ availableNamespaces: '/fake_available_namespaces',
+ createBulkImport: '/fake_create_bulk_import',
+};
+
+describe('Bulk import resolvers', () => {
+ let axiosMockAdapter;
+ let client;
+
+ beforeEach(() => {
+ axiosMockAdapter = new MockAdapter(axios);
+ client = createMockClient({
+ cache: new InMemoryCache({
+ fragmentMatcher: { match: () => true },
+ addTypename: false,
+ }),
+ resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS }),
+ });
+ });
+
+ afterEach(() => {
+ axiosMockAdapter.restore();
+ });
+
+ describe('queries', () => {
+ describe('availableNamespaces', () => {
+ let results;
+
+ beforeEach(async () => {
+ axiosMockAdapter
+ .onGet(FAKE_ENDPOINTS.availableNamespaces)
+ .reply(httpStatus.OK, availableNamespacesFixture);
+
+ const response = await client.query({ query: availableNamespacesQuery });
+ results = response.data.availableNamespaces;
+ });
+
+ it('mirrors REST endpoint response fields', () => {
+ const extractRelevantFields = obj => ({ id: obj.id, full_path: obj.full_path });
+
+ expect(results.map(extractRelevantFields)).toStrictEqual(
+ availableNamespacesFixture.map(extractRelevantFields),
+ );
+ });
+ });
+
+ describe('bulkImportSourceGroups', () => {
+ let results;
+
+ beforeEach(async () => {
+ axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
+ axiosMockAdapter
+ .onGet(FAKE_ENDPOINTS.availableNamespaces)
+ .reply(httpStatus.OK, availableNamespacesFixture);
+
+ const response = await client.query({ query: bulkImportSourceGroupsQuery });
+ results = response.data.bulkImportSourceGroups;
+ });
+
+ it('mirrors REST endpoint response fields', () => {
+ const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url'];
+ expect(
+ results.every((r, idx) =>
+ MIRRORED_FIELDS.every(
+ field => r[field] === statusEndpointFixture.importable_data[idx][field],
+ ),
+ ),
+ ).toBe(true);
+ });
+
+ it('populates each result instance with status field default to none', () => {
+ expect(results.every(r => r.status === STATUSES.NONE)).toBe(true);
+ });
+
+ it('populates each result instance with import_target defaulted to first available namespace', () => {
+ expect(
+ results.every(
+ r => r.import_target.target_namespace === availableNamespacesFixture[0].full_path,
+ ),
+ ).toBe(true);
+ });
+ });
+ });
+
+ describe('mutations', () => {
+ let results;
+ const GROUP_ID = 1;
+
+ beforeEach(() => {
+ client.writeQuery({
+ query: bulkImportSourceGroupsQuery,
+ data: {
+ bulkImportSourceGroups: [
+ {
+ __typename: clientTypenames.BulkImportSourceGroup,
+ id: GROUP_ID,
+ status: STATUSES.NONE,
+ web_url: 'https://fake.host/1',
+ full_path: 'fake_group_1',
+ full_name: 'fake_name_1',
+ import_target: {
+ target_namespace: 'root',
+ new_name: 'group1',
+ },
+ },
+ ],
+ },
+ });
+
+ client
+ .watchQuery({
+ query: bulkImportSourceGroupsQuery,
+ fetchPolicy: 'cache-only',
+ })
+ .subscribe(({ data }) => {
+ results = data.bulkImportSourceGroups;
+ });
+ });
+
+ it('setTargetNamespaces updates group target namespace', async () => {
+ const NEW_TARGET_NAMESPACE = 'target';
+ await client.mutate({
+ mutation: setTargetNamespaceMutation,
+ variables: { sourceGroupId: GROUP_ID, targetNamespace: NEW_TARGET_NAMESPACE },
+ });
+
+ expect(results[0].import_target.target_namespace).toBe(NEW_TARGET_NAMESPACE);
+ });
+
+ it('setNewName updates group target name', async () => {
+ const NEW_NAME = 'new';
+ await client.mutate({
+ mutation: setNewNameMutation,
+ variables: { sourceGroupId: GROUP_ID, newName: NEW_NAME },
+ });
+
+ expect(results[0].import_target.new_name).toBe(NEW_NAME);
+ });
+
+ describe('importGroup', () => {
+ it('sets status to SCHEDULING when request initiates', async () => {
+ axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(() => new Promise(() => {}));
+
+ client.mutate({
+ mutation: importGroupMutation,
+ variables: { sourceGroupId: GROUP_ID },
+ });
+ await waitForPromises();
+
+ const { bulkImportSourceGroups: intermediateResults } = client.readQuery({
+ query: bulkImportSourceGroupsQuery,
+ });
+
+ expect(intermediateResults[0].status).toBe(STATUSES.SCHEDULING);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
new file mode 100644
index 00000000000..62e9581bd2d
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js
@@ -0,0 +1,51 @@
+import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
+
+export const generateFakeEntry = ({ id, status, ...rest }) => ({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ web_url: `https://fake.host/${id}`,
+ full_path: `fake_group_${id}`,
+ full_name: `fake_name_${id}`,
+ import_target: {
+ target_namespace: 'root',
+ new_name: `group${id}`,
+ },
+ id,
+ status,
+ ...rest,
+});
+
+export const statusEndpointFixture = {
+ importable_data: [
+ {
+ id: 2595438,
+ full_name: 'AutoBreakfast',
+ full_path: 'auto-breakfast',
+ web_url: 'https://gitlab.com/groups/auto-breakfast',
+ },
+ {
+ id: 4347861,
+ full_name: 'GitLab Data',
+ full_path: 'gitlab-data',
+ web_url: 'https://gitlab.com/groups/gitlab-data',
+ },
+ {
+ id: 5723700,
+ full_name: 'GitLab Services',
+ full_path: 'gitlab-services',
+ web_url: 'https://gitlab.com/groups/gitlab-services',
+ },
+ {
+ id: 349181,
+ full_name: 'GitLab-examples',
+ full_path: 'gitlab-examples',
+ web_url: 'https://gitlab.com/groups/gitlab-examples',
+ },
+ ],
+};
+
+export const availableNamespacesFixture = [
+ { id: 24, full_path: 'Commit451' },
+ { id: 22, full_path: 'gitlab-org' },
+ { id: 23, full_path: 'gnuwget' },
+ { id: 25, full_path: 'jashkenas' },
+];
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
new file mode 100644
index 00000000000..5940ea544ea
--- /dev/null
+++ b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
@@ -0,0 +1,82 @@
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
+import ImportSourceGroupFragment from '~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql';
+import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
+
+describe('SourceGroupsManager', () => {
+ let manager;
+ let client;
+
+ const getFakeGroup = () => ({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ id: 5,
+ });
+
+ beforeEach(() => {
+ client = {
+ readFragment: jest.fn(),
+ writeFragment: jest.fn(),
+ };
+
+ manager = new SourceGroupsManager({ client });
+ });
+
+ it('finds item by group id', () => {
+ const ID = 5;
+
+ const FAKE_GROUP = getFakeGroup();
+ client.readFragment.mockReturnValue(FAKE_GROUP);
+ const group = manager.findById(ID);
+ expect(group).toBe(FAKE_GROUP);
+ expect(client.readFragment).toHaveBeenCalledWith({
+ fragment: ImportSourceGroupFragment,
+ id: defaultDataIdFromObject(getFakeGroup()),
+ });
+ });
+
+ it('updates group with provided function', () => {
+ const UPDATED_GROUP = {};
+ const fn = jest.fn().mockReturnValue(UPDATED_GROUP);
+ manager.update(getFakeGroup(), fn);
+
+ expect(client.writeFragment).toHaveBeenCalledWith({
+ fragment: ImportSourceGroupFragment,
+ id: defaultDataIdFromObject(getFakeGroup()),
+ data: UPDATED_GROUP,
+ });
+ });
+
+ it('updates group by id with provided function', () => {
+ const UPDATED_GROUP = {};
+ const fn = jest.fn().mockReturnValue(UPDATED_GROUP);
+ client.readFragment.mockReturnValue(getFakeGroup());
+ manager.updateById(getFakeGroup().id, fn);
+
+ expect(client.readFragment).toHaveBeenCalledWith({
+ fragment: ImportSourceGroupFragment,
+ id: defaultDataIdFromObject(getFakeGroup()),
+ });
+
+ expect(client.writeFragment).toHaveBeenCalledWith({
+ fragment: ImportSourceGroupFragment,
+ id: defaultDataIdFromObject(getFakeGroup()),
+ data: UPDATED_GROUP,
+ });
+ });
+
+ it('sets import status when group is provided', () => {
+ client.readFragment.mockReturnValue(getFakeGroup());
+
+ const NEW_STATUS = 'NEW_STATUS';
+ manager.setImportStatus(getFakeGroup(), NEW_STATUS);
+
+ expect(client.writeFragment).toHaveBeenCalledWith({
+ fragment: ImportSourceGroupFragment,
+ id: defaultDataIdFromObject(getFakeGroup()),
+ data: {
+ ...getFakeGroup(),
+ status: NEW_STATUS,
+ },
+ });
+ });
+});
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 3d048b35015..f927d5912bb 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -384,6 +384,16 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
+
+ context 'when using a non-prefixed access token' do
+ let(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) }
+
+ it 'returns user' do
+ set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}")
+
+ expect(find_user_from_access_token).to eq user
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb
index cbeae33fbcf..ba99650396d 100644
--- a/spec/lib/gitlab/ci/build/rules_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules_spec.rb
@@ -187,11 +187,17 @@ RSpec.describe Gitlab::Ci::Build::Rules do
let(:start_in) { nil }
let(:allow_failure) { nil }
- subject { Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure) }
+ subject(:result) do
+ Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure)
+ end
describe '#build_attributes' do
+ subject(:build_attributes) do
+ result.build_attributes
+ end
+
it 'compacts nil values' do
- expect(subject.build_attributes).to eq(options: {}, when: 'on_success')
+ is_expected.to eq(options: {}, when: 'on_success')
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb b/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb
new file mode 100644
index 00000000000..7aaad57f5cd
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Entry::AllowFailure do
+ let(:entry) { described_class.new(config.deep_dup) }
+ let(:expected_config) { config }
+
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ shared_examples 'valid entry' do
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq(expected_config)
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'with boolean values' do
+ it_behaves_like 'valid entry' do
+ let(:config) { true }
+ end
+
+ it_behaves_like 'valid entry' do
+ let(:config) { false }
+ end
+ end
+
+ context 'with hash values' do
+ it_behaves_like 'valid entry' do
+ let(:config) { { exit_codes: 137 } }
+ let(:expected_config) { { exit_codes: [137] } }
+ end
+
+ it_behaves_like 'valid entry' do
+ let(:config) { { exit_codes: [42, 137] } }
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ shared_examples 'invalid entry' do
+ describe '#valid?' do
+ it { expect(entry).not_to be_valid }
+ it { expect(entry.errors).to include(error_message) }
+ end
+ end
+
+ context 'when it has a wrong type' do
+ let(:config) { [1] }
+ let(:error_message) do
+ 'allow failure config should be a hash or a boolean value'
+ end
+
+ it_behaves_like 'invalid entry'
+ end
+
+ context 'with string exit codes' do
+ let(:config) { { exit_codes: 'string' } }
+ let(:error_message) do
+ 'allow failure exit codes should be an array of integers or an integer'
+ end
+
+ it_behaves_like 'invalid entry'
+ end
+
+ context 'with array of strings as exit codes' do
+ let(:config) { { exit_codes: ['string 1', 'string 2'] } }
+ let(:error_message) do
+ 'allow failure exit codes should be an array of integers or an integer'
+ end
+
+ it_behaves_like 'invalid entry'
+ end
+
+ context 'when it has an extra keys' do
+ let(:config) { { extra: true } }
+ let(:error_message) do
+ 'allow failure config contains unknown keys: extra'
+ end
+
+ it_behaves_like 'invalid entry'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index 8b2e0410474..b3b7901074a 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -227,6 +227,23 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
end
end
end
+
+ context 'when bridge config contains exit_codes' do
+ let(:config) do
+ { script: 'rspec', allow_failure: { exit_codes: [42] } }
+ end
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns an error message' do
+ expect(subject.errors)
+ .to include(/allow failure should be a boolean value/)
+ end
+ end
+ end
end
describe '#manual_action?' do
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index e0e8bc93770..7834a1a94f2 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -670,6 +670,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
end
describe '#ignored?' do
+ before do
+ entry.compose!
+ end
+
context 'when job is a manual action' do
context 'when it is not specified if job is allowed to fail' do
let(:config) do
@@ -700,6 +704,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
expect(entry).not_to be_ignored
end
end
+
+ context 'when job is dynamically allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual', allow_failure: { exit_codes: 42 } }
+ end
+
+ it 'is not an ignored job' do
+ expect(entry).not_to be_ignored
+ end
+ end
end
context 'when job is not a manual action' do
@@ -709,6 +723,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
it 'is not an ignored job' do
expect(entry).not_to be_ignored
end
+
+ it 'does not return allow_failure' do
+ expect(entry.value.key?(:allow_failure_criteria)).to be_falsey
+ end
end
context 'when job is allowed to fail' do
@@ -717,6 +735,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
it 'is an ignored job' do
expect(entry).to be_ignored
end
+
+ it 'does not return allow_failure_criteria' do
+ expect(entry.value.key?(:allow_failure_criteria)).to be_falsey
+ end
end
context 'when job is not allowed to fail' do
@@ -725,6 +747,32 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
it 'is not an ignored job' do
expect(entry).not_to be_ignored
end
+
+ it 'does not return allow_failure_criteria' do
+ expect(entry.value.key?(:allow_failure_criteria)).to be_falsey
+ end
+ end
+
+ context 'when job is dynamically allowed to fail' do
+ let(:config) { { script: 'deploy', allow_failure: { exit_codes: 42 } } }
+
+ it 'is not an ignored job' do
+ expect(entry).not_to be_ignored
+ end
+
+ it 'returns allow_failure_criteria' do
+ expect(entry.value[:allow_failure_criteria]).to match(exit_codes: [42])
+ end
+
+ context 'with ci_allow_failure_with_exit_codes disabled' do
+ before do
+ stub_feature_flags(ci_allow_failure_with_exit_codes: false)
+ end
+
+ it 'does not return allow_failure_criteria' do
+ expect(entry.value.key?(:allow_failure_criteria)).to be_falsey
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 0b961336f3f..3cbc498c3a4 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -165,6 +165,45 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.to include(options: {}) }
end
+
+ context 'with allow_failure' do
+ let(:options) do
+ { allow_failure_criteria: { exit_codes: [42] } }
+ end
+
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always' }]
+ end
+
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ options: options,
+ rules: rules
+ }
+ end
+
+ context 'when rules does not override allow_failure' do
+ it { is_expected.to match a_hash_including(options: options) }
+ end
+
+ context 'when rules set allow_failure to true' do
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always', allow_failure: true }]
+ end
+
+ it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
+ end
+
+ context 'when rules set allow_failure to false' do
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always', allow_failure: false }]
+ end
+
+ it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
+ end
+ end
end
describe '#bridge?' do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 411337fc32f..5ad1b3dd241 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -231,6 +231,23 @@ module Gitlab
expect(subject[:allow_failure]).to be true
end
end
+
+ context 'when allow_failure has exit_codes' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ when: 'manual',
+ allow_failure: { exit_codes: 1 } })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+
+ it 'saves allow_failure_criteria into options' do
+ expect(subject[:options]).to match(
+ a_hash_including(allow_failure_criteria: { exit_codes: [1] }))
+ end
+ end
end
context 'when job is not a manual action' do
@@ -254,6 +271,22 @@ module Gitlab
expect(subject[:allow_failure]).to be false
end
end
+
+ context 'when allow_failure is dynamically specified' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ allow_failure: { exit_codes: 1 } })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+
+ it 'saves allow_failure_criteria into options' do
+ expect(subject[:options]).to match(
+ a_hash_including(allow_failure_criteria: { exit_codes: [1] }))
+ end
+ end
end
end
@@ -2494,7 +2527,13 @@ module Gitlab
context 'returns errors if job allow_failure parameter is not an boolean' do
let(:config) { YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) }
- it_behaves_like 'returns errors', 'jobs:rspec allow failure should be a boolean value'
+ it_behaves_like 'returns errors', 'jobs:rspec allow failure should be a hash or a boolean value'
+ end
+
+ context 'returns errors if job exit_code parameter from allow_failure is not an integer' do
+ let(:config) { YAML.dump({ rspec: { script: "test", allow_failure: { exit_codes: 'string' } } }) }
+
+ it_behaves_like 'returns errors', 'jobs:rspec:allow_failure exit codes should be an array of integers or an integer'
end
context 'returns errors if job stage is not a string' do
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index c70ac5f3ca9..c2d96369425 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -224,7 +224,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
gitlab: 2
},
projects_imported: {
- total: 20,
+ total: 2,
gitlab_project: 2,
gitlab: 2,
github: 2,
@@ -248,7 +248,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
gitlab: 1
},
projects_imported: {
- total: 10,
+ total: 1,
gitlab_project: 1,
gitlab: 1,
github: 1,
diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb
index 5fb7cdb4443..7cf7b825d7d 100644
--- a/spec/models/concerns/case_sensitivity_spec.rb
+++ b/spec/models/concerns/case_sensitivity_spec.rb
@@ -4,16 +4,16 @@ require 'spec_helper'
RSpec.describe CaseSensitivity do
describe '.iwhere' do
- let(:connection) { ActiveRecord::Base.connection }
- let(:model) do
+ let_it_be(:connection) { ActiveRecord::Base.connection }
+ let_it_be(:model) do
Class.new(ActiveRecord::Base) do
include CaseSensitivity
self.table_name = 'namespaces'
end
end
- let!(:model_1) { model.create!(path: 'mOdEl-1', name: 'mOdEl 1') }
- let!(:model_2) { model.create!(path: 'mOdEl-2', name: 'mOdEl 2') }
+ let_it_be(:model_1) { model.create!(path: 'mOdEl-1', name: 'mOdEl 1') }
+ let_it_be(:model_2) { model.create!(path: 'mOdEl-2', name: 'mOdEl 2') }
it 'finds a single instance by a single attribute regardless of case' do
expect(model.iwhere(path: 'MODEL-1')).to contain_exactly(model_1)
@@ -28,6 +28,10 @@ RSpec.describe CaseSensitivity do
.to contain_exactly(model_1)
end
+ it 'finds instances by custom Arel attributes' do
+ expect(model.iwhere(model.arel_table[:path] => 'MODEL-1')).to contain_exactly(model_1)
+ end
+
it 'builds a query using LOWER' do
query = model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1').to_sql
expected_query = <<~QRY.strip
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index e4cf68663ef..6ab87053258 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -2,8 +2,69 @@
require 'spec_helper'
+RSpec.shared_examples '.find_by_full_path' do
+ describe '.find_by_full_path', :aggregate_failures do
+ it 'finds records by their full path' do
+ expect(described_class.find_by_full_path(record.full_path)).to eq(record)
+ expect(described_class.find_by_full_path(record.full_path.upcase)).to eq(record)
+ end
+
+ it 'returns nil for unknown paths' do
+ expect(described_class.find_by_full_path('unknown')).to be_nil
+ end
+
+ it 'includes route information when loading a record' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ described_class.find_by_full_path(record.full_path)
+ end.count
+
+ expect do
+ described_class.find_by_full_path(record.full_path).route
+ end.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'with redirect routes' do
+ let_it_be(:redirect_route) { create(:redirect_route, source: record) }
+
+ context 'without follow_redirects option' do
+ it 'does not find records by their redirected path' do
+ expect(described_class.find_by_full_path(redirect_route.path)).to be_nil
+ expect(described_class.find_by_full_path(redirect_route.path.upcase)).to be_nil
+ end
+ end
+
+ context 'with follow_redirects option set to true' do
+ it 'finds records by their canonical path' do
+ expect(described_class.find_by_full_path(record.full_path, follow_redirects: true)).to eq(record)
+ expect(described_class.find_by_full_path(record.full_path.upcase, follow_redirects: true)).to eq(record)
+ end
+
+ it 'finds records by their redirected path' do
+ expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to eq(record)
+ expect(described_class.find_by_full_path(redirect_route.path.upcase, follow_redirects: true)).to eq(record)
+ end
+
+ it 'returns nil for unknown paths' do
+ expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to be_nil
+ end
+ end
+ end
+ end
+end
+
+RSpec.describe Routable do
+ it_behaves_like '.find_by_full_path' do
+ let_it_be(:record) { create(:group) }
+ end
+
+ it_behaves_like '.find_by_full_path' do
+ let_it_be(:record) { create(:project) }
+ end
+end
+
RSpec.describe Group, 'Routable' do
- let!(:group) { create(:group, name: 'foo') }
+ let_it_be_with_reload(:group) { create(:group, name: 'foo') }
+ let_it_be(:nested_group) { create(:group, parent: group) }
describe 'Validations' do
it { is_expected.to validate_presence_of(:route) }
@@ -59,61 +120,20 @@ RSpec.describe Group, 'Routable' do
end
describe '.find_by_full_path' do
- let!(:nested_group) { create(:group, parent: group) }
-
- context 'without any redirect routes' do
- it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
- it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
- it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
- it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
-
- it 'includes route information when loading a record' do
- path = group.to_param
- control_count = ActiveRecord::QueryRecorder.new { described_class.find_by_full_path(path) }.count
-
- expect { described_class.find_by_full_path(path).route }.not_to exceed_all_query_limit(control_count)
- end
+ it_behaves_like '.find_by_full_path' do
+ let_it_be(:record) { group }
end
- context 'with redirect routes' do
- let!(:group_redirect_route) { group.redirect_routes.create!(path: 'bar') }
- let!(:nested_group_redirect_route) { nested_group.redirect_routes.create!(path: nested_group.path.sub('foo', 'bar')) }
-
- context 'without follow_redirects option' do
- context 'with the given path not matching any route' do
- it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
- end
-
- context 'with the given path matching the canonical route' do
- it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
- it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
- it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
- end
-
- context 'with the given path matching a redirect route' do
- it { expect(described_class.find_by_full_path(group_redirect_route.path)).to eq(nil) }
- it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase)).to eq(nil) }
- it { expect(described_class.find_by_full_path(nested_group_redirect_route.path)).to eq(nil) }
- end
- end
-
- context 'with follow_redirects option set to true' do
- context 'with the given path not matching any route' do
- it { expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil) }
- end
+ it_behaves_like '.find_by_full_path' do
+ let_it_be(:record) { nested_group }
+ end
- context 'with the given path matching the canonical route' do
- it { expect(described_class.find_by_full_path(group.to_param, follow_redirects: true)).to eq(group) }
- it { expect(described_class.find_by_full_path(group.to_param.upcase, follow_redirects: true)).to eq(group) }
- it { expect(described_class.find_by_full_path(nested_group.to_param, follow_redirects: true)).to eq(nested_group) }
- end
+ it 'does not find projects with a matching path' do
+ project = create(:project)
+ redirect_route = create(:redirect_route, source: project)
- context 'with the given path matching a redirect route' do
- it { expect(described_class.find_by_full_path(group_redirect_route.path, follow_redirects: true)).to eq(group) }
- it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase, follow_redirects: true)).to eq(group) }
- it { expect(described_class.find_by_full_path(nested_group_redirect_route.path, follow_redirects: true)).to eq(nested_group) }
- end
- end
+ expect(described_class.find_by_full_path(project.full_path)).to be_nil
+ expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to be_nil
end
end
@@ -131,8 +151,6 @@ RSpec.describe Group, 'Routable' do
end
context 'with valid paths' do
- let!(:nested_group) { create(:group, parent: group) }
-
it 'returns the projects matching the paths' do
result = described_class.where_full_path_in([group.to_param, nested_group.to_param])
@@ -148,32 +166,36 @@ RSpec.describe Group, 'Routable' do
end
describe '#full_path' do
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
-
it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
end
describe '#full_name' do
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
-
it { expect(group.full_name).to eq(group.name) }
it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
end
end
RSpec.describe Project, 'Routable' do
- describe '#full_path' do
- let(:project) { build_stubbed(:project) }
+ let_it_be(:project) { create(:project) }
+ it_behaves_like '.find_by_full_path' do
+ let_it_be(:record) { project }
+ end
+
+ it 'does not find groups with a matching path' do
+ group = create(:group)
+ redirect_route = create(:redirect_route, source: group)
+
+ expect(described_class.find_by_full_path(group.full_path)).to be_nil
+ expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to be_nil
+ end
+
+ describe '#full_path' do
it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" }
end
describe '#full_name' do
- let(:project) { build_stubbed(:project) }
-
it { expect(project.full_name).to eq "#{project.namespace.human_name} / #{project.name}" }
end
end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 90e94b5dca9..d8b77e1cd0d 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -105,8 +105,8 @@ RSpec.describe PersonalAccessToken, 'TokenAuthenticatable' do
it 'sets new token' do
subject
- expect(personal_access_token.token).to eq(token_value)
- expect(personal_access_token.token_digest).to eq(Gitlab::CryptoHelper.sha256(token_value))
+ expect(personal_access_token.token).to eq("#{PersonalAccessToken.token_prefix}#{token_value}")
+ expect(personal_access_token.token_digest).to eq(Gitlab::CryptoHelper.sha256("#{PersonalAccessToken.token_prefix}#{token_value}"))
end
end
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index a540c85d4c7..e04f63befd0 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -220,6 +220,8 @@ RSpec.describe API::Internal::Base do
end
it 'returns a token without expiry when the expires_at parameter is missing' do
+ token_size = (PersonalAccessToken.token_prefix || '').size + 20
+
post api('/internal/personal_access_token'),
params: {
secret_token: secret_token,
@@ -229,12 +231,14 @@ RSpec.describe API::Internal::Base do
}
expect(json_response['success']).to be_truthy
- expect(json_response['token']).to match(/\A\S{20}\z/)
+ expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
expect(json_response['scopes']).to match_array(%w(read_api read_repository))
expect(json_response['expires_at']).to be_nil
end
it 'returns a token with expiry when it receives a valid expires_at parameter' do
+ token_size = (PersonalAccessToken.token_prefix || '').size + 20
+
post api('/internal/personal_access_token'),
params: {
secret_token: secret_token,
@@ -245,7 +249,7 @@ RSpec.describe API::Internal::Base do
}
expect(json_response['success']).to be_truthy
- expect(json_response['token']).to match(/\A\S{20}\z/)
+ expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
expect(json_response['scopes']).to match_array(%w(read_api read_repository))
expect(json_response['expires_at']).to eq('9001-11-17')
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 03320549e44..8fb0f8fc51a 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -43,6 +43,7 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['spam_check_endpoint_url']).to be_nil
expect(json_response['wiki_page_max_content_bytes']).to be_a(Integer)
expect(json_response['require_admin_approval_after_user_signup']).to eq(true)
+ expect(json_response['personal_access_token_prefix']).to be_nil
end
end
@@ -122,7 +123,8 @@ RSpec.describe API::Settings, 'Settings' do
spam_check_endpoint_url: 'https://example.com/spam_check',
disabled_oauth_sign_in_sources: 'unknown',
import_sources: 'github,bitbucket',
- wiki_page_max_content_bytes: 12345
+ wiki_page_max_content_bytes: 12345,
+ personal_access_token_prefix: "GL-"
}
expect(response).to have_gitlab_http_status(:ok)
@@ -166,6 +168,7 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['disabled_oauth_sign_in_sources']).to eq([])
expect(json_response['import_sources']).to match_array(%w(github bitbucket))
expect(json_response['wiki_page_max_content_bytes']).to eq(12345)
+ expect(json_response['personal_access_token_prefix']).to eq("GL-")
end
end
@@ -451,5 +454,25 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['error']).to eq('spam_check_endpoint_url is missing')
end
end
+
+ context "personal access token prefix settings" do
+ context "handles validation errors" do
+ it "fails to update the settings with too long prefix" do
+ put api("/application/settings", admin), params: { personal_access_token_prefix: "prefix" * 10 }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ message = json_response["message"]
+ expect(message["personal_access_token_prefix"]).to include(a_string_matching("is too long"))
+ end
+
+ it "fails to update the settings with invalid characters in the prefix" do
+ put api("/application/settings", admin), params: { personal_access_token_prefix: "éñ" }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ message = json_response["message"]
+ expect(message["personal_access_token_prefix"]).to include(a_string_matching("can contain only letters of the Base64 alphabet"))
+ end
+ end
+ end
end
end
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index a0ff2fff0ef..a18c8f23ee7 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -93,6 +93,73 @@ RSpec.describe Ci::CreatePipelineService do
end
end
end
+
+ context 'with allow_failure and exit_codes', :aggregate_failures do
+ def find_job(name)
+ pipeline.builds.find_by(name: name)
+ end
+
+ let(:config) do
+ <<-EOY
+ job-1:
+ script: exit 42
+ allow_failure:
+ exit_codes: 42
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "master"
+ allow_failure: false
+
+ job-2:
+ script: exit 42
+ allow_failure:
+ exit_codes: 42
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "master"
+ allow_failure: true
+
+ job-3:
+ script: exit 42
+ allow_failure:
+ exit_codes: 42
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "master"
+ when: manual
+ EOY
+ end
+
+ it 'creates a pipeline' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly(
+ 'job-1', 'job-2', 'job-3'
+ )
+ end
+
+ it 'assigns job:allow_failure values to the builds' do
+ expect(find_job('job-1').allow_failure).to eq(false)
+ expect(find_job('job-2').allow_failure).to eq(true)
+ expect(find_job('job-3').allow_failure).to eq(false)
+ end
+
+ it 'removes exit_codes if allow_failure is specified' do
+ expect(find_job('job-1').options.dig(:allow_failure_criteria)).to be_nil
+ expect(find_job('job-2').options.dig(:allow_failure_criteria)).to be_nil
+ expect(find_job('job-3').options.dig(:allow_failure_criteria, :exit_codes)).to eq([42])
+ end
+
+ context 'with ci_allow_failure_with_exit_codes disabled' do
+ before do
+ stub_feature_flags(ci_allow_failure_with_exit_codes: false)
+ end
+
+ it 'does not persist allow_failure_criteria' do
+ expect(pipeline).to be_persisted
+
+ expect(find_job('job-1').options.key?(:allow_failure_criteria)).to be_falsey
+ expect(find_job('job-2').options.key?(:allow_failure_criteria)).to be_falsey
+ expect(find_job('job-3').options.key?(:allow_failure_criteria)).to be_falsey
+ end
+ end
+ end
end
context 'when workflow:rules are used' do
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index 4c8a45d3b90..dcbf494186a 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -504,6 +504,17 @@ RSpec.shared_examples 'wiki controller actions' do
end
end
+ describe '#git_access' do
+ render_views
+
+ it 'renders the git access page' do
+ get :git_access, params: routing_params
+
+ expect(response).to render_template('shared/wikis/git_access')
+ expect(response.body).to include(wiki.http_url_to_repo)
+ end
+ end
+
def redirect_to_wiki(wiki, page)
redirect_to(controller.wiki_page_path(wiki, page))
end
diff --git a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
index ddf223efd99..3e691862937 100644
--- a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
+++ b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
@@ -16,16 +16,6 @@ RSpec.describe 'shared/wikis/_sidebar.html.haml' do
expect(rendered).to have_link('Clone repository')
end
- context 'the wiki is not a project wiki' do
- it 'does not include the clone repository link' do
- allow(wiki).to receive(:container).and_return(create(:group))
-
- render
-
- expect(rendered).not_to have_link('Clone repository')
- end
- end
-
context 'the sidebar failed to load' do
before do
assign(:sidebar_error, Object.new)