Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-15 15:09:59 +00:00
parent e894595ad8
commit 0ff373dc41
93 changed files with 2473 additions and 221 deletions

View File

@ -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

View File

@ -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'

View File

@ -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>

View File

@ -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>

View File

@ -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 });

View File

@ -0,0 +1,8 @@
fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup {
id
web_url
full_path
full_name
status
import_target
}

View File

@ -0,0 +1,3 @@
mutation importGroup($sourceGroupId: String!) {
importGroup(sourceGroupId: $sourceGroupId) @client
}

View File

@ -0,0 +1,3 @@
mutation setNewName($newName: String!, $sourceGroupId: String!) {
setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client
}

View File

@ -0,0 +1,3 @@
mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) {
setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client
}

View File

@ -0,0 +1,6 @@
query availableNamespaces {
availableNamespaces @client {
id
full_path
}
}

View File

@ -0,0 +1,7 @@
#import "../fragments/bulk_import_source_group_item.fragment.graphql"
query bulkImportSourceGroups {
bulkImportSourceGroups @client {
...BulkImportSourceGroupItem
}
}

View File

@ -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;
});
}
}

View File

@ -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);
},
});
}

View File

@ -0,0 +1,4 @@
import { mountImportGroupsApp } from '~/import_entities/import_groups';
const mountElement = document.getElementById('import-groups-mount-element');
mountImportGroupsApp(mountElement);

View File

@ -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>

View File

@ -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;
}

View File

@ -186,6 +186,10 @@ module WikiActions
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def git_access
render 'shared/wikis/git_access'
end
private
def container

View File

@ -6,7 +6,4 @@ class Projects::WikisController < Projects::ApplicationController
alias_method :container, :project
feature_category :wiki
def git_access
end
end

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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? }

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -51,6 +51,9 @@
= _('Specify an e-mail address regex pattern to identify default internal users.')
= 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

View File

@ -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) } }

View File

@ -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.')

View File

@ -0,0 +1,5 @@
---
title: Support Git access for group wikis
merge_request: 45892
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Change the unique index on `security_findings` table
merge_request: 50046
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Configurable personal access token prefix
merge_request: 20968
author: 'Max Wittig & Diego Louzán'
type: added

View File

@ -0,0 +1,5 @@
---
title: Update projects_imported.total usage metric
merge_request: 49568
author:
type: fixed

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
6c8fc7904f50a792e10b5f1b0abe90ba21b1bdfd47430b3caa0df870c0a24079

View File

@ -0,0 +1 @@
bfb8ac3b697675bd4fca53273c6c6feb2f7a5659cbdaf57b9b4adb3e189b74ad

View File

@ -0,0 +1 @@
734ef1c319549df72bbbfe3acf93ca05f7a6c5547a1efdcaba780195181f5f9a

View File

@ -0,0 +1 @@
916f29e6ab89551fd785c3a8584c24b72d9002ada30d159e9ff826cb247199b5

View File

@ -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);

View File

@ -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. |

View File

@ -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 -->

View File

@ -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).

View File

@ -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

View File

@ -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'

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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(

View File

@ -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?
#
# 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)
project, redirected_path = find_project(wiki_path)
[project&.wiki, redirected_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

View File

@ -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

View File

@ -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 ""

View File

@ -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
{

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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);
});
});
});

View File

@ -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,
});
});
});
});

View File

@ -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);
});
});
});
});

View File

@ -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' },
];

View File

@ -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,
},
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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')) }
it_behaves_like '.find_by_full_path' do
let_it_be(:record) { nested_group }
end
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
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 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
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
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)